diff --git a/doc/release-notes.md b/doc/release-notes.md
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -3,4 +3,5 @@
This release includes the following features and fixes:
+ - Added CORS headers and pre-flight request support via RPC
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 = gArgs.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
@@ -218,16 +218,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";
}
@@ -428,6 +426,13 @@
http, MIN_SUPPORTED_BODY_SIZE + 2 * config.GetMaxBlockSize());
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");
evhttp_free(http);
@@ -632,19 +637,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
@@ -896,6 +896,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,55 @@
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()