| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <jsapi.h> |
| #include <js/Initialization.h> |
| #include "config.h" |
| #include "util.h" |
| |
| // Soft dependency on cURL bindings because they're |
| // only used when running the JS tests from the |
| // command line which is rare. |
| #ifndef HAVE_CURL |
| |
| void |
| http_check_enabled() |
| { |
| fprintf(stderr, "HTTP API was disabled at compile time.\n"); |
| exit(3); |
| } |
| |
| |
| bool |
| http_ctor(JSContext* cx, JSObject* req) |
| { |
| return false; |
| } |
| |
| |
| void |
| http_dtor(JSFreeOp* fop, JSObject* req) |
| { |
| return; |
| } |
| |
| |
| bool |
| http_open(JSContext* cx, JSObject* req, JS::Value mth, JS::Value url, JS::Value snc) |
| { |
| return false; |
| } |
| |
| |
| bool |
| http_set_hdr(JSContext* cx, JSObject* req, JS::Value name, JS::Value val) |
| { |
| return false; |
| } |
| |
| |
| bool |
| http_send(JSContext* cx, JSObject* req, JS::Value body) |
| { |
| return false; |
| } |
| |
| |
| int |
| http_status(JSContext* cx, JSObject* req) |
| { |
| return -1; |
| } |
| |
| bool |
| http_uri(JSContext* cx, JSObject* req, couch_args* args, JS::Value* uri_val) |
| { |
| return false; |
| } |
| |
| |
| #else |
| #include <curl/curl.h> |
| #ifndef XP_WIN |
| #include <unistd.h> |
| #endif |
| |
| |
| void |
| http_check_enabled() |
| { |
| return; |
| } |
| |
| |
| // Map some of the string function names to things which exist on Windows |
| #ifdef XP_WIN |
| #define strcasecmp _strcmpi |
| #define strncasecmp _strnicmp |
| #endif |
| |
| |
| typedef struct curl_slist CurlHeaders; |
| |
| |
| typedef struct { |
| int method; |
| std::string url; |
| CurlHeaders* req_headers; |
| int16_t last_status; |
| } HTTPData; |
| |
| |
| const char* METHODS[] = {"GET", "HEAD", "POST", "PUT", "DELETE", "COPY", "OPTIONS", NULL}; |
| |
| |
| #define GET 0 |
| #define HEAD 1 |
| #define POST 2 |
| #define PUT 3 |
| #define DELETE 4 |
| #define COPY 5 |
| #define OPTIONS 6 |
| |
| |
| static bool go(JSContext* cx, JSObject* obj, HTTPData* http, std::string& body); |
| |
| |
| bool |
| http_ctor(JSContext* cx, JSObject* req) |
| { |
| HTTPData* http = new HTTPData(); |
| bool ret = false; |
| |
| if(!http) |
| { |
| JS_ReportErrorUTF8(cx, "Failed to create CouchHTTP instance."); |
| goto error; |
| } |
| |
| http->method = -1; |
| http->req_headers = NULL; |
| http->last_status = -1; |
| |
| JS_SetPrivate(req, http); |
| |
| ret = true; |
| goto success; |
| |
| error: |
| if(http) delete http; |
| |
| success: |
| return ret; |
| } |
| |
| |
| void |
| http_dtor(JSFreeOp* fop, JSObject* obj) |
| { |
| HTTPData* http = (HTTPData*) JS_GetPrivate(obj); |
| if(http) { |
| if(http->req_headers) curl_slist_free_all(http->req_headers); |
| delete http; |
| } |
| } |
| |
| |
| bool |
| http_open(JSContext* cx, JSObject* req, JS::Value mth, JS::Value url, JS::Value snc) |
| { |
| HTTPData* http = (HTTPData*) JS_GetPrivate(req); |
| int methid; |
| |
| if(!http) { |
| JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance."); |
| return false; |
| } |
| |
| if(!mth.isString()) { |
| JS_ReportErrorUTF8(cx, "Method must be a string."); |
| return false; |
| } |
| |
| std::string method; |
| if(!js_to_string(cx, JS::RootedValue(cx, mth), method)) { |
| JS_ReportErrorUTF8(cx, "Failed to encode method."); |
| return false; |
| } |
| |
| for(methid = 0; METHODS[methid] != NULL; methid++) { |
| if(strcasecmp(METHODS[methid], method.c_str()) == 0) break; |
| } |
| |
| if(methid > OPTIONS) { |
| JS_ReportErrorUTF8(cx, "Invalid method specified."); |
| return false; |
| } |
| |
| http->method = methid; |
| |
| if(!url.isString()) { |
| JS_ReportErrorUTF8(cx, "URL must be a string"); |
| return false; |
| } |
| |
| std::string urlstr; |
| if(!js_to_string(cx, JS::RootedValue(cx, url), urlstr)) { |
| JS_ReportErrorUTF8(cx, "Failed to encode URL."); |
| return false; |
| } |
| http->url = urlstr; |
| |
| if(snc.isBoolean() && snc.isTrue()) { |
| JS_ReportErrorUTF8(cx, "Synchronous flag must be false."); |
| return false; |
| } |
| |
| if(http->req_headers) { |
| curl_slist_free_all(http->req_headers); |
| http->req_headers = NULL; |
| } |
| |
| // Disable Expect: 100-continue |
| http->req_headers = curl_slist_append(http->req_headers, "Expect:"); |
| |
| return true; |
| } |
| |
| |
| bool |
| http_set_hdr(JSContext* cx, JSObject* req, JS::Value name, JS::Value val) |
| { |
| HTTPData* http = (HTTPData*) JS_GetPrivate(req); |
| |
| if(!http) { |
| JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance."); |
| return false; |
| } |
| |
| if(!name.isString()) |
| { |
| JS_ReportErrorUTF8(cx, "Header names must be strings."); |
| return false; |
| } |
| |
| std::string keystr; |
| if(!js_to_string(cx, JS::RootedValue(cx, name), keystr)) |
| { |
| JS_ReportErrorUTF8(cx, "Failed to encode header name."); |
| return false; |
| } |
| |
| if(!val.isString()) |
| { |
| JS_ReportErrorUTF8(cx, "Header values must be strings."); |
| return false; |
| } |
| |
| std::string valstr; |
| if(!js_to_string(cx, JS::RootedValue(cx, val), valstr)) { |
| JS_ReportErrorUTF8(cx, "Failed to encode header value."); |
| return false; |
| } |
| |
| std::string header = keystr + ": " + valstr; |
| http->req_headers = curl_slist_append(http->req_headers, header.c_str()); |
| |
| return true; |
| } |
| |
| bool |
| http_send(JSContext* cx, JSObject* req, JS::Value body) |
| { |
| HTTPData* http = (HTTPData*) JS_GetPrivate(req); |
| |
| if(!http) { |
| JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance."); |
| return false; |
| } |
| |
| std::string bodystr; |
| if(!js_to_string(cx, JS::RootedValue(cx, body), bodystr)) { |
| JS_ReportErrorUTF8(cx, "Failed to encode body."); |
| return false; |
| } |
| |
| return go(cx, req, http, bodystr); |
| } |
| |
| int |
| http_status(JSContext* cx, JSObject* req) |
| { |
| HTTPData* http = (HTTPData*) JS_GetPrivate(req); |
| |
| if(!http) { |
| JS_ReportErrorUTF8(cx, "Invalid CouchHTTP instance."); |
| return false; |
| } |
| |
| return http->last_status; |
| } |
| |
| bool |
| http_uri(JSContext* cx, JSObject* req, couch_args* args, JS::Value* uri_val) |
| { |
| FILE* uri_fp = NULL; |
| JSString* uri_str; |
| |
| // Default is http://localhost:15986/ when no uri file is specified |
| if (!args->uri_file) { |
| uri_str = JS_NewStringCopyZ(cx, "http://localhost:15986/"); |
| *uri_val = JS::StringValue(uri_str); |
| JS_SetReservedSlot(req, 0, *uri_val); |
| return true; |
| } |
| |
| // Else check to see if the base url is cached in a reserved slot |
| *uri_val = JS_GetReservedSlot(req, 0); |
| if (!(*uri_val).isUndefined()) { |
| return true; |
| } |
| |
| // Read the first line of the couch.uri file. |
| if(!((uri_fp = fopen(args->uri_file, "r")) && |
| (uri_str = couch_readline(cx, uri_fp)))) { |
| JS_ReportErrorUTF8(cx, "Failed to read couch.uri file."); |
| goto error; |
| } |
| |
| fclose(uri_fp); |
| *uri_val = JS::StringValue(uri_str); |
| JS_SetReservedSlot(req, 0, *uri_val); |
| return true; |
| |
| error: |
| if(uri_fp) fclose(uri_fp); |
| return false; |
| } |
| |
| |
| // Curl Helpers |
| |
| typedef struct { |
| HTTPData* http; |
| JSContext* cx; |
| JSObject* resp_headers; |
| const char* sendbuf; |
| size_t sendlen; |
| size_t sent; |
| int sent_once; |
| char* recvbuf; |
| size_t recvlen; |
| size_t read; |
| } CurlState; |
| |
| /* |
| * I really hate doing this but this doesn't have to be |
| * uber awesome, it just has to work. |
| */ |
| CURL* HTTP_HANDLE = NULL; |
| char ERRBUF[CURL_ERROR_SIZE]; |
| |
| static size_t send_body(void *ptr, size_t size, size_t nmem, void *data); |
| static int seek_body(void *ptr, curl_off_t offset, int origin); |
| static size_t recv_body(void *ptr, size_t size, size_t nmem, void *data); |
| static size_t recv_header(void *ptr, size_t size, size_t nmem, void *data); |
| |
| static bool |
| go(JSContext* cx, JSObject* obj, HTTPData* http, std::string& body) |
| { |
| CurlState state; |
| JSString* jsbody; |
| bool ret = false; |
| JS::Value tmp; |
| JS::RootedObject robj(cx, obj); |
| JS::RootedValue vobj(cx); |
| |
| |
| state.cx = cx; |
| state.http = http; |
| |
| state.sendbuf = body.c_str(); |
| state.sendlen = body.size(); |
| state.sent = 0; |
| state.sent_once = 0; |
| |
| state.recvbuf = NULL; |
| state.recvlen = 0; |
| state.read = 0; |
| |
| if(HTTP_HANDLE == NULL) { |
| HTTP_HANDLE = curl_easy_init(); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_READFUNCTION, send_body); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_SEEKFUNCTION, |
| (curl_seek_callback) seek_body); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_HEADERFUNCTION, recv_header); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_WRITEFUNCTION, recv_body); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_NOPROGRESS, 1); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_ERRORBUFFER, ERRBUF); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_COOKIEFILE, ""); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_USERAGENT, |
| "CouchHTTP Client - Relax"); |
| } |
| |
| if(!HTTP_HANDLE) { |
| JS_ReportErrorUTF8(cx, "Failed to initialize cURL handle."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| |
| tmp = JS_GetReservedSlot(obj, 0); |
| |
| std::string referer; |
| if(!js_to_string(cx, JS::RootedValue(cx, tmp), referer)) { |
| JS_ReportErrorUTF8(cx, "Failed to encode referer."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_REFERER, referer.c_str()); |
| |
| if(http->method < 0 || http->method > OPTIONS) { |
| JS_ReportErrorUTF8(cx, "INTERNAL: Unknown method."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_CUSTOMREQUEST, METHODS[http->method]); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_NOBODY, 0); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_FOLLOWLOCATION, 1); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_UPLOAD, 0); |
| |
| if(http->method == HEAD) { |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_NOBODY, 1); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_FOLLOWLOCATION, 0); |
| } else if(http->method == POST || http->method == PUT) { |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_UPLOAD, 1); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_FOLLOWLOCATION, 0); |
| } |
| |
| if(body.size() > 0) { |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_INFILESIZE, body.size()); |
| } else { |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_INFILESIZE, 0); |
| } |
| |
| // curl_easy_setopt(HTTP_HANDLE, CURLOPT_VERBOSE, 1); |
| |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_URL, http->url.c_str()); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_HTTPHEADER, http->req_headers); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_READDATA, &state); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_SEEKDATA, &state); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_WRITEHEADER, &state); |
| curl_easy_setopt(HTTP_HANDLE, CURLOPT_WRITEDATA, &state); |
| |
| if(curl_easy_perform(HTTP_HANDLE) != 0) { |
| JS_ReportErrorUTF8(cx, "Failed to execute HTTP request: %s", ERRBUF); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| |
| if(!state.resp_headers) { |
| JS_ReportErrorUTF8(cx, "Failed to recieve HTTP headers."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| tmp = JS::ObjectValue(*state.resp_headers); |
| JS::RootedValue rtmp(cx, tmp); |
| |
| if(!JS_DefineProperty( |
| cx, robj, |
| "_headers", |
| rtmp, |
| JSPROP_READONLY |
| )) { |
| JS_ReportErrorUTF8(cx, "INTERNAL: Failed to set response headers."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret;; |
| } |
| |
| if(state.recvbuf) { |
| state.recvbuf[state.read] = '\0'; |
| std::string bodystr(state.recvbuf, state.read); |
| jsbody = string_to_js(cx, bodystr); |
| if(!jsbody) { |
| // If we can't decode the body as UTF-8 we forcefully |
| // convert it to a string by just forcing each byte |
| // to a char16_t. |
| jsbody = JS_NewStringCopyN(cx, state.recvbuf, state.read); |
| if(!jsbody) { |
| if(!JS_IsExceptionPending(cx)) { |
| JS_ReportErrorUTF8(cx, "INTERNAL: Failed to decode body."); |
| } |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| } |
| tmp = JS::StringValue(jsbody); |
| } else { |
| tmp = JS_GetEmptyStringValue(cx); |
| } |
| |
| JS::RootedValue rtmp2(cx, tmp); |
| |
| if(!JS_DefineProperty( |
| cx, robj, |
| "responseText", |
| rtmp2, |
| JSPROP_READONLY |
| )) { |
| JS_ReportErrorUTF8(cx, "INTERNAL: Failed to set responseText."); |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| |
| ret = true; |
| if(state.recvbuf) JS_free(cx, state.recvbuf); |
| return ret; |
| } |
| |
| static size_t |
| send_body(void *ptr, size_t size, size_t nmem, void *data) |
| { |
| CurlState* state = static_cast<CurlState*>(data); |
| size_t length = size * nmem; |
| size_t towrite = state->sendlen - state->sent; |
| |
| // Assume this is cURL trying to resend a request that |
| // failed. |
| if(towrite == 0 && state->sent_once == 0) { |
| state->sent_once = 1; |
| return 0; |
| } else if(towrite == 0) { |
| state->sent = 0; |
| state->sent_once = 0; |
| towrite = state->sendlen; |
| } |
| |
| if(length < towrite) towrite = length; |
| |
| memcpy(ptr, state->sendbuf + state->sent, towrite); |
| state->sent += towrite; |
| |
| return towrite; |
| } |
| |
| static int |
| seek_body(void* ptr, curl_off_t offset, int origin) |
| { |
| CurlState* state = static_cast<CurlState*>(ptr); |
| if(origin != SEEK_SET) return -1; |
| |
| state->sent = static_cast<size_t>(offset); |
| return static_cast<int>(state->sent); |
| } |
| |
| static size_t |
| recv_header(void *ptr, size_t size, size_t nmem, void *data) |
| { |
| CurlState* state = static_cast<CurlState*>(data); |
| char code[4]; |
| char* header = static_cast<char*>(ptr); |
| size_t length = size * nmem; |
| JSString* hdr = NULL; |
| uint32_t hdrlen; |
| |
| if(length > 7 && strncasecmp(header, "HTTP/1.", 7) == 0) { |
| if(length < 12) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| memcpy(code, header+9, 3*sizeof(char)); |
| code[3] = '\0'; |
| state->http->last_status = atoi(code); |
| |
| state->resp_headers = JS_NewArrayObject(state->cx, 0); |
| if(!state->resp_headers) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| return length; |
| } |
| |
| // We get a notice at the \r\n\r\n after headers. |
| if(length <= 2) { |
| return length; |
| } |
| |
| // Append the new header to our array. |
| std::string hdrstr(header, length); |
| hdr = string_to_js(state->cx, hdrstr); |
| if(!hdr) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| JS::RootedObject obj(state->cx, state->resp_headers); |
| if(!JS_GetArrayLength(state->cx, obj, &hdrlen)) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| JS::RootedString hdrval(state->cx, hdr); |
| if(!JS_SetElement(state->cx, obj, hdrlen, hdrval)) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| return length; |
| } |
| |
| static size_t |
| recv_body(void *ptr, size_t size, size_t nmem, void *data) |
| { |
| CurlState* state = static_cast<CurlState*>(data); |
| size_t length = size * nmem; |
| char* tmp = NULL; |
| |
| if(!state->recvbuf) { |
| state->recvlen = 4096; |
| state->read = 0; |
| state->recvbuf = static_cast<char*>(JS_malloc( |
| state->cx, |
| state->recvlen |
| )); |
| } |
| |
| if(!state->recvbuf) { |
| return CURLE_WRITE_ERROR; |
| } |
| |
| // +1 so we can add '\0' back up in the go function. |
| size_t oldlen = state->recvlen; |
| while(length+1 > state->recvlen - state->read) state->recvlen *= 2; |
| tmp = static_cast<char*>(JS_realloc( |
| state->cx, |
| state->recvbuf, |
| oldlen, |
| state->recvlen |
| )); |
| if(!tmp) return CURLE_WRITE_ERROR; |
| state->recvbuf = tmp; |
| |
| memcpy(state->recvbuf + state->read, ptr, length); |
| state->read += length; |
| return length; |
| } |
| |
| #endif /* HAVE_CURL */ |