Thread HTTP 404 as erorr on HTTP level
[libisds.git] / src / soap.c
blob20bf38babd1b05077aa3bf03ef5b8f54119f34ed
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;
15 /* Close connection to server and destroy CURL handle associated
16 * with @context */
17 _hidden isds_error close_connection(struct isds_ctx *context) {
18 if (!context) return IE_INVALID_CONTEXT;
20 if (context->curl) {
21 curl_easy_cleanup(context->curl);
22 context->curl = NULL;
23 isds_log(ILF_HTTP, ILL_DEBUG, _("Connection to server closed"));
24 return IE_SUCCESS;
25 } else {
26 return IE_CONNECTION_CLOSED;
31 /* CURL call back function called when chunk of HTTP reponse body is available.
32 * @buffer points to new data
33 * @size * @nmemb is length of the chunk in bytes. Zero means empty body.
34 * @userp is private structure.
35 * Must reuturn the length of the chunk, otherwise CURL will signal
36 * CURL_WRITE_ERROR. */
37 static size_t write_body(void *buffer, size_t size, size_t nmemb, void *userp) {
38 struct soap_body *body = (struct soap_body *) userp;
39 void *new_data;
41 /* FIXME: Check for (size * nmemb + body->lengt) !> SIZE_T_MAX.
42 * Precompute the product then. */
44 if (!body) return 0; /* This should never happen */
45 if (0 == (size * nmemb)) return 0; /* Empty body */
47 new_data = realloc(body->data, body->length + size * nmemb);
48 if (!new_data) return 0;
50 memcpy(new_data + body->length, buffer, size * nmemb);
52 body->data = new_data;
53 body->length += size * nmemb;
55 return (size * nmemb);
59 /* Do HTTP request.
60 * @context holds the base URL,
61 * @url is a (CGI) file of SOAP URL,
62 * @request is body for POST request
63 * @request_length is length of @request in bytes
64 * @reponse is automatically reallocated() buffer to fit HTTP response with
65 * @response_length (does not need to match allocatef memory exactly). You must
66 * free() the @response.
67 * @mime_type is automatically allocated MIME type send by server (*NULL if not
68 * sent). Set NULL if you don't care.
69 * @charset is charset of the body signaled by server. The same constrains
70 * like on @mime_type apply.
71 * @http_code is final HTTP code returned by server. This can be 200, 401, 500
72 * or any other one. Pass NULL if you don't interrest.
73 * In case of error, the response memory, MIME type, charset and lenght will be
74 * deallocated and zerod automatically. Thus be sure they are preallocated or
75 * they points to NULL.
76 * Be ware that successful return value does not mean the HTTP request has
77 * been accepted by the server. You must cosult @http_code. OTOH, failure
78 * return value means the request could not been sent (e.g. SSL error).
79 * Side effect: message buffer */
80 static isds_error http(struct isds_ctx *context, const char *url,
81 const void *request, const size_t request_length,
82 void **response, size_t *response_length,
83 char **mime_type, char **charset, long *http_code) {
85 CURLcode curl_err;
86 isds_error err = IE_SUCCESS;
87 struct soap_body body;
88 char *content_type;
89 struct curl_slist *headers = NULL;
92 if (!context) return IE_INVALID_CONTEXT;
93 if (!url) return IE_INVAL;
94 if (request_length > 0 && !request) return IE_INVAL;
95 if (!response || !response_length) return IE_INVAL;
97 /* Set the body here to allow deallocataion in leave block */
98 body.data = *response;
99 body.length = 0;
101 /* Set Request-URI */
102 curl_err = curl_easy_setopt(context->curl, CURLOPT_URL, url);
104 /* Set TLS options */
105 if (!curl_err && context->tls_verify_server) {
106 if (!*context->tls_verify_server)
107 isds_log(ILF_SEC, ILL_WARNING,
108 _("Disabling server identity verification. "
109 "That was your decision.\n"));
110 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSL_VERIFYPEER,
111 (*context->tls_verify_server)? 1L : 0L);
112 if (!curl_err) {
113 curl_err = curl_easy_setopt(context->curl, CURLOPT_SSL_VERIFYHOST,
114 (*context->tls_verify_server)? 2L : 0L);
117 if (!curl_err && context->tls_ca_file) {
118 isds_log(ILF_SEC, ILL_INFO,
119 _("CA certificates will be searched in `%s' file since now\n"),
120 context->tls_ca_file);
121 curl_err = curl_easy_setopt(context->curl, CURLOPT_CAINFO,
122 context->tls_ca_file);
124 if (!curl_err && context->tls_ca_dir) {
125 isds_log(ILF_SEC, ILL_INFO,
126 _("CA certificates will be searched in `%s' directory "
127 "since now\n"), context->tls_ca_file);
128 curl_err = curl_easy_setopt(context->curl, CURLOPT_CAINFO,
129 context->tls_ca_file);
133 /* Set credentials */
134 if (!curl_err && context->username) {
135 curl_err = curl_easy_setopt(context->curl, CURLOPT_USERNAME,
136 context->username);
138 if (!curl_err && context->password) {
139 curl_err = curl_easy_setopt(context->curl, CURLOPT_PASSWORD,
140 context->password);
143 /* Set timeout */
144 if (!curl_err) {
145 curl_err = curl_easy_setopt(context->curl, CURLOPT_NOSIGNAL, 1);
147 if (!curl_err && context->timeout) {
148 curl_err = curl_easy_setopt(context->curl, CURLOPT_TIMEOUT_MS,
149 context->timeout);
152 /* Set other CURL features */
153 if (!curl_err) {
154 curl_err = curl_easy_setopt(context->curl, CURLOPT_FAILONERROR, 0);
156 if (!curl_err) {
157 curl_err = curl_easy_setopt(context->curl, CURLOPT_FOLLOWLOCATION, 1);
159 if (!curl_err) {
160 /* TODO: Make the redirect depth configurable */
161 curl_err = curl_easy_setopt(context->curl, CURLOPT_MAXREDIRS, 8);
163 if (!curl_err) {
164 curl_err = curl_easy_setopt(context->curl,
165 CURLOPT_UNRESTRICTED_AUTH, 1);
167 if (!curl_err) {
168 curl_err = curl_easy_setopt(context->curl, CURLOPT_COOKIEFILE, "");
171 /* Set get-response function */
172 if (!curl_err) {
173 curl_err = curl_easy_setopt(context->curl, CURLOPT_WRITEFUNCTION,
174 write_body);
176 if (!curl_err) {
177 curl_err = curl_easy_setopt(context->curl, CURLOPT_WRITEDATA, &body);
180 /* Set MIME types and headers requires by SOAP 1.1.
181 * SOAP 1.1 requires text/xml, SOAP 1.2 requires application/soap+xml */
182 if (!curl_err) {
183 headers = curl_slist_append(headers,
184 "Accept: application/soap+xml,application/xml,text/xml");
185 if (!headers) {
186 err = IE_NOMEM;
187 goto leave;
189 headers = curl_slist_append(headers, "Content-Type: text/xml");
190 if (!headers) {
191 err = IE_NOMEM;
192 goto leave;
194 headers = curl_slist_append(headers, "SOAPAction: ");
195 if (!headers) {
196 err = IE_NOMEM;
197 goto leave;
199 curl_err = curl_easy_setopt(context->curl, CURLOPT_HTTPHEADER, headers);
201 if (!curl_err) {
202 /* Set user agent identification */
203 /* TODO: Present library version, curl etc. in User-Agent */
204 curl_err = curl_easy_setopt(context->curl, CURLOPT_USERAGENT, "libisds");
207 /* Set POST request body */
208 if (!curl_err) {
209 curl_err = curl_easy_setopt(context->curl, CURLOPT_POST, 1);
211 if (!curl_err) {
212 curl_err = curl_easy_setopt(context->curl, CURLOPT_POSTFIELDS, request);
214 if (!curl_err) {
215 curl_err = curl_easy_setopt(context->curl, CURLOPT_POSTFIELDSIZE,
216 request_length);
219 /* Check for errors so far */
220 if (curl_err) {
221 isds_log_message(context, curl_easy_strerror(curl_err));
222 err = IE_NETWORK;
223 goto leave;
226 isds_log(ILF_HTTP, ILL_DEBUG, _("Sending POST request to %s\n"), url);
227 isds_log(ILF_HTTP, ILL_DEBUG,
228 _("POST body length: %zu, content follows:\n"), request_length);
229 isds_log(ILF_HTTP, ILL_DEBUG, "%.*s\n", request_length, request);
230 isds_log(ILF_HTTP, ILL_DEBUG, _("End of POST body\n"));
231 if ((log_facilities & ILF_HTTP) && (log_level >= ILL_DEBUG) ) {
232 curl_easy_setopt(context->curl, CURLOPT_VERBOSE, 1);
236 /* Do the request */
237 curl_err = curl_easy_perform(context->curl);
239 /* Wipe credentials out of the handler */
240 if (context->username) {
241 curl_easy_setopt(context->curl, CURLOPT_USERNAME, NULL);
243 if (context->password) {
244 curl_easy_setopt(context->curl, CURLOPT_PASSWORD, NULL);
247 if (!curl_err)
248 curl_err = curl_easy_getinfo(context->curl, CURLINFO_CONTENT_TYPE,
249 &content_type);
251 isds_log(ILF_HTTP, ILL_DEBUG, _("Received final response to %s\n"), url);
252 isds_log(ILF_HTTP, ILL_DEBUG,
253 _("Response body length: %zu, content follows:\n"),
254 body.length);
255 isds_log(ILF_HTTP, ILL_DEBUG, "%.*s\n", body.length, body.data);
256 isds_log(ILF_HTTP, ILL_DEBUG, _("End of response body\n"));
258 if (curl_err) {
259 /* TODO: CURL is not internationalized yet. Collect CURL messages for
260 * I18N. */
261 isds_printf_message(context,
262 _("%s: %s"), url, _(curl_easy_strerror(curl_err)));
263 err = IE_NETWORK;
264 goto leave;
267 /* Extract MIME type and charset */
268 if (content_type) {
269 char *sep;
270 size_t offset;
272 sep = strchr(content_type, ';');
273 if (sep) offset = (size_t) (sep - content_type);
274 else offset = strlen(content_type);
276 if (mime_type) {
277 *mime_type = malloc(offset + 1);
278 if (!*mime_type) {
279 err = IE_NOMEM;
280 goto leave;
282 memcpy(*mime_type, content_type, offset);
283 (*mime_type)[offset] = '\0';
286 if (charset) {
287 if (!sep) {
288 *charset = NULL;
289 } else {
290 sep = strstr(sep, "charset=");
291 if (!sep) {
292 *charset = NULL;
293 } else {
294 *charset = strdup(sep + 8);
295 if (!*charset) {
296 err = IE_NOMEM;
297 goto leave;
304 /* Get HTTP response code */
305 if (http_code) {
306 curl_err = curl_easy_getinfo(context->curl,
307 CURLINFO_RESPONSE_CODE, http_code);
308 if (curl_err) {
309 err = IE_ERROR;
310 goto leave;
314 leave:
315 curl_slist_free_all(headers);
317 if (err) {
318 free(body.data);
319 body.data = NULL;
320 body.length = 0;
322 if (mime_type) {
323 free(*mime_type);
324 *mime_type = NULL;
326 if (charset) {
327 free(*charset);
328 *charset = NULL;
331 close_connection(context);
334 *response = body.data;
335 *response_length = body.length;
337 return err;
341 /* Do SOAP request.
342 * @context holds the base URL,
343 * @file is a (CGI) file of SOAP URL,
344 * @request is XML node set with SOAP request body.
345 * @file must be NULL, @request should be NULL rather than empty, if they should
346 * not be signaled in the SOAP request.
347 * @reponse is automatically allocated() node set with SOAP response body.
348 * You must xmlFreeNodeList() it. This is literal body, empty (NULL), one node
349 * or more nodes can be returned.
350 * In case of error the response will be deallocated automatically.
351 * Side effect: message buffer */
352 _hidden isds_error soap(struct isds_ctx *context, const char *file,
353 const xmlNodePtr request, xmlNodePtr *response) {
355 isds_error err = IE_SUCCESS;
356 char *url = NULL;
357 char *mime_type = NULL;
358 long http_code = 0;
359 xmlBufferPtr http_request = NULL;
360 xmlSaveCtxtPtr save_ctx = NULL;
361 xmlDocPtr request_soap_doc = NULL;
362 xmlNodePtr request_soap_envelope = NULL, request_soap_body = NULL;
363 xmlNsPtr soap_ns = NULL;
364 void *http_response = NULL;
365 size_t response_length = 0;
366 xmlDocPtr response_soap_doc = NULL;
367 xmlNodePtr response_root = NULL;
368 xmlXPathContextPtr xpath_ctx = NULL;
369 xmlXPathObjectPtr response_soap_headers = NULL, response_soap_body = NULL,
370 response_soap_fault = NULL;
373 if (!context) return IE_INVALID_CONTEXT;
374 if (!response) return IE_INVAL;
376 xmlFreeNodeList(*response);
377 *response = NULL;
379 url = astrcat(context->url, file);
380 if (!url) return IE_NOMEM;
382 /* Build SOAP request envelope */
383 request_soap_doc = xmlNewDoc(BAD_CAST "1.0");
384 if (!request_soap_doc) {
385 isds_log_message(context, _("Could not build SOAP request document"));
386 err = IE_ERROR;
387 goto leave;
389 request_soap_envelope = xmlNewNode(NULL, BAD_CAST "Envelope");
390 if (!request_soap_envelope) {
391 isds_log_message(context, _("Could not build SOAP request envelope"));
392 err = IE_ERROR;
393 goto leave;
395 xmlDocSetRootElement(request_soap_doc, request_soap_envelope);
396 /* Only this way we get namespace definition as @xmlns:soap,
397 * otherwise we get namespace prefix without definition */
398 soap_ns = xmlNewNs(request_soap_envelope, BAD_CAST SOAP_NS, NULL);
399 if(!soap_ns) {
400 isds_log_message(context, _("Could not create SOAP name space"));
401 err = IE_ERROR;
402 goto leave;
404 xmlSetNs(request_soap_envelope, soap_ns);
405 request_soap_body = xmlNewChild(request_soap_envelope, NULL,
406 BAD_CAST "Body", NULL);
407 if (!request_soap_body) {
408 isds_log_message(context, _("Could not add Body to SOAP request envelope"));
409 err = IE_ERROR;
410 goto leave;
413 /* Append request XML node set to SOAP body if request is not empty */
414 /* XXX: Copy of request must be used, otherwise xmlFreeDoc(request_soap_doc)
415 * would destroy this outer structure. */
416 if (request) {
417 xmlNodePtr request_copy = xmlCopyNodeList(request);
418 if (!request_copy) {
419 isds_log_message(context,
420 _("Could not copy request content"));
421 err = IE_ERROR;
422 goto leave;
424 if (!xmlAddChildList(request_soap_body, request_copy)) {
425 xmlFreeNodeList(request_copy);
426 isds_log_message(context,
427 _("Could not add request content to SOAP request envelope"));
428 err = IE_ERROR;
429 goto leave;
434 /* Serialize the SOAP request into HTTP request body */
435 http_request = xmlBufferCreate();
436 if (!http_request) {
437 isds_log_message(context,
438 _("Could not create xmlBuffer for HTTP request body"));
439 err = IE_ERROR;
440 goto leave;
442 /* Last argument 1 means format the XML tree. This is pretty but it breaks
443 * digital signatures probably because ISDS abadoned XMLDSig */
444 save_ctx = xmlSaveToBuffer(http_request, "UTF-8", 1);
445 if (!save_ctx) {
446 isds_log_message(context,
447 _("Could not create XML serializer"));
448 err = IE_ERROR;
449 goto leave;
451 /* XXX: According LibXML documentation, this function does not return
452 * meaningfull value yet */
453 xmlSaveDoc(save_ctx, request_soap_doc);
454 if (-1 == xmlSaveFlush(save_ctx)) {
455 isds_log_message(context,
456 _("Could not serialize SOAP request to HTTP request bddy"));
457 err = IE_ERROR;
458 goto leave;
461 isds_log(ILF_SOAP, ILL_DEBUG,
462 _("SOAP request to sent to %s:\n%.*s\nEnd of SOAP request\n"),
463 url, http_request->use, http_request->content);
465 err = http(context, url, http_request->content, http_request->use,
466 &http_response, &response_length,
467 &mime_type, NULL, &http_code);
469 /* TODO: HTTP binding for SOAP prescribes non-200 HTTP return codes
470 * to be processes too. */
472 if (err) {
473 goto leave;
476 /* Check for HTTP return code */
477 isds_log(ILF_SOAP, ILL_DEBUG, _("Server returned %ld HTTP code\n"),
478 http_code);
479 switch (http_code) {
480 /* XXX: We must see which code is used for not permitted ISDS
481 * operation like downloading message without proper user
482 * permissions. In that cat we should keep connection opened. */
483 case 401:
484 err = IE_NOT_LOGGED_IN;
485 isds_log_message(context, _("Authentication failed"));
486 goto leave;
487 break;
488 case 404:
489 err = IE_HTTP;
490 isds_log_message(context,
491 _("Code 404: Document not found on server"));
492 goto leave;
493 break;
494 /* 500 should return standard SOAP message */
497 /* Check for Content-Type: text/xml.
498 * Do it after HTTP code check because 401 Unauthorized returns HTML web
499 * page for browsers. */
500 if (mime_type && strcmp(mime_type, "text/xml")
501 && strcmp(mime_type, "application/soap+xml")
502 && strcmp(mime_type, "application/xml")) {
503 char *mime_type_locale = utf82locale(mime_type);
504 isds_printf_message(context,
505 _("%s: bad MIME type sent by server: %s"), url,
506 mime_type_locale);
507 free(mime_type_locale);
508 err = IE_SOAP;
509 goto leave;
512 /* TODO: Convert returned body into XML default encoding */
514 /* Parse the HTTP body as XML */
515 response_soap_doc = xmlParseMemory(http_response, response_length);
516 if (!response_soap_doc) {
517 err = IE_XML;
518 goto leave;
521 xpath_ctx = xmlXPathNewContext(response_soap_doc);
522 if (!xpath_ctx) {
523 err = IE_ERROR;
524 goto leave;
527 if (register_namespaces(xpath_ctx)) {
528 err = IE_ERROR;
529 goto leave;
532 isds_log(ILF_SOAP, ILL_DEBUG,
533 _("SOAP response received:\n%.*s\nEnd of SOAP response\n"),
534 response_length, http_response);
537 /* Check for SOAP version */
538 response_root = xmlDocGetRootElement(response_soap_doc);
539 if (!response_root) {
540 isds_log_message(context, "SOAP response has no root element");
541 err = IE_SOAP;
542 goto leave;
544 if (xmlStrcmp(response_root->name, BAD_CAST "Envelope") ||
545 xmlStrcmp(response_root->ns->href, BAD_CAST SOAP_NS)) {
546 isds_log_message(context, "SOAP response is not SOAP 1.1 document");
547 err = IE_SOAP;
548 goto leave;
551 /* Check for SOAP Headers */
552 response_soap_headers = xmlXPathEvalExpression(
553 BAD_CAST "/soap:Envelope/soap:Header/"
554 "*[@soap:mustUnderstand/text() = true()]", xpath_ctx);
555 if (!response_soap_headers) {
556 err = IE_ERROR;
557 goto leave;
559 if (!xmlXPathNodeSetIsEmpty(response_soap_headers->nodesetval)) {
560 isds_log_message(context,
561 _("SOAP response requires unsupported feature"));
562 /* TODO: log the headers
563 * xmlChar *fragment = NULL;
564 * fragment = xmlXPathCastNodeSetToSting(response_soap_headers->nodesetval);*/
565 err = IE_NOTSUP;
566 goto leave;
569 /* Get SOAP Body */
570 response_soap_body = xmlXPathEvalExpression(
571 BAD_CAST "/soap:Envelope/soap:Body", xpath_ctx);
572 if (!response_soap_body) {
573 err = IE_ERROR;
574 goto leave;
576 if (xmlXPathNodeSetIsEmpty(response_soap_body->nodesetval)) {
577 isds_log_message(context,
578 _("SOAP response does not contain SOAP Body element"));
579 err = IE_SOAP;
580 goto leave;
582 if (response_soap_body->nodesetval->nodeNr > 1) {
583 isds_log_message(context,
584 _("SOAP response has more than one Body element"));
585 err = IE_SOAP;
586 goto leave;
589 /* Check for SOAP Fault */
590 response_soap_fault = xmlXPathEvalExpression(
591 BAD_CAST "/soap:Envelope/soap:Body/soap:Fault", xpath_ctx);
592 if (!response_soap_fault) {
593 err = IE_ERROR;
594 goto leave;
596 if (!xmlXPathNodeSetIsEmpty(response_soap_fault->nodesetval)) {
597 /* TODO: log the fultcode and faultstring */
598 isds_log_message(context, _("SOAP response signals Fault"));
599 err = IE_SOAP;
600 goto leave;
604 /* Extract XML Tree with ISDS response from SOAP envelope and return it.
605 * XXX: response_soap_body is Body, we need children which may not exist
606 * (i.e. empty Body). */
607 /* TODO: Destroy SOAP response but Body childern. This is more memory
608 * friendly than copying (potentialy) fat body */
609 if (response_soap_body->nodesetval->nodeTab[0]->children) {
610 *response = xmlDocCopyNodeList(response_soap_doc,
611 response_soap_body->nodesetval->nodeTab[0]->children);
612 if (!*response) {
613 err = IE_NOMEM;
614 goto leave;
616 } else *response = NULL;
620 leave:
621 if (err) {
622 xmlFreeNodeList(*response);
623 *response = NULL;
626 xmlXPathFreeObject(response_soap_fault);
627 xmlXPathFreeObject(response_soap_body);
628 xmlXPathFreeObject(response_soap_headers);
629 xmlXPathFreeContext(xpath_ctx);
630 xmlFreeDoc(response_soap_doc);
631 free(mime_type);
632 free(http_response);
633 xmlSaveClose(save_ctx);
634 xmlBufferFree(http_request);
635 xmlFreeDoc(request_soap_doc); /* recursive, frees request_body, soap_ns*/
636 free(url);
638 return err;
642 /* LibXML functions:
644 * void xmlInitParser(void)
645 * Initialization function for the XML parser. This is not reentrant. Call
646 * once before processing in case of use in multithreaded programs.
648 * int xmlInitParserCtxt(xmlParserCtxtPtr ctxt)
649 * Initialize a parser context
651 * xmlDocPtr xmlCtxtReadDoc(xmlParserCtxtPtr ctxt, const xmlChar * cur,
652 * const * char URL, const char * encoding, int options);
653 * Parse in-memory NULL-terminated document @cur.
655 * xmlDocPtr xmlParseMemory(const char * buffer, int size)
656 * Parse an XML in-memory block and build a tree.
658 * xmlParserCtxtPtr xmlCreateMemoryParserCtxt(const char * buffer, int
659 * size);
660 * Create a parser context for an XML in-memory document.
662 * xmlParserCtxtPtr xmlCreateDocParserCtxt(const xmlChar * cur)
663 * Creates a parser context for an XML in-memory document.
665 * xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt,
666 * const char * buffer, int size, const char * URL, const char * encoding,
667 * int options)
668 * Parse an XML in-memory document and build a tree. This reuses the existing
669 * @ctxt parser context.
671 * void xmlCleanupParser(void)
672 * Cleanup function for the XML library. It tries to reclaim all parsing
673 * related glob document related memory. Calling this function should not
674 * prevent reusing the libr finished using the library or XML document built
675 * with it.
677 * void xmlClearParserCtxt(xmlParserCtxtPtr ctxt)
678 * Clear (release owned resources) and reinitialize a parser context.
680 * void xmlCtxtReset(xmlParserCtxtPtr ctxt)
681 * Reset a parser context
683 * void xmlFreeParserCtxt(xmlParserCtxtPtr ctxt)
684 * Free all the memory used by a parser context. However the parsed document
685 * in ctxt->myDoc is not freed.
687 * void xmlFreeDoc(xmlDocPtr cur)
688 * Free up all the structures used by a document, tree included.