diff --git a/src/httprpc.cpp b/src/httprpc.cpp --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -59,6 +59,8 @@ static std::string strRPCUserColonPass; /* Stored RPC timer interface (for unregistration) */ static HTTPRPCTimerInterface *httpRPCTimerInterface = 0; +/* RPC CORS Domain, allowed Origin */ +static std::string strRPCCORSDomain; static void JSONErrorReply(HTTPRequest *req, const UniValue &objError, const UniValue &id) { @@ -149,8 +151,135 @@ return multiUserAuthorized(strUserPass); } +static bool checkCORS(HTTPRequest *req) { + // https://www.w3.org/TR/cors/#resource-requests + + // 1. If the Origin header is not present terminate this set of steps. + // The request is outside the scope of this specification. + std::pair origin = req->GetHeader("origin"); + if (!origin.first) { + return false; + } + + // 2. If the value of the Origin header is not a case-sensitive match for + // any of the values in list of origins do not set any additional headers + // and terminate this set of steps. + // Note: Always matching is acceptable since the list of origins can be + // unbounded. + if (origin.second != strRPCCORSDomain) { + return false; + } + + if (req->GetRequestMethod() == HTTPRequest::OPTIONS) { + // 6.2 Preflight Request + // In response to a preflight request the resource indicates which + // methods and headers (other than simple methods and simple + // headers) it is willing to handle and whether it supports + // credentials. + // Resources must use the following set of steps to determine which + // additional headers to use in the response: + + // 3. Let method be the value as result of parsing the + // Access-Control-Request-Method header. + // If there is no Access-Control-Request-Method header or if parsing + // failed, do not set any additional headers and terminate this set + // of steps. The request is outside the scope of this specification. + std::pair method = + req->GetHeader("access-control-request-method"); + if (!method.first) { + return false; + } + + // 4. Let header field-names be the values as result of parsing + // the Access-Control-Request-Headers headers. + // If there are no Access-Control-Request-Headers headers let header + // field-names be the empty list. + // If parsing failed do not set any additional headers and terminate + // this set of steps. The request is outside the scope of this + // specification. + std::pair header_field_names = + req->GetHeader("access-control-request-headers"); + + // 5. If method is not a case-sensitive match for any of the + // values in list of methods do not set any additional headers + // and terminate this set of steps. + // Note: Always matching is acceptable since the list of methods + // can be unbounded. + if (method.second != "POST") { + return false; + } + + // 6. If any of the header field-names is not a ASCII case- + // insensitive match for any of the values in list of headers do not + // set any additional headers and terminate this set of steps. + // Note: Always matching is acceptable since the list of headers can + // be unbounded. + const std::string& list_of_headers = "authorization,content-type"; + + // 7. If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value, and add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + req->WriteHeader("Access-Control-Allow-Origin", origin.second); + req->WriteHeader("Access-Control-Allow-Credentials", "true"); + + // 8. Optionally add a single Access-Control-Max-Age header with as + // value the amount of seconds the user agent is allowed to cache + // the result of the request. + + // 9. If method is a simple method this step may be skipped. + // Add one or more Access-Control-Allow-Methods headers consisting + // of (a subset of) the list of methods. + // If a method is a simple method it does not need to be listed, but + // this is not prohibited. + // Note: Since the list of methods can be unbounded, simply + // returning the method indicated by + // Access-Control-Request-Method (if supported) can be enough. + req->WriteHeader("Access-Control-Allow-Methods", method.second); + + // 10. If each of the header field-names is a simple header and none + // is Content-Type, this step may be skipped. + // Add one or more Access-Control-Allow-Headers headers consisting + // of (a subset of) the list of headers. + req->WriteHeader( + "Access-Control-Allow-Headers", + header_field_names.first ? header_field_names.second + : list_of_headers); + req->WriteReply(HTTP_OK); + return true; + } + + // 6.1 Simple Cross-Origin Request, Actual Request, and Redirects + // In response to a simple cross-origin request or actual request the + // resource indicates whether or not to share the response. + // If the resource has been relocated, it indicates whether to share its + // new URL. + // Resources must use the following set of steps to determine which + // additional headers to use in the response: + + // 3. If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value, and add a single Access-Control-Allow-Credentials + // header with the case-sensitive string "true" as value. + req->WriteHeader("Access-Control-Allow-Origin", origin.second); + req->WriteHeader("Access-Control-Allow-Credentials", "true"); + + // 4. If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + req->WriteHeader("Access-Control-Expose-Headers", "WWW-Authenticate"); + + return false; +} + static bool HTTPReq_JSONRPC(Config &config, HTTPRequest *req, const std::string &) { + // First, check and/or set CORS headers + if (checkCORS(req)) { + return true; + } + // JSONRPC handles only POST if (req->GetRequestMethod() != HTTPRequest::POST) { req->WriteReply(HTTP_BAD_METHOD, @@ -236,6 +365,8 @@ strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", ""); } + + strRPCCORSDomain = GetArg("-rpccorsdomain", ""); return true; } diff --git a/src/httpserver.h b/src/httpserver.h --- a/src/httpserver.h +++ b/src/httpserver.h @@ -64,7 +64,7 @@ HTTPRequest(struct evhttp_request *req); ~HTTPRequest(); - enum RequestMethod { UNKNOWN, GET, POST, HEAD, PUT }; + enum RequestMethod { UNKNOWN, GET, POST, HEAD, PUT, OPTIONS }; /** Get requested URI. */ diff --git a/src/httpserver.cpp b/src/httpserver.cpp --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -210,16 +210,14 @@ switch (m) { case HTTPRequest::GET: return "GET"; - break; case HTTPRequest::POST: return "POST"; - break; case HTTPRequest::HEAD: return "HEAD"; - break; case HTTPRequest::PUT: return "PUT"; - break; + case HTTPRequest::OPTIONS: + return "OPTIONS"; default: return "unknown"; } @@ -418,6 +416,14 @@ evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE); evhttp_set_max_body_size(http, MAX_SIZE); evhttp_set_gencb(http, http_request_cb, &config); + /* Only POST and OPTIONS are supported, but we return HTTP 405 for the others */ + evhttp_set_allowed_methods(http, + EVHTTP_REQ_GET | + EVHTTP_REQ_POST | + EVHTTP_REQ_HEAD | + EVHTTP_REQ_PUT | + EVHTTP_REQ_DELETE | + EVHTTP_REQ_OPTIONS); if (!HTTPBindAddresses(http)) { LogPrintf("Unable to bind any endpoint for RPC server\n"); @@ -623,19 +629,16 @@ switch (evhttp_request_get_command(req)) { case EVHTTP_REQ_GET: return GET; - break; case EVHTTP_REQ_POST: return POST; - break; case EVHTTP_REQ_HEAD: return HEAD; - break; case EVHTTP_REQ_PUT: return PUT; - break; + case EVHTTP_REQ_OPTIONS: + return OPTIONS; default: return UNKNOWN; - break; } } diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -891,6 +891,9 @@ strprintf( _("Set the number of threads to service RPC calls (default: %d)"), DEFAULT_HTTP_THREADS)); + strUsage += HelpMessageOpt( + "-rpccorsdomain=value", + "Domain from which to accept cross origin requests (browser enforced)"); if (showDebug) { strUsage += HelpMessageOpt( "-rpcworkqueue=", strprintf("Set the depth of the work queue to " diff --git a/test/functional/httpbasics.py b/test/functional/httpbasics.py --- a/test/functional/httpbasics.py +++ b/test/functional/httpbasics.py @@ -19,6 +19,7 @@ self.num_nodes = 3 def setup_network(self): + self.extra_args =[["-rpccorsdomain=null"], [], []] self.setup_nodes() def run_test(self): @@ -120,6 +121,54 @@ out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) + # Check Standard CORS request + origin = "null" + + conn = http.client.HTTPConnection(url.hostname, url.port) + conn.connect() + authpair = url.username + ':' + url.password + headers = {"Authorization": "Basic " + str_to_b64str(authpair), + "Origin": origin} + conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + out1 = conn.getresponse() + assert_equal(out1.status, http.client.OK) + assert_equal(out1.headers["Access-Control-Allow-Origin"], origin) + assert_equal(out1.headers["Access-Control-Allow-Credentials"], "true") + assert_equal(out1.headers["Access-Control-Expose-Headers"], + "WWW-Authenticate") + assert(b'"error":null' in out1.read()) + + # Check Pre-flight CORS request + corsheaders = {"Origin": origin, + "Access-Control-Request-Method": "POST"} + conn.request('OPTIONS', '/', None, corsheaders) + out1 = conn.getresponse() + assert_equal(out1.status, http.client.OK) + assert_equal(out1.headers["Access-Control-Allow-Origin"], origin) + assert_equal(out1.headers["Access-Control-Allow-Credentials"], "true") + assert_equal(out1.headers["Access-Control-Allow-Methods"], "POST") + assert_equal(out1.headers["Access-Control-Allow-Headers"], + "authorization,content-type") + assert_equal(b'', out1.read()) + + # Check Standard CORS request to node without CORS, expected failure + conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) + conn.connect() + authpair = url.username + ':' + url.password + headers = {"Authorization": "Basic " + str_to_b64str(authpair), + "Origin": origin} + conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + out1 = conn.getresponse() + assert_equal(out1.status, http.client.UNAUTHORIZED) + assert_equal(b'', out1.read()) + + # Check Pre-flight CORS request to node without CORS, expected failure + corsheaders = {"Origin": origin, + "Access-Control-Request-Method": "POST"} + conn.request('OPTIONS', '/', None, corsheaders) + out1 = conn.getresponse() + assert_equal(out1.status, http.client.METHOD_NOT_ALLOWED) + assert_equal(b'JSONRPC server handles only POST requests', out1.read()) if __name__ == '__main__': HTTPBasicsTest().main()