Branch data Line data Source code
1 : : /* zxcall.c - Web Service Client tool
2 : : * Copyright (c) 2010 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.
3 : : * This is confidential unpublished proprietary source code of the author.
4 : : * NO WARRANTY, not even implied warranties. Contains trade secrets.
5 : : * Distribution prohibited unless authorized in writing.
6 : : * Licensed under Apache License 2.0, see file COPYING.
7 : : * $Id: zxcot.c,v 1.5 2009-11-29 12:23:06 sampo Exp $
8 : : *
9 : : * 27.8.2009, created --Sampo
10 : : */
11 : :
12 : : #include "platform.h" /* for dirent.h */
13 : :
14 : : #include <string.h>
15 : : #include <stdio.h>
16 : : #include <fcntl.h>
17 : : #include <errno.h>
18 : :
19 : : #include "errmac.h"
20 : : #include "zx.h"
21 : : #include "zxid.h"
22 : : #include "zxidutil.h"
23 : : #include "zxidconf.h"
24 : : #include "c/zxidvers.h"
25 : : #include "c/zx-const.h"
26 : : #include "c/zx-ns.h"
27 : : #include "c/zx-data.h"
28 : :
29 : : char* help =
30 : : "zxcall - Web Service Client tool R" ZXID_REL "\n\
31 : : SAML 2.0 and ID-WSF 2.0 are standards for federated identity and web services.\n\
32 : : Copyright (c) 2010 Sampo Kellomaki (sampo@iki.fi), All Rights Reserved.\n\
33 : : NO WARRANTY, not even implied warranties. Licensed under Apache License v2.0\n\
34 : : See http://www.apache.org/licenses/LICENSE-2.0\n\
35 : : Send well researched bug reports to the author. Home: zxid.org\n\
36 : : \n\
37 : : Usage: zxcall [options] -s SESID -t SVCTYPE <soap_req_body.xml >soap_resp.xml\n\
38 : : zxcall [options] -a IDP USER:PW -t SVCTYPE <soap_req_body.xml >soap_resp.xml\n\
39 : : zxcall [options] -a IDP USER:PW -t SVCTYPE -nd # Discovery only\n\
40 : : zxcall [options] -a IDP USER:PW # Authentication only\n\
41 : : zxcall [options] -s SESID -im EID # Identity Mapping to EID\n\
42 : : zxcall [options] -s SESID -l # List session cache\n\
43 : : -c CONF Optional configuration string (default -c PATH=/var/zxid/)\n\
44 : : Most of the configuration is read from /var/zxid/zxid.conf\n\
45 : : -s SESID Session ID referring to a directory in /var/zxid/ses\n\
46 : : Use zxidhlo to do SSO and then cut and paste from there.\n\
47 : : -a IDP USER:PW Use Authentication service to authenticate the user and\n\
48 : : create session. IDP is IdP's Entity ID. This is alternative to -s\n\
49 : : -t SVCTYPE Service Type URI. Used for discovery. Mandatory (omitting -t\n\
50 : : causes authorization only mode, provided that -az was specified).\n\
51 : : -u URL Optional endpoint URL or ProviderID. Discovery must match this.\n\
52 : : -di DISCOOPTS Optional discovery options. Query string format.\n\
53 : : -din N Discovery index (default: 1=pick first).\n\
54 : : -az AZCREDS Optional authorization credentials. Query string format.\n\
55 : : N.B. For authorization to work PDP_URL configuration option is needed.\n\
56 : : -im DSTEID Map session's login identity to identity at some other SP using ID-WSF\n\
57 : : -nidmap DSTEID Map session's login identity to identity at some other SP using SAML\n\
58 : : -e SOAPBODY Pass SOAP body as argument (default is to read from STDIN)\n\
59 : : -b In response, only return content of SOAP body, omitting Envelope and Body.\n\
60 : : -nd Discovery only (you need to specify -t SVCTYPE as well)\n\
61 : : -n Dryrun. Do not actually make call. Instead print it to stdout.\n\
62 : : -l List EPR cache (you need to specify -s SEDID or -a as well)\n\
63 : : -v Verbose messages.\n\
64 : : -q Be extra quiet.\n\
65 : : -d Turn on debugging.\n\
66 : : -dc Dump config.\n\
67 : : -h This help message\n\
68 : : -- End of options\n\
69 : : \n\
70 : : echo '<query>Foo</query>' | zxcall -a https://idp.tas3.eu/zxididp?o=B user:pw -t urn:x-demo-svc\n\
71 : : \n";
72 : :
73 : : int dryrun = 0;
74 : : int verbose = 1;
75 : : int out_fmt = 0;
76 : : int din = 1;
77 : : int di_only = 0;
78 : : /* int ssos = 0; -nssos SSOS only (you need to specify -a IDP USER:PW as well)\n\ */
79 : : int listses = 0;
80 : : char* entid = 0;
81 : : char* idp = 0;
82 : : char* user = 0;
83 : : char* sid = 0;
84 : : char* svc = 0;
85 : : char* url = 0;
86 : : char* di = 0;
87 : : char* az = 0;
88 : : char* im_to = 0;
89 : : char* nidmap_to = 0;
90 : : char* bdy = 0;
91 : : zxid_conf* cf;
92 : :
93 : : /* Called by: main x8, zxcall_main, zxcot_main, zxdecode_main */
94 : : static void opt(int* argc, char*** argv, char*** env)
95 : 23 : {
96 : : struct zx_str* ss;
97 [ + - ]: 23 : if (*argc <= 1) return;
98 : :
99 : : while (1) {
100 : 99 : ++(*argv); --(*argc);
101 : :
102 [ + + + - ]: 99 : if (!(*argc) || ((*argv)[0][0] != '-')) break; /* normal exit from options loop */
103 : :
104 [ + + + + : 79 : switch ((*argv)[0][1]) {
+ + + + +
+ + + + +
+ ]
105 [ + - ]: 1 : case '-': if ((*argv)[0][2]) break;
106 : 1 : ++(*argv); --(*argc);
107 : : DD("End of options by --");
108 : 1 : return; /* -- ends the options */
109 : :
110 : : case 'a':
111 [ + + - ]: 18 : switch ((*argv)[0][2]) {
112 : : case '\0':
113 : 17 : ++(*argv); --(*argc);
114 [ + - ]: 17 : if ((*argc) < 2) break;
115 : 17 : idp = (*argv)[0];
116 : 17 : ++(*argv); --(*argc);
117 : 17 : user = (*argv)[0];
118 : 17 : continue;
119 : : case 'z':
120 : 1 : ++(*argv); --(*argc);
121 [ + - ]: 1 : if ((*argc) < 1) break;
122 : 1 : az = (*argv)[0];
123 : 1 : continue;
124 : : }
125 : 0 : break;
126 : :
127 : : case 'b':
128 [ + - ]: 9 : switch ((*argv)[0][2]) {
129 : : case '\0':
130 : 9 : ++out_fmt;
131 : 9 : continue;
132 : : }
133 : 0 : break;
134 : :
135 : : case 'c':
136 [ + - ]: 1 : switch ((*argv)[0][2]) {
137 : : case '\0':
138 : 1 : ++(*argv); --(*argc);
139 [ + - ]: 1 : if ((*argc) < 1) break;
140 : 1 : zxid_parse_conf(cf, (*argv)[0]);
141 : 1 : continue;
142 : : }
143 : 0 : break;
144 : :
145 : : case 'd':
146 [ + + + - ]: 13 : switch ((*argv)[0][2]) {
147 : : case '\0':
148 : 9 : ++zx_debug;
149 [ - + ]: 9 : if (zx_debug == 2)
150 : 0 : strncpy(zx_instance, "\t\e[43mzxcall\e[0m", sizeof(zx_instance));
151 : 9 : continue;
152 : : case 'i':
153 [ + + - ]: 2 : switch ((*argv)[0][3]) {
154 : : case '\0':
155 : 1 : ++(*argv); --(*argc);
156 [ + - ]: 1 : if ((*argc) < 1) break;
157 : 1 : di = (*argv)[0];
158 : 1 : continue;
159 : : case 'n':
160 : 1 : ++(*argv); --(*argc);
161 [ + - ]: 1 : if ((*argc) < 1) break;
162 : 1 : sscanf((*argv)[0], "%i", &din);
163 : 1 : continue;
164 : : }
165 : : case 'c':
166 : 2 : ss = zxid_show_conf(cf);
167 [ + + ]: 2 : if (verbose>1) {
168 : 1 : printf("\n======== CONF ========\n%.*s\n^^^^^^^^ CONF ^^^^^^^^\n",ss->len,ss->s);
169 : 1 : exit(0);
170 : : }
171 : 1 : fprintf(stderr, "\n======== CONF ========\n%.*s\n^^^^^^^^ CONF ^^^^^^^^\n",ss->len,ss->s);
172 : 1 : continue;
173 : : }
174 : 0 : break;
175 : :
176 : : case 'e':
177 [ + - ]: 9 : switch ((*argv)[0][2]) {
178 : : case '\0':
179 : 9 : ++(*argv); --(*argc);
180 [ + - ]: 9 : if ((*argc) < 1) break;
181 : 9 : bdy = (*argv)[0];
182 : 9 : continue;
183 : : }
184 : 0 : break;
185 : :
186 : : case 'i':
187 [ + - ]: 1 : switch ((*argv)[0][2]) {
188 : : case 'm':
189 : 1 : ++(*argv); --(*argc);
190 [ + - ]: 1 : if ((*argc) < 1) break;
191 : 1 : im_to = (*argv)[0];
192 : 1 : continue;
193 : : }
194 : 0 : break;
195 : :
196 : : case 'l':
197 [ + - ]: 2 : switch ((*argv)[0][2]) {
198 : : case '\0':
199 : 2 : ++listses;
200 : 2 : continue;
201 : : #if 0
202 : : case 'i':
203 : : if (!strcmp((*argv)[0],"-license")) {
204 : : extern char* license;
205 : : fprintf(stderr, license);
206 : : exit(0);
207 : : }
208 : : break;
209 : : #endif
210 : : }
211 : 0 : break;
212 : :
213 : : case 'n':
214 [ + + + - ]: 3 : switch ((*argv)[0][2]) {
215 : : case 'd':
216 : 1 : ++di_only;
217 : 1 : continue;
218 : : #if 0
219 : : case 's':
220 : : if (!strcmp((*argv)[0],"-nssos")) {
221 : : ++ssos;
222 : : continue;
223 : : }
224 : : break;
225 : : #endif
226 : : case 'i':
227 [ + - ]: 1 : if (!strcmp((*argv)[0],"-nidmap")) {
228 : 1 : ++(*argv); --(*argc);
229 [ + - ]: 1 : if ((*argc) < 1) break;
230 : 1 : nidmap_to = (*argv)[0];
231 : 1 : continue;
232 : : }
233 : 0 : break;
234 : : case '\0':
235 : 1 : ++dryrun;
236 : 1 : continue;
237 : : }
238 : 0 : break;
239 : :
240 : : case 'q':
241 [ + - ]: 1 : switch ((*argv)[0][2]) {
242 : : case '\0':
243 : 1 : verbose = 0;
244 : 1 : continue;
245 : : }
246 : 0 : break;
247 : :
248 : : case 's':
249 [ + - ]: 2 : switch ((*argv)[0][2]) {
250 : : case '\0':
251 : 2 : ++(*argv); --(*argc);
252 [ + - ]: 2 : if ((*argc) < 1) break;
253 : 2 : sid = (*argv)[0];
254 : 2 : continue;
255 : : }
256 : 0 : break;
257 : :
258 : : case 't':
259 [ + - ]: 13 : switch ((*argv)[0][2]) {
260 : : case '\0':
261 : 13 : ++(*argv); --(*argc);
262 [ + - ]: 13 : if ((*argc) < 1) break;
263 : 13 : svc = (*argv)[0];
264 : 13 : continue;
265 : : }
266 : 0 : break;
267 : :
268 : : case 'u':
269 [ + - ]: 1 : switch ((*argv)[0][2]) {
270 : : case '\0':
271 : 1 : ++(*argv); --(*argc);
272 [ + - ]: 1 : if ((*argc) < 1) break;
273 : 1 : url = (*argv)[0];
274 : 1 : continue;
275 : : }
276 : 0 : break;
277 : :
278 : : case 'v':
279 [ + - ]: 4 : switch ((*argv)[0][2]) {
280 : : case '\0':
281 : 4 : ++verbose;
282 : 4 : continue;
283 : : }
284 : : break;
285 : :
286 : : }
287 : : /* fall thru means unrecognized flag */
288 [ + - ]: 1 : if (*argc)
289 : 1 : fprintf(stderr, "Unrecognized flag `%s'\n", (*argv)[0]);
290 : 2 : help:
291 [ + + ]: 2 : if (verbose>1) {
292 : 1 : printf(help);
293 : 1 : exit(0);
294 : : }
295 : 1 : fprintf(stderr, help);
296 : : /*fprintf(stderr, "version=0x%06x rel(%s)\n", zxid_version(), zxid_version_str());*/
297 : 1 : exit(3);
298 : 76 : }
299 [ + + + + ]: 20 : if (!sid && !idp) {
300 : 1 : fprintf(stderr, "MUST specify either -s or -a\n");
301 : 1 : goto help;
302 : : }
303 : : }
304 : :
305 : :
306 : : /*() List session and especially its EPR cache to stdout.
307 : : * Typical name: /var/zxid/ses/SESID/SVCTYPE,SHA1
308 : : *
309 : : * cf:: ZXID configuration object, also used for memory allocation
310 : : * ses:: Session object in whose EPR cache the file is searched
311 : : *
312 : : * See also: zxid_find_epr() */
313 : :
314 : : /* Called by: zxcall_main */
315 : : int zxid_print_session(zxid_conf* cf, zxid_ses* ses)
316 : 1 : {
317 : : struct zx_root_s* r;
318 : 1 : int epr_len, din = 0;
319 : : char path[ZXID_MAX_BUF];
320 : : char* epr_buf; /* MUST NOT come from stack. */
321 : : DIR* dir;
322 : : struct dirent * de;
323 : : zxid_epr* epr;
324 : : struct zx_a_Metadata_s* md;
325 : : struct zx_str* ss;
326 : :
327 : 1 : D_INDENT("lstses: ");
328 : :
329 [ - + ]: 1 : if (!name_from_path(path, sizeof(path), "%s" ZXID_SES_DIR "%s", cf->path, ses->sid)) {
330 : 0 : D_DEDENT("lstses: ");
331 : 0 : return 0;
332 : : }
333 : :
334 : 1 : printf("SESID: %s\nSESDIR: %s\n", ses->sid, path);
335 : 1 : dir = opendir(path);
336 [ - + ]: 1 : if (!dir) {
337 : 0 : perror("opendir to find epr in session");
338 : 0 : ERR("Opening session for find epr by opendir failed path(%s) sesptr=%p", path, ses);
339 : 0 : D_DEDENT("lstses: ");
340 : 0 : return 0;
341 : : }
342 : :
343 [ + + ]: 7 : while (de = readdir(dir)) {
344 [ - + # # ]: 5 : D("%d Considering file(%s)", din, de->d_name);
345 [ + + ]: 5 : if (de->d_name[0] == '.') /* . .. and "hidden" files */
346 : 3 : continue;
347 [ - + ]: 2 : if (de->d_name[strlen(de->d_name)-1] == '~') /* Ignore backups from hand edited EPRs. */
348 : 0 : continue;
349 [ - + # # ]: 2 : D("%d Checking EPR content file(%s)", din, de->d_name);
350 : 2 : epr_buf = read_all_alloc(cf->ctx, "lstses", 1, &epr_len,
351 : : "%s" ZXID_SES_DIR "%s/%s", cf->path, ses->sid, de->d_name);
352 [ - + ]: 2 : if (!epr_buf)
353 : 0 : continue;
354 : :
355 : 2 : r = zx_dec_zx_root(cf->ctx, epr_len, epr_buf, "lstses");
356 [ + - - + ]: 2 : if (!r || !r->EndpointReference) {
357 : 0 : ERR("No EPR found. Failed to parse epr_buf(%.*s)", epr_len, epr_buf);
358 : 0 : continue;
359 : : }
360 : 2 : epr = r->EndpointReference;
361 : 2 : ZX_FREE(cf->ctx, r);
362 : :
363 : 2 : md = epr->Metadata;
364 [ + - + - : 2 : if (!md || !ZX_SIMPLE_ELEM_CHK(md->ServiceType)) {
+ - + - +
- + - -
+ ]
365 : 0 : ERR("No Metadata %p or ServiceType. Failed to parse epr_buf(%.*s)", md, epr_len, epr_buf);
366 : 0 : printf("EPR %d no service type\n", ++din);
367 : : } else {
368 [ + - + - : 2 : ss = ZX_GET_CONTENT(md->ServiceType);
+ - ]
369 : 2 : printf("EPR %d SvcType: %.*s\n", ++din, ss->len, ss->s);
370 : : }
371 : 2 : ss = zxid_get_epr_address(cf, epr);
372 [ + - + - ]: 2 : printf(" URL: %.*s\n", ss?ss->len:0, ss?ss->s:"");
373 : 2 : ss = zxid_get_epr_entid(cf, epr);
374 [ + - + - ]: 2 : printf(" EntityID: %.*s\n", ss?ss->len:0, ss?ss->s:"");
375 : 2 : ss = zxid_get_epr_desc(cf, epr);
376 [ + - + - ]: 2 : printf(" Description: %.*s\n", ss?ss->len:0, ss?ss->s:"");
377 : : }
378 : 1 : ZX_FREE(cf->ctx, epr_buf);
379 : 1 : closedir(dir);
380 : 1 : D_DEDENT("lstses: ");
381 : 1 : return 0;
382 : : }
383 : :
384 : : #ifndef zxcall_main
385 : : #define zxcall_main main
386 : : #endif
387 : :
388 : : /*() Web Services Client tool */
389 : :
390 : : /* Called by: */
391 : : int zxcall_main(int argc, char** argv, char** env)
392 : 23 : {
393 : : int siz, got, n;
394 : : char* p;
395 : : struct zx_str* ss;
396 : : zxid_ses* ses;
397 : : zxid_entity* idp_meta;
398 : : zxid_epr* epr;
399 : :
400 : 23 : strncpy(zx_instance, "\tzxcall", sizeof(zx_instance));
401 : 23 : cf = zxid_new_conf_to_cf(0);
402 : 23 : opt(&argc, &argv, &env);
403 : :
404 [ + + ]: 20 : if (sid) {
405 [ - + # # ]: 2 : D("Existing session sesid(%s)", sid);
406 : 2 : ses = zxid_fetch_ses(cf, sid);
407 [ + - ]: 2 : if (!ses) {
408 : 2 : ERR("Session not found or error in session sesid(%s)", sid);
409 : 2 : return 1;
410 : : }
411 : : } else {
412 [ + + - + ]: 18 : D("Obtain session from authentication service(%s)", idp);
413 : 18 : idp_meta = zxid_get_ent(cf, idp);
414 [ + + ]: 18 : if (!idp_meta) {
415 : 1 : ERR("IdP metadata not found and could not be fetched. idp(%s)", idp);
416 : 1 : return 1;
417 : : }
418 [ + + + - ]: 17 : for (p = user; !ONE_OF_2(*p, ':', 0); ++p) ;
419 [ + - ]: 17 : if (*p)
420 : 17 : *p++ = 0;
421 : 17 : ses = zxid_as_call(cf, idp_meta, user, p);
422 [ + + ]: 17 : if (!ses) {
423 : 2 : ERR("Login using Authentication Service failed idp(%s)", idp);
424 : 2 : return 1;
425 : : }
426 : : }
427 : :
428 [ + + ]: 15 : if (listses)
429 : 1 : return zxid_print_session(cf, ses);
430 : :
431 [ + + ]: 14 : if (im_to) {
432 [ - + # # ]: 1 : D("ID-WSF Map to identity at eid(%s)", im_to);
433 : 1 : zxid_map_identity_token(cf, ses, im_to, 0);
434 : : //printf("%.*s\n", ZX_GET_CONTENT_LEN(nameid), ZX_GET_CONTENT_S(nameid));
435 : 1 : return 0;
436 : : }
437 : :
438 [ + + ]: 13 : if (nidmap_to) {
439 [ - + # # ]: 1 : D("SAML Map to identity at eid(%s)", nidmap_to);
440 : 1 : zxid_nidmap_identity_token(cf, ses, nidmap_to, 0);
441 : : //printf("%.*s\n", ZX_GET_CONTENT_LEN(nameid), ZX_GET_CONTENT_S(nameid));
442 : 1 : return 0;
443 : : }
444 : :
445 [ + + ]: 12 : if (di_only) {
446 [ - + # # : 1 : D("Discover only. svctype(%s), index=%d", STRNULLCHK(svc), din);
# # ]
447 : 1 : epr = zxid_get_epr(cf, ses, svc, url, di, 0 /*action*/, din);
448 [ - + ]: 1 : if (!epr) {
449 [ # # ]: 0 : ERR("Discovery failed to find any epr of service type(%s)", STRNULLCHK(svc));
450 : 0 : return 3;
451 : : }
452 : 1 : ss = zxid_get_epr_address(cf, epr);
453 [ + - + - : 1 : printf("Found epr. index=%d service type(%s)\n URL: %.*s\n",
+ - ]
454 : : din, STRNULLCHK(svc), ss?ss->len:0, ss?ss->s:"");
455 : 1 : ss = zxid_get_epr_entid(cf, epr);
456 [ + - + - ]: 1 : printf(" EntityID: %.*s\n", ss?ss->len:0, ss?ss->s:"");
457 : 1 : ss = zxid_get_epr_desc(cf, epr);
458 [ + - + - ]: 1 : printf(" Description: %.*s\n", ss?ss->len:0, ss?ss->s:"");
459 : 1 : return 0;
460 : : }
461 : :
462 [ + + ]: 11 : if (svc) {
463 [ + + - + ]: 10 : D("Call service svctype(%s)", svc);
464 [ + + ]: 10 : if (!bdy) {
465 [ + - ]: 3 : if (verbose)
466 : 3 : fprintf(stderr, "Reading SOAP request body from stdin...\n");
467 : 3 : siz = 4096;
468 : 3 : p = bdy = ZX_ALLOC(cf->ctx, siz);
469 : : while (1) {
470 : 3 : n = read_all_fd(0, p, siz+bdy-p-1, &got);
471 [ - + ]: 3 : if (n == -1) {
472 : 0 : perror("reading SOAP req from stdin");
473 : 0 : break;
474 : : }
475 : 3 : p += got;
476 [ - + ]: 3 : if (got < siz+bdy-p-1) break;
477 : 0 : siz += 60*1024;
478 [ # # # # : 0 : REALLOCN(bdy, siz);
# # # # #
# # # #
# ]
479 : 0 : }
480 : 3 : *p = 0;
481 : : }
482 [ - + ]: 10 : if (dryrun) {
483 [ # # ]: 0 : if (verbose)
484 : 0 : fprintf(stderr, "Dryrun. Call aborted.\n");
485 : 0 : return 0;
486 : : }
487 [ + - ]: 10 : if (verbose)
488 : 10 : fprintf(stderr, "Calling...\n");
489 : :
490 : 10 : ss = zxid_call(cf, ses, svc, url, di, az, bdy);
491 [ + + - + ]: 10 : if (!ss || !ss->s) {
492 : 1 : ERR("Call failed %p", ss);
493 : 1 : return 2;
494 : : }
495 [ + - ]: 9 : if (verbose)
496 : 9 : fprintf(stderr, "Call returned %d bytes.\n", ss->len);
497 [ + + ]: 9 : if (out_fmt) {
498 : 6 : p = zxid_extract_body(cf, ss->s);
499 : 6 : printf("%s", p);
500 : : } else
501 : 3 : printf("%.*s", ss->len, ss->s);
502 [ + - ]: 1 : } else if (az) {
503 [ - + # # ]: 1 : D("Call Az(%s)", az);
504 [ - + ]: 1 : if (dryrun) {
505 [ # # ]: 0 : if (verbose)
506 : 0 : fprintf(stderr, "Dryrun. zxid_az() aborted.\n");
507 : 0 : return 0;
508 : : }
509 [ + - ]: 1 : if (zxid_az_cf_ses(cf, az, ses)) {
510 [ + - ]: 1 : if (verbose)
511 : 1 : fprintf(stderr, "Permit.\n");
512 : 1 : return 0;
513 : : } else {
514 [ # # ]: 0 : if (verbose)
515 : 0 : fprintf(stderr, "Deny.\n");
516 : 0 : return 1;
517 : : }
518 : : } else {
519 [ # # # # ]: 0 : D("Neither service type (-t) nor -az supplied. Performed only authentication. %d",0);
520 [ # # ]: 0 : if (verbose)
521 : 0 : fprintf(stderr, "Authentication only.\n");
522 : : }
523 : 9 : return 0;
524 : : }
525 : :
526 : : /* EOF -- zxcall.c */
|