otp: Redirect with cookie and test it
[libisds.git] / src / soap.c
bloba16046b21a91401156e99e268c4ea100f883a6b4
1 #define _XOPEN_SOURCE 500 /* strdup from string.h */
2 #include "isds_priv.h"
3 #include "soap.h"
4 #include "utils.h"
5 #include <stdlib.h>
6 #include <string.h>
8 /* Private structure for write_body() call back */
9 struct soap_body {
10 void *data;
11 size_t length;
14 /* Private structure for write_header() call back */
15 struct auth_headers {
16 _Bool is_complete; /* Response has finished, next iteration is new
17 response, values become obsolete. */
18 char *last_header; /* Temporary storage for previous unfinished header */
19 char *method; /* WWW-Authenticate value */
20 char *code; /* X-Response-message-code value */
21 isds_otp_resolution resolution; /* Decoded .code member */
22 char *message; /* X-Response-message-text value */
23 char *redirect; /* Redirect URL */
27 /* Deallocate content of struct auth_headers */
28 static void auth_headers_free(struct auth_headers *headers) {
29 zfree(headers->last_header);
30 zfree(headers->method);
31 zfree(headers->code);
32 zfree(headers->message);
33 zfree(headers->redirect);
37 /* If given @line is HTTP header of @name,
38 * return pointer to the header value. Otherwise return NULL.
39 * @name is header name without name---value separator, terminated with 0. */
40 static const char *header_value(const char *line, const char *name) {
41 const char *value;
42 if (line == NULL || name == NULL) return NULL;
44 for (value = line; ; value++, name++) {
45 if (*value == '\0') return NULL; /* Line too short */
46 if (*name == '\0') break; /* Name matches */
47 if (*name != *value) return NULL; /* Name does not match */
50 /* Check separator. RFC2616, section 4.2 requires collon only. */
51 if (*value++ != ':') return NULL;
53 return value;
57 /* Decode HTTP header value per RFC ???
58 * @encoded_value is encoded HTTP header value terminated with NUL. It can
59 * contain HTTP LWS separators that will be replaced with a space.
60 * @return newly allocated decoded value without EOL, or return NULL */
61 static char *decode_header_value(const char *encoded_value) {
62 char *decoded = NULL, *decoded_cursor;
63 size_t content_length;
64 _Bool text_started = 0, lws_seen = 0;
66 if (encoded_value == NULL) return NULL;
67 content_length = strlen(encoded_value);
69 decoded = malloc(content_length + 1);
70 if (decoded == NULL) {
71 /* ENOMEM */
72 return NULL;
75 /* Decode */
76 /* RFC 2616, section 4.2: Remove surrounding LWS, replace inner ones with
77 * a space. */
78 /* FIXME: Implement the RFC (?=UTF-8?B?...?) */
79 for(decoded_cursor = decoded; *encoded_value; encoded_value++) {
80 if (*encoded_value == '\r' || *encoded_value == '\n' ||
81 *encoded_value == '\t' || *encoded_value == ' ') {
82 lws_seen = 1;
83 continue;
85 if (lws_seen) {
86 lws_seen = 0;
87 if (text_started) *(decoded_cursor++) = ' ';
89 *(decoded_cursor++) = *encoded_value;
90 text_started = 1;
92 *decoded_cursor = '\0';
94 return decoded;
98 /* Return true, if server requests OTP authorization method that client
99 * requested. Otherwise return false.
100 * @client_method is method client requested
101 * @server_method is value of WWW-Authenticate header */
102 /*static _Bool otp_method_matches(const isds_otp_method client_method,
103 const char *server_method) {
104 char *method_name = NULL;
106 switch (client_method) {
107 case OTP_HASH: method_name = "hotp"; break;
108 case OTP_TIME: method_name = "totp"; break;
109 default: return 0;
112 if (!strncmp(server_method, method_name, 4) && (
113 server_method[4] == '\0' || server_method[4] == ' ' ||
114 server_method[4] == '\t'))
115 return 1;
116 return 0;
120 /* Convert UTF-8 @string to HTTP OTP resolution enum type.
121 * @Return corresponding value or OTP_RESOLUTION_UNKNOWN if @string is not
122 * defined or unknown value. */
123 static isds_otp_resolution string2isds_otp_resolution(const char *string) {
124 if (string == NULL)
125 return OTP_RESOLUTION_UNKNOWN;
126 else if (!strcmp(string, "authentication.info.totpSended"))
127 return OTP_RESOLUTION_TOTP_SENT;
128 else if (!strcmp(string, "authentication.error.userIsNotAuthenticated"))
129 return OTP_RESOLUTION_BAD_AUTHENTICATION;
130 else if (!strcmp(string, "authentication.error.intruderDetected"))
131 return OTP_RESOLUTION_ACCESS_BLOCKED;
132 else if (!strcmp(string, "authentication.error.paswordExpired"))
133 return OTP_RESOLUTION_PASSWORD_EXPIRED;
134 else if (!strcmp(string, "authentication.info.cannotSendQuickly"))
135 return OTP_RESOLUTION_TO_FAST;
136 else if (!strcmp(string, "authentication.error.badRole"))
137 return OTP_RESOLUTION_UNAUTHORIZED;
138 else if (!strcmp(string, "authentication.info.totpNotSended"))
139 return OTP_RESOLUTION_TOTP_NOT_SENT;
140 else
141 return OTP_RESOLUTION_UNKNOWN;
145 /* Close connection to server and destroy CURL handle associated
146 * with @context */
147 _hidden isds_error _isds_close_connection(struct isds_ctx *context) {
148 if (!context) return IE_INVALID_CONTEXT;
150 if (context->curl) {
151 curl_easy_cleanup(context->curl);
152 context->curl = NULL;
153 isds_log(ILF_HTTP, ILL_DEBUG, _("Connection to server %s closed\n"),
154 context->url);
155 return IE_SUCCESS;
156 } else {
157 return IE_CONNECTION_CLOSED;
162 /* CURL call back function called when chunk of HTTP response body is available.
163 * @buffer points to new data
164 * @size * @nmemb is length of the chunk in bytes. Zero means empty body.
165 * @userp is private structure.
166 * Must return the length of the chunk, otherwise CURL will signal
167 * CURL_WRITE_ERROR. */
168 static size_t write_body(void *buffer, size_t size, size_t nmemb, void *userp) {
169 struct soap_body *body = (struct soap_body *) userp;
170 void *new_data;
172 /* FIXME: Check for (size * nmemb + body->lengt) !> SIZE_T_MAX.
173 * Precompute the product then. */
175 if (!body) return 0; /* This should never happen */
176 if (0 == (size * nmemb)) return 0; /* Empty body */
178 new_data = realloc(body->data, body->length + size * nmemb);
179 if (!new_data) return 0;
181 memcpy(new_data + body->length, buffer, size * nmemb);
183 body->data = new_data;
184 body->length += size * nmemb;
186 return (size * nmemb);
190 /* CURL call back function called when a HTTP response header is available.
191 * This is called for each header even if reply consists of more responses.
192 * @buffer points to new header (no zero terminator, but HTTP EOL is included)
193 * @size * @nmemb is length of the header in bytes
194 * @userp is private structure.
195 * Must return the length of the header, otherwise CURL will signal
196 * CURL_WRITE_ERROR. */
197 static size_t write_header(void *buffer, size_t size, size_t nmemb, void *userp) {
198 struct auth_headers *headers = (struct auth_headers *) userp;
199 size_t length;
200 const char *value;
202 /* FIXME: Check for (size * nmemb) !> SIZE_T_MAX.
203 * Precompute the product then. */
204 length = size * nmemb;
206 if (NULL == headers) return 0; /* This should never happen */
207 if (0 == length) {
208 /* ??? Is this the empty line delimiter? */
209 return 0; /* Empty headers */
212 /* New response, invalide authentication headers. */
213 /* XXX: Chunked encoding trailer is not supported */
214 if (headers->is_complete) auth_headers_free(headers);
216 /* Append continuation to multi-line header */
217 if (*(char *)buffer == ' ' || *(char *)buffer == '\t') {
218 if (headers->last_header != NULL) {
219 size_t old_length = strlen(headers->last_header);
220 char *longer_header = realloc(headers->last_header, old_length + length);
221 if (longer_header == NULL) {
222 /* ENOMEM */
223 return 0;
225 strncpy(longer_header + old_length, (char*)buffer + 1, length - 1);
226 longer_header[old_length + length - 1] = '\0';
227 headers->last_header = longer_header;
228 } else {
229 /* Invalid continuation without starting header will be skipped. */
230 isds_log(ILF_HTTP, ILL_WARNING,
231 _("HTTP header continuation without starting header has "
232 "been encountered. Skipping invalid HTTP response "
233 "line.\n"));
235 goto leave;
238 /* Decode last header */
239 value = header_value(headers->last_header, "WWW-Authenticate");
240 if (value != NULL) {
241 free(headers->method);
242 if (NULL == (headers->method = decode_header_value(value))) {
243 /* TODO: Set IE_NOMEM to context */
244 return 0;
246 goto store;
249 value = header_value(headers->last_header, "X-Response-message-code");
250 if (value != NULL) {
251 free(headers->code);
252 if (NULL == (headers->code = decode_header_value(value))) {
253 /* TODO: Set IE_NOMEM to context */
254 return 0;
256 goto store;
259 value = header_value(headers->last_header, "X-Response-message-text");
260 if (value != NULL) {
261 free(headers->message);
262 if (NULL == (headers->message = decode_header_value(value))) {
263 /* TODO: Set IE_NOMEM to context */
264 return 0;
266 goto store;
269 store:
270 /* Last header decoded, free it */
271 zfree(headers->last_header);
273 if (!strncmp(buffer, "\r\n", length)) {
274 /* Current line is header---body separator */
275 headers->is_complete = 1;
276 goto leave;
277 } else {
278 /* Current line is new header, store it */
279 headers->last_header = malloc(length + 1);
280 if (headers->last_header == NULL) {
281 /* TODO: Set IE_NOMEM to context */
282 return 0;
284 memcpy(headers->last_header, buffer, length);
285 headers->last_header[length] = '\0';
288 leave:
289 return (length);
293 /* CURL progress callback proxy to rearrange arguments.
294 * @curl_data is session context */
295 static int progress_proxy(void *curl_data, double download_total,
296 double download_current, double upload_total, double upload_current) {
297 struct isds_ctx *context = (struct isds_ctx *) curl_data;
298 int abort = 0;
300 if (context && context->progress_callback) {
301 abort = context->progress_callback(
302 upload_total, upload_current,
303 download_total, download_current,
304 context->progress_callback_data);
305 if (abort) {
306 isds_log(ILF_HTTP, ILL_INFO,
307 _("Application aborted HTTP transfer"));
311 return abort;
315 /* CURL call back function called when curl has something to log.
316 * @curl is cURL context
317 * @type is cURL log facility
318 * @buffer points to log data, XXX: not zero-terminated
319 * @size is length of log data
320 * @userp is private structure.
321 * Must return 0. */
322 static int log_curl(CURL *curl, curl_infotype type, char *buffer, size_t size,
323 void *userp) {
324 if (!buffer || 0 == size) return 0;
325 if (type == CURLINFO_TEXT || type == CURLINFO_HEADER_IN ||
326 type == CURLINFO_HEADER_OUT)
327 isds_log(ILF_HTTP, ILL_DEBUG, "%*s", size, buffer);
328 return 0;
332 /* Do HTTP request.
333 * @context holds the base URL,
334 * @url is a (CGI) file of SOAP URL,
335 * @request is body for POST request
336 * @request_length is length of @request in bytes
337 * @reponse is automatically reallocated() buffer to fit HTTP response with
338 * @response_length (does not need to match allocated memory exactly). You must
339 * free() the @response.
340 * @mime_type is automatically allocated MIME type send by server (*NULL if not
341 * sent). Set NULL if you don't care.
342 * @charset is charset of the body signaled by server. The same constrains
343 * like on @mime_type apply.
344 * @http_code is final HTTP code returned by server. This can be 200, 401, 500
345 * or any other one. Pass NULL if you don't interest.
346 * In case of error, the response memory, MIME type, charset and length will be
347 * deallocated and zeroed automatically. Thus be sure they are preallocated or
348 * they points to NULL.
349 * @response_otp_headers is pre-allocated structure for OTP authentication
350 * headers sent by server. Members must be valid pointers or NULLs.
351 * Pass NULL if you don't interest.
352 * Be ware that successful return value does not mean the HTTP request has
353 * been accepted by the server. You must consult @http_code. OTOH, failure
354 * return value means the request could not been sent (e.g. SSL error).
355 * Side effect: message buffer */
356 static isds_error http(struct isds_ctx *context, const char *url,
357 const void *request, const size_t request_length,
358 void **response, size_t *response_length,
359 char **mime_type, char **charset, long *http_code,
360 struct auth_headers *response_otp_headers) {
362 CURLcode curl_err;
363 isds_error err = IE_SUCCESS;
364 struct soap_body body;
365 char *content_type;
366 struct curl_slist *headers = NULL;
369 if (!context) return IE_INVALID_CONTEXT;
370 if (!url) return IE_INVAL;
371 if (request_length > 0 && !request) return IE_INVAL;
372 if (!response || !response_length) return IE_INVAL;
374 /* Clean authentication headers */
376 /* Set the body here to allow deallocation in leave block */
377 body.data = *response;
378 body.length = 0;
380 /* Set Request-URI */
381 curl_err = curl_easy_setopt(context->curl, CURLOPT_URL, url);
383 /* Set TLS options */
384 if (!curl_err && context->tls_verify_server) {
385 if (!*context->tls_verify_server)
386 isds_log(ILF_SEC, ILL_WARNING,
387 _("Disabling server identity verification. "
388 "That was your decision.\n"));
389 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSL_VERIFYPEER,
390 (*context->tls_verify_server)? 1L : 0L);
391 if (!curl_err) {
392 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSL_VERIFYHOST,
393 (*context->tls_verify_server)? 2L : 0L);
396 if (!curl_err && context->tls_ca_file) {
397 isds_log(ILF_SEC, ILL_INFO,
398 _("CA certificates will be searched in `%s' file since now\n"),
399 context->tls_ca_file);
400 curl_err = curl_easy_setopt(context->curl, CURLOPT_CAINFO,
401 context->tls_ca_file);
403 if (!curl_err && context->tls_ca_dir) {
404 isds_log(ILF_SEC, ILL_INFO,
405 _("CA certificates will be searched in `%s' directory "
406 "since now\n"), context->tls_ca_dir);
407 curl_err = curl_easy_setopt(context->curl, CURLOPT_CAPATH,
408 context->tls_ca_dir);
410 if (!curl_err && context->tls_crl_file) {
411 #if HAVE_DECL_CURLOPT_CRLFILE /* Since curl-7.19.0 */
412 isds_log(ILF_SEC, ILL_INFO,
413 _("CRLs will be searched in `%s' file since now\n"),
414 context->tls_crl_file);
415 curl_err = curl_easy_setopt(context->curl, CURLOPT_CRLFILE,
416 context->tls_crl_file);
417 #else
418 isds_log(ILF_SEC, ILL_WARNING,
419 _("Your curl library cannot pass certificate revocation "
420 "list to cryptographic library.\n"
421 "Make sure cryptographic library default setting "
422 "delivers proper CRLs,\n"
423 "or upgrade curl.\n"));
424 #endif /* not HAVE_DECL_CURLOPT_CRLFILE */
428 /* Set credentials */
429 #if HAVE_DECL_CURLOPT_USERNAME /* Since curl-7.19.1 */
430 if (!curl_err && context->username) {
431 curl_err = curl_easy_setopt(context->curl, CURLOPT_USERNAME,
432 context->username);
434 if (!curl_err && context->password) {
435 curl_err = curl_easy_setopt(context->curl, CURLOPT_PASSWORD,
436 context->password);
438 #else
439 if (!curl_err && (context->username || context->password)) {
440 char *userpwd =
441 _isds_astrcat3(context->username, ":", context->password);
442 if (!userpwd) {
443 isds_log_message(context, _("Could not pass credentials to CURL"));
444 err = IE_NOMEM;
445 goto leave;
447 curl_err = curl_easy_setopt(context->curl, CURLOPT_USERPWD, userpwd);
448 free(userpwd);
450 #endif /* not HAVE_DECL_CURLOPT_USERNAME */
452 /* Set PKI credentials */
453 if (!curl_err && (context->pki_credentials)) {
454 if (context->pki_credentials->engine) {
455 /* Select SSL engine */
456 isds_log(ILF_SEC, ILL_INFO,
457 _("Cryptographic engine `%s' will be used for "
458 "key or certificate\n"),
459 context->pki_credentials->engine);
460 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLENGINE,
461 context->pki_credentials->engine);
464 if (!curl_err) {
465 /* Select certificate format */
466 #if HAVE_DECL_CURLOPT_SSLCERTTYPE /* since curl-7.9.3 */
467 if (context->pki_credentials->certificate_format ==
468 PKI_FORMAT_ENG) {
469 /* XXX: It's valid to have certificate in engine without name.
470 * Engines can select certificate according private key and
471 * vice versa. */
472 if (context->pki_credentials->certificate)
473 isds_log(ILF_SEC, ILL_INFO, _("Client `%s' certificate "
474 "will be read from `%s' engine\n"),
475 context->pki_credentials->certificate,
476 context->pki_credentials->engine);
477 else
478 isds_log(ILF_SEC, ILL_INFO, _("Client certificate "
479 "will be read from `%s' engine\n"),
480 context->pki_credentials->engine);
481 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLCERTTYPE,
482 "ENG");
483 } else if (context->pki_credentials->certificate) {
484 isds_log(ILF_SEC, ILL_INFO, _("Client %s certificate "
485 "will be read from `%s' file\n"),
486 (context->pki_credentials->certificate_format ==
487 PKI_FORMAT_DER) ? _("DER") : _("PEM"),
488 context->pki_credentials->certificate);
489 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLCERTTYPE,
490 (context->pki_credentials->certificate_format ==
491 PKI_FORMAT_DER) ? "DER" : "PEM");
493 #else
494 if ((context->pki_credentials->certificate_format ==
495 PKI_FORMAT_ENG ||
496 context->pki_credentials->certificate))
497 isds_log(ILF_SEC, ILL_WARNING,
498 _("Your curl library cannot distinguish certificate "
499 "formats. Make sure your cryptographic library\n"
500 "understands your certificate file by default, "
501 "or upgrade curl.\n"));
502 #endif /* not HAVE_DECL_CURLOPT_SSLCERTTYPE */
505 if (!curl_err && context->pki_credentials->certificate) {
506 /* Select certificate */
507 if (!curl_err)
508 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLCERT,
509 context->pki_credentials->certificate);
512 if (!curl_err) {
513 /* Select key format */
514 if (context->pki_credentials->key_format == PKI_FORMAT_ENG) {
515 if (context->pki_credentials->key)
516 isds_log(ILF_SEC, ILL_INFO, _("Client private key `%s' "
517 "from `%s' engine will be used\n"),
518 context->pki_credentials->key,
519 context->pki_credentials->engine);
520 else
521 isds_log(ILF_SEC, ILL_INFO, _("Client private key "
522 "from `%s' engine will be used\n"),
523 context->pki_credentials->engine);
524 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLKEYTYPE,
525 "ENG");
526 } else if (context->pki_credentials->key) {
527 isds_log(ILF_SEC, ILL_INFO, _("Client %s private key will be "
528 "read from `%s' file\n"),
529 (context->pki_credentials->key_format ==
530 PKI_FORMAT_DER) ? _("DER") : _("PEM"),
531 context->pki_credentials->key);
532 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLKEYTYPE,
533 (context->pki_credentials->key_format ==
534 PKI_FORMAT_DER) ? "DER" : "PEM");
537 if (!curl_err)
538 /* Select key */
539 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSLKEY,
540 context->pki_credentials->key);
542 if (!curl_err) {
543 /* Pass key pass-phrase */
544 #if HAVE_DECL_CURLOPT_KEYPASSWD /* since curl-7.16.5 */
545 curl_err = curl_easy_setopt(context->curl,
546 CURLOPT_KEYPASSWD,
547 context->pki_credentials->passphrase);
548 #elif HAVE_DECL_CURLOPT_SSLKEYPASSWD /* up to curl-7.16.4 */
549 curl_err = curl_easy_setopt(context->curl,
550 CURLOPT_SSLKEYPASSWD,
551 context->pki_credentials->passphrase);
552 #else /* up to curl-7.9.2 */
553 curl_err = curl_easy_setopt(context->curl,
554 CURLOPT_SSLCERTPASSWD,
555 context->pki_credentials->passphrase);
556 #endif
561 /* Set authorization cookie for OTP session */
562 if (!curl_err && context->otp != NULL) {
563 isds_log(ILF_SEC, ILL_INFO,
564 _("Cookies will be stored and send "
565 "because context has been authorized by OTP.\n"));
566 curl_err = curl_easy_setopt(context->curl, CURLOPT_COOKIEFILE, "");
569 /* Set timeout */
570 if (!curl_err) {
571 curl_err = curl_easy_setopt(context->curl, CURLOPT_NOSIGNAL, 1);
573 if (!curl_err && context->timeout) {
574 #if HAVE_DECL_CURLOPT_TIMEOUT_MS /* Since curl-7.16.2 */
575 curl_err = curl_easy_setopt(context->curl, CURLOPT_TIMEOUT_MS,
576 context->timeout);
577 #else
578 curl_err = curl_easy_setopt(context->curl, CURLOPT_TIMEOUT,
579 context->timeout / 1000);
580 #endif /* not HAVE_DECL_CURLOPT_TIMEOUT_MS */
583 /* Register callback */
584 if (context->progress_callback) {
585 if (!curl_err) {
586 curl_err = curl_easy_setopt(context->curl, CURLOPT_NOPROGRESS, 0);
588 if (!curl_err) {
589 curl_err = curl_easy_setopt(context->curl,
590 CURLOPT_PROGRESSFUNCTION, progress_proxy);
592 if (!curl_err) {
593 curl_err = curl_easy_setopt(context->curl, CURLOPT_PROGRESSDATA,
594 context);
598 /* Set other CURL features */
599 if (!curl_err) {
600 curl_err = curl_easy_setopt(context->curl, CURLOPT_FAILONERROR, 0);
603 /* Set get-response function */
604 if (!curl_err) {
605 curl_err = curl_easy_setopt(context->curl, CURLOPT_WRITEFUNCTION,
606 write_body);
608 if (!curl_err) {
609 curl_err = curl_easy_setopt(context->curl, CURLOPT_WRITEDATA, &body);
612 if (response_otp_headers != NULL) {
613 /* Set get-response-headers function */
614 if (!curl_err) {
615 curl_err = curl_easy_setopt(context->curl, CURLOPT_HEADERFUNCTION,
616 write_header);
618 if (!curl_err) {
619 curl_err = curl_easy_setopt(context->curl, CURLOPT_WRITEHEADER,
620 response_otp_headers);
624 /* Set MIME types and headers requires by SOAP 1.1.
625 * SOAP 1.1 requires text/xml, SOAP 1.2 requires application/soap+xml */
626 if (!curl_err) {
627 headers = curl_slist_append(headers,
628 "Accept: application/soap+xml,application/xml,text/xml");
629 if (!headers) {
630 err = IE_NOMEM;
631 goto leave;
633 headers = curl_slist_append(headers, "Content-Type: text/xml");
634 if (!headers) {
635 err = IE_NOMEM;
636 goto leave;
638 headers = curl_slist_append(headers, "SOAPAction: ");
639 if (!headers) {
640 err = IE_NOMEM;
641 goto leave;
643 curl_err = curl_easy_setopt(context->curl, CURLOPT_HTTPHEADER, headers);
645 if (!curl_err) {
646 /* Set user agent identification */
647 curl_err = curl_easy_setopt(context->curl, CURLOPT_USERAGENT,
648 "libisds/" PACKAGE_VERSION);
651 /* Set POST request body */
652 if (!curl_err) {
653 curl_err = curl_easy_setopt(context->curl, CURLOPT_POST, 1);
655 if (!curl_err) {
656 curl_err = curl_easy_setopt(context->curl, CURLOPT_POSTFIELDS, request);
658 if (!curl_err) {
659 curl_err = curl_easy_setopt(context->curl, CURLOPT_POSTFIELDSIZE,
660 request_length);
663 /* Check for errors so far */
664 if (curl_err) {
665 isds_log_message(context, curl_easy_strerror(curl_err));
666 err = IE_NETWORK;
667 goto leave;
670 isds_log(ILF_HTTP, ILL_DEBUG, _("Sending POST request to %s\n"), url);
671 isds_log(ILF_HTTP, ILL_DEBUG,
672 _("POST body length: %zu, content follows:\n"), request_length);
673 isds_log(ILF_HTTP, ILL_DEBUG, "%.*s\n", request_length, request);
674 isds_log(ILF_HTTP, ILL_DEBUG, _("End of POST body\n"));
675 if ((log_facilities & ILF_HTTP) && (log_level >= ILL_DEBUG) ) {
676 curl_easy_setopt(context->curl, CURLOPT_VERBOSE, 1);
677 curl_easy_setopt(context->curl, CURLOPT_DEBUGFUNCTION, log_curl);
678 } else {
679 curl_easy_setopt(context->curl, CURLOPT_VERBOSE, 0);
680 curl_easy_setopt(context->curl, CURLOPT_DEBUGFUNCTION, NULL);
684 /* Do the request */
685 curl_err = curl_easy_perform(context->curl);
687 if (!curl_err)
688 curl_err = curl_easy_getinfo(context->curl, CURLINFO_CONTENT_TYPE,
689 &content_type);
691 if (curl_err) {
692 /* TODO: CURL is not internationalized yet. Collect CURL messages for
693 * I18N. */
694 isds_printf_message(context,
695 _("%s: %s"), url, _(curl_easy_strerror(curl_err)));
696 if (curl_err == CURLE_ABORTED_BY_CALLBACK)
697 err = IE_ABORTED;
698 else
699 err = IE_NETWORK;
700 goto leave;
703 isds_log(ILF_HTTP, ILL_DEBUG, _("Final response to %s received\n"), url);
704 isds_log(ILF_HTTP, ILL_DEBUG,
705 _("Response body length: %zu, content follows:\n"),
706 body.length);
707 isds_log(ILF_HTTP, ILL_DEBUG, "%.*s\n", body.length, body.data);
708 isds_log(ILF_HTTP, ILL_DEBUG, _("End of response body\n"));
711 /* Extract MIME type and charset */
712 if (content_type) {
713 char *sep;
714 size_t offset;
716 sep = strchr(content_type, ';');
717 if (sep) offset = (size_t) (sep - content_type);
718 else offset = strlen(content_type);
720 if (mime_type) {
721 *mime_type = malloc(offset + 1);
722 if (!*mime_type) {
723 err = IE_NOMEM;
724 goto leave;
726 memcpy(*mime_type, content_type, offset);
727 (*mime_type)[offset] = '\0';
730 if (charset) {
731 if (!sep) {
732 *charset = NULL;
733 } else {
734 sep = strstr(sep, "charset=");
735 if (!sep) {
736 *charset = NULL;
737 } else {
738 *charset = strdup(sep + 8);
739 if (!*charset) {
740 err = IE_NOMEM;
741 goto leave;
748 /* Get HTTP response code */
749 if (http_code) {
750 curl_err = curl_easy_getinfo(context->curl,
751 CURLINFO_RESPONSE_CODE, http_code);
752 if (curl_err) {
753 err = IE_ERROR;
754 goto leave;
758 /* Store OTP authentication results */
759 if (response_otp_headers && response_otp_headers->is_complete) {
760 isds_log(ILF_SEC, ILL_DEBUG,
761 _("OTP authentication headers received: "
762 "method=%s, code=%s, message=%s\n"),
763 response_otp_headers->method, response_otp_headers->code,
764 response_otp_headers->message);
766 /* XXX: Don't make unknown code fatal. Missing code can be succcess if
767 * HTTP code is 302. This is checked in _isds_soap(). */
768 response_otp_headers->resolution =
769 string2isds_otp_resolution(response_otp_headers->code);
771 if (response_otp_headers->message != NULL) {
772 char *message_locale = _isds_utf82locale(response_otp_headers->message);
773 if (message_locale == NULL) {
774 err = IE_NOMEM;
775 goto leave;
777 isds_printf_message(context,
778 _("Server returned OTP authentication message: %s"),
779 message_locale);
780 free(message_locale);
783 char *next_url = NULL; /* Weak pointer managed by cURL */
784 curl_err = curl_easy_getinfo(context->curl, CURLINFO_REDIRECT_URL,
785 &next_url);
786 if (curl_err) {
787 err = IE_ERROR;
788 goto leave;
790 if (next_url != NULL) {
791 isds_log(ILF_SEC, ILL_DEBUG,
792 _("OTP authentication headers redirect to: <%s>\n"),
793 next_url);
794 free(response_otp_headers->redirect);
795 response_otp_headers->redirect = strdup(next_url);
796 if (response_otp_headers->redirect == NULL) {
797 err = IE_NOMEM;
798 goto leave;
802 leave:
803 curl_slist_free_all(headers);
805 if (err) {
806 free(body.data);
807 body.data = NULL;
808 body.length = 0;
810 if (mime_type) {
811 free(*mime_type);
812 *mime_type = NULL;
814 if (charset) {
815 free(*charset);
816 *charset = NULL;
819 if (err != IE_ABORTED) _isds_close_connection(context);
822 *response = body.data;
823 *response_length = body.length;
825 return err;
829 /* Do SOAP request.
830 * @context holds the base URL,
831 * @file is a (CGI) file of SOAP URL,
832 * @request is XML node set with SOAP request body.
833 * @file must be NULL, @request should be NULL rather than empty, if they should
834 * not be signaled in the SOAP request.
835 * @reponse is automatically allocated() node set with SOAP response body.
836 * You must xmlFreeNodeList() it. This is literal body, empty (NULL), one node
837 * or more nodes can be returned.
838 * @raw_response is automatically allocated bit stream with response body. Use
839 * NULL if you don't care
840 * @raw_response_length is size of @raw_response in bytes
841 * In case of error the response will be deallocated automatically.
842 * Side effect: message buffer */
843 _hidden isds_error _isds_soap(struct isds_ctx *context, const char *file,
844 const xmlNodePtr request, xmlNodePtr *response,
845 void **raw_response, size_t *raw_response_length) {
847 isds_error err = IE_SUCCESS;
848 char *url = NULL;
849 char *mime_type = NULL;
850 long http_code = 0;
851 struct auth_headers response_otp_headers;
852 xmlBufferPtr http_request = NULL;
853 xmlSaveCtxtPtr save_ctx = NULL;
854 xmlDocPtr request_soap_doc = NULL;
855 xmlNodePtr request_soap_envelope = NULL, request_soap_body = NULL;
856 xmlNsPtr soap_ns = NULL;
857 void *http_response = NULL;
858 size_t response_length = 0;
859 xmlDocPtr response_soap_doc = NULL;
860 xmlNodePtr response_root = NULL;
861 xmlXPathContextPtr xpath_ctx = NULL;
862 xmlXPathObjectPtr response_soap_headers = NULL, response_soap_body = NULL,
863 response_soap_fault = NULL;
866 if (!context) return IE_INVALID_CONTEXT;
867 if (!response) return IE_INVAL;
868 if (!raw_response_length && raw_response) return IE_INVAL;
870 xmlFreeNodeList(*response);
871 *response = NULL;
872 if (raw_response) *raw_response = NULL;
874 url = _isds_astrcat(context->url, file);
875 if (!url) return IE_NOMEM;
877 /* Build SOAP request envelope */
878 request_soap_doc = xmlNewDoc(BAD_CAST "1.0");
879 if (!request_soap_doc) {
880 isds_log_message(context, _("could not build soap request document"));
881 err = IE_ERROR;
882 goto leave;
884 request_soap_envelope = xmlNewNode(NULL, BAD_CAST "Envelope");
885 if (!request_soap_envelope) {
886 isds_log_message(context, _("Could not build SOAP request envelope"));
887 err = IE_ERROR;
888 goto leave;
890 xmlDocSetRootElement(request_soap_doc, request_soap_envelope);
891 /* Only this way we get namespace definition as @xmlns:soap,
892 * otherwise we get namespace prefix without definition */
893 soap_ns = xmlNewNs(request_soap_envelope, BAD_CAST SOAP_NS, NULL);
894 if(!soap_ns) {
895 isds_log_message(context, _("Could not create SOAP name space"));
896 err = IE_ERROR;
897 goto leave;
899 xmlSetNs(request_soap_envelope, soap_ns);
900 request_soap_body = xmlNewChild(request_soap_envelope, NULL,
901 BAD_CAST "Body", NULL);
902 if (!request_soap_body) {
903 isds_log_message(context,
904 _("Could not add Body to SOAP request envelope"));
905 err = IE_ERROR;
906 goto leave;
909 /* Append request XML node set to SOAP body if request is not empty */
910 /* XXX: Copy of request must be used, otherwise xmlFreeDoc(request_soap_doc)
911 * would destroy this outer structure. */
912 if (request) {
913 xmlNodePtr request_copy = xmlCopyNodeList(request);
914 if (!request_copy) {
915 isds_log_message(context,
916 _("Could not copy request content"));
917 err = IE_ERROR;
918 goto leave;
920 if (!xmlAddChildList(request_soap_body, request_copy)) {
921 xmlFreeNodeList(request_copy);
922 isds_log_message(context,
923 _("Could not add request content to SOAP "
924 "request envelope"));
925 err = IE_ERROR;
926 goto leave;
931 /* Serialize the SOAP request into HTTP request body */
932 http_request = xmlBufferCreate();
933 if (!http_request) {
934 isds_log_message(context,
935 _("Could not create xmlBuffer for HTTP request body"));
936 err = IE_ERROR;
937 goto leave;
939 /* Last argument 1 means format the XML tree. This is pretty but it breaks
940 * XML document transport as it adds text nodes (indentiation) between
941 * elements. */
942 save_ctx = xmlSaveToBuffer(http_request, "UTF-8", 0);
943 if (!save_ctx) {
944 isds_log_message(context,
945 _("Could not create XML serializer"));
946 err = IE_ERROR;
947 goto leave;
949 /* XXX: According LibXML documentation, this function does not return
950 * meaningful value yet */
951 xmlSaveDoc(save_ctx, request_soap_doc);
952 if (-1 == xmlSaveFlush(save_ctx)) {
953 isds_log_message(context,
954 _("Could not serialize SOAP request to HTTP request body"));
955 err = IE_ERROR;
956 goto leave;
959 if (context->otp != NULL)
960 memset(&response_otp_headers, 0, sizeof(response_otp_headers));
961 redirect:
962 if (context->otp != NULL) auth_headers_free(&response_otp_headers);
963 isds_log(ILF_SOAP, ILL_DEBUG,
964 _("SOAP request to sent to %s:\n%.*s\nEnd of SOAP request\n"),
965 url, http_request->use, http_request->content);
967 err = http(context, url, http_request->content, http_request->use,
968 &http_response, &response_length,
969 &mime_type, NULL, &http_code,
970 (context->otp == NULL) ? NULL: &response_otp_headers);
972 /* TODO: HTTP binding for SOAP prescribes non-200 HTTP return codes
973 * to be processed too. */
975 if (err) {
976 goto leave;
979 /* Check for HTTP return code */
980 isds_log(ILF_SOAP, ILL_DEBUG, _("Server returned %ld HTTP code\n"),
981 http_code);
982 switch (http_code) {
983 /* XXX: We must see which code is used for not permitted ISDS
984 * operation like downloading message without proper user
985 * permissions. In that case we should keep connection opened. */
986 case 302:
987 if (context->otp) {
988 if (response_otp_headers.resolution == OTP_RESOLUTION_UNKNOWN)
989 context->otp->resolution = OTP_RESOLUTION_SUCCESS;
990 else
991 context->otp->resolution = response_otp_headers.resolution;
992 err = IE_PARTIAL_SUCCESS;
993 isds_printf_message(context,
994 _("Server redirects on <%s> because OTP authentication "
995 "succeeded."),
996 url);
997 if (context->otp->otp_code != NULL &&
998 response_otp_headers.redirect != NULL) {
999 /* XXX: If OTP code is known, this must be second OTP phase, so
1000 * sent final POST request. */
1001 free(url);
1002 url = response_otp_headers.redirect;
1003 response_otp_headers.redirect = NULL;
1004 goto redirect;
1005 } else {
1006 /* XXX: Otherwise bail out to ask application for OTP code. */
1007 goto leave;
1009 } else {
1010 err = IE_HTTP;
1011 isds_printf_message(context,
1012 _("Code 302: Server redirects on <%s> request. "
1013 "Redirection is forbidden in stateless mode."),
1014 url);
1015 goto leave;
1017 break;
1018 case 401: /* ISDS server returns 401 even if Authorization
1019 presents. */
1020 case 403: /* HTTP/1.0 prescribes 403 if Authorization presents. */
1021 if (context->otp)
1022 context->otp->resolution = response_otp_headers.resolution;
1023 err = IE_NOT_LOGGED_IN;
1024 isds_log_message(context, _("Authentication failed"));
1025 goto leave;
1026 break;
1027 case 404:
1028 err = IE_HTTP;
1029 isds_printf_message(context,
1030 _("Code 404: Document (%s) not found on server"), url);
1031 goto leave;
1032 break;
1033 /* 500 should return standard SOAP message */
1036 /* Check for Content-Type: text/xml.
1037 * Do it after HTTP code check because 401 Unauthorized returns HTML web
1038 * page for browsers. */
1039 if (mime_type && strcmp(mime_type, "text/xml")
1040 && strcmp(mime_type, "application/soap+xml")
1041 && strcmp(mime_type, "application/xml")) {
1042 char *mime_type_locale = _isds_utf82locale(mime_type);
1043 isds_printf_message(context,
1044 _("%s: bad MIME type sent by server: %s"), url,
1045 mime_type_locale);
1046 free(mime_type_locale);
1047 err = IE_SOAP;
1048 goto leave;
1051 /* TODO: Convert returned body into XML default encoding */
1053 /* Parse the HTTP body as XML */
1054 response_soap_doc = xmlParseMemory(http_response, response_length);
1055 if (!response_soap_doc) {
1056 err = IE_XML;
1057 goto leave;
1060 xpath_ctx = xmlXPathNewContext(response_soap_doc);
1061 if (!xpath_ctx) {
1062 err = IE_ERROR;
1063 goto leave;
1066 if (_isds_register_namespaces(xpath_ctx, MESSAGE_NS_UNSIGNED)) {
1067 err = IE_ERROR;
1068 goto leave;
1071 isds_log(ILF_SOAP, ILL_DEBUG,
1072 _("SOAP response received:\n%.*s\nEnd of SOAP response\n"),
1073 response_length, http_response);
1076 /* Check for SOAP version */
1077 response_root = xmlDocGetRootElement(response_soap_doc);
1078 if (!response_root) {
1079 isds_log_message(context, "SOAP response has no root element");
1080 err = IE_SOAP;
1081 goto leave;
1083 if (xmlStrcmp(response_root->name, BAD_CAST "Envelope") ||
1084 xmlStrcmp(response_root->ns->href, BAD_CAST SOAP_NS)) {
1085 isds_log_message(context, "SOAP response is not SOAP 1.1 document");
1086 err = IE_SOAP;
1087 goto leave;
1090 /* Check for SOAP Headers */
1091 response_soap_headers = xmlXPathEvalExpression(
1092 BAD_CAST "/soap:Envelope/soap:Header/"
1093 "*[@soap:mustUnderstand/text() = true()]", xpath_ctx);
1094 if (!response_soap_headers) {
1095 err = IE_ERROR;
1096 goto leave;
1098 if (!xmlXPathNodeSetIsEmpty(response_soap_headers->nodesetval)) {
1099 isds_log_message(context,
1100 _("SOAP response requires unsupported feature"));
1101 /* TODO: log the headers
1102 * xmlChar *fragment = NULL;
1103 * fragment = xmlXPathCastNodeSetToSting(response_soap_headers->nodesetval);*/
1104 err = IE_NOTSUP;
1105 goto leave;
1108 /* Get SOAP Body */
1109 response_soap_body = xmlXPathEvalExpression(
1110 BAD_CAST "/soap:Envelope/soap:Body", xpath_ctx);
1111 if (!response_soap_body) {
1112 err = IE_ERROR;
1113 goto leave;
1115 if (xmlXPathNodeSetIsEmpty(response_soap_body->nodesetval)) {
1116 isds_log_message(context,
1117 _("SOAP response does not contain SOAP Body element"));
1118 err = IE_SOAP;
1119 goto leave;
1121 if (response_soap_body->nodesetval->nodeNr > 1) {
1122 isds_log_message(context,
1123 _("SOAP response has more than one Body element"));
1124 err = IE_SOAP;
1125 goto leave;
1128 /* Check for SOAP Fault */
1129 response_soap_fault = xmlXPathEvalExpression(
1130 BAD_CAST "/soap:Envelope/soap:Body/soap:Fault", xpath_ctx);
1131 if (!response_soap_fault) {
1132 err = IE_ERROR;
1133 goto leave;
1135 if (!xmlXPathNodeSetIsEmpty(response_soap_fault->nodesetval)) {
1136 /* Server signals Fault. Gather error message and croak. */
1137 /* XXX: Only first message is passed */
1138 char *message = NULL, *message_locale = NULL;
1139 xpath_ctx->node = response_soap_fault->nodesetval->nodeTab[0];
1140 xmlXPathFreeObject(response_soap_fault);
1141 /* XXX: faultstring and faultcode are in no name space according
1142 * ISDS specification */
1143 /* First more verbose faultstring */
1144 response_soap_fault = xmlXPathEvalExpression(
1145 BAD_CAST "faultstring[1]/text()", xpath_ctx);
1146 if (response_soap_fault &&
1147 !xmlXPathNodeSetIsEmpty(response_soap_fault->nodesetval)) {
1148 message = (char *)
1149 xmlXPathCastNodeSetToString(response_soap_fault->nodesetval);
1150 message_locale = _isds_utf82locale(message);
1152 /* If not available, try shorter faultcode */
1153 if (!message_locale) {
1154 free(message);
1155 xmlXPathFreeObject(response_soap_fault);
1156 response_soap_fault = xmlXPathEvalExpression(
1157 BAD_CAST "faultcode[1]/text()", xpath_ctx);
1158 if (response_soap_fault &&
1159 !xmlXPathNodeSetIsEmpty(response_soap_fault->nodesetval)) {
1160 message = (char *)
1161 xmlXPathCastNodeSetToString(
1162 response_soap_fault->nodesetval);
1163 message_locale = _isds_utf82locale(message);
1167 /* Croak */
1168 if (message_locale)
1169 isds_printf_message(context, _("SOAP response signals Fault: %s"),
1170 message_locale);
1171 else
1172 isds_log_message(context, _("SOAP response signals Fault"));
1174 free(message_locale);
1175 free(message);
1177 err = IE_SOAP;
1178 goto leave;
1182 /* Extract XML Tree with ISDS response from SOAP envelope and return it.
1183 * XXX: response_soap_body is Body, we need children which may not exist
1184 * (i.e. empty Body). */
1185 /* TODO: Destroy SOAP response but Body children. This is more memory
1186 * friendly than copying (potentially) fat body */
1187 if (response_soap_body->nodesetval->nodeTab[0]->children) {
1188 *response = xmlDocCopyNodeList(response_soap_doc,
1189 response_soap_body->nodesetval->nodeTab[0]->children);
1190 if (!*response) {
1191 err = IE_NOMEM;
1192 goto leave;
1194 } else *response = NULL;
1196 /* Save raw response */
1197 if (raw_response) {
1198 *raw_response = http_response;
1199 *raw_response_length = response_length;
1200 http_response = NULL;
1204 leave:
1205 if (err) {
1206 xmlFreeNodeList(*response);
1207 *response = NULL;
1210 xmlXPathFreeObject(response_soap_fault);
1211 xmlXPathFreeObject(response_soap_body);
1212 xmlXPathFreeObject(response_soap_headers);
1213 xmlXPathFreeContext(xpath_ctx);
1214 xmlFreeDoc(response_soap_doc);
1215 auth_headers_free(&response_otp_headers);
1216 free(mime_type);
1217 free(http_response);
1218 xmlSaveClose(save_ctx);
1219 xmlBufferFree(http_request);
1220 xmlFreeDoc(request_soap_doc); /* recursive, frees request_body, soap_ns*/
1221 free(url);
1223 return err;
1227 /* LibXML functions:
1229 * void xmlInitParser(void)
1230 * Initialization function for the XML parser. This is not reentrant. Call
1231 * once before processing in case of use in multithreaded programs.
1233 * int xmlInitParserCtxt(xmlParserCtxtPtr ctxt)
1234 * Initialize a parser context
1236 * xmlDocPtr xmlCtxtReadDoc(xmlParserCtxtPtr ctxt, const xmlChar * cur,
1237 * const * char URL, const char * encoding, int options);
1238 * Parse in-memory NULL-terminated document @cur.
1240 * xmlDocPtr xmlParseMemory(const char * buffer, int size)
1241 * Parse an XML in-memory block and build a tree.
1243 * xmlParserCtxtPtr xmlCreateMemoryParserCtxt(const char * buffer, int
1244 * size);
1245 * Create a parser context for an XML in-memory document.
1247 * xmlParserCtxtPtr xmlCreateDocParserCtxt(const xmlChar * cur)
1248 * Creates a parser context for an XML in-memory document.
1250 * xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt,
1251 * const char * buffer, int size, const char * URL, const char * encoding,
1252 * int options)
1253 * Parse an XML in-memory document and build a tree. This reuses the existing
1254 * @ctxt parser context.
1256 * void xmlCleanupParser(void)
1257 * Cleanup function for the XML library. It tries to reclaim all parsing
1258 * related glob document related memory. Calling this function should not
1259 * prevent reusing the libr finished using the library or XML document built
1260 * with it.
1262 * void xmlClearParserCtxt(xmlParserCtxtPtr ctxt)
1263 * Clear (release owned resources) and reinitialize a parser context.
1265 * void xmlCtxtReset(xmlParserCtxtPtr ctxt)
1266 * Reset a parser context
1268 * void xmlFreeParserCtxt(xmlParserCtxtPtr ctxt)
1269 * Free all the memory used by a parser context. However the parsed document
1270 * in ctxt->myDoc is not freed.
1272 * void xmlFreeDoc(xmlDocPtr cur)
1273 * Free up all the structures used by a document, tree included.