diff --git a/lib/httpServer.js b/lib/httpServer.js index c88425927c..a8b82850a8 100644 --- a/lib/httpServer.js +++ b/lib/httpServer.js @@ -15,20 +15,62 @@ module.exports = function(provider, logger) { // At this point, we have the headers, method, url and body, and can now // do whatever we need to in order to respond to this request. - var headers = { - "Access-Control-Allow-Origin": "*" - }; + var headers = {}; + + // https://fetch.spec.whatwg.org/#http-requests + let isCORSRequest = request.headers.hasOwnProperty("origin"); + let isCORSPreflight = isCORSRequest && + method === "OPTIONS" && + request.headers.hasOwnProperty("access-control-request-headers") && + request.headers.hasOwnProperty("access-control-request-method"); + + if (isCORSRequest) { + // From the spec: "It cannot be reliably identified as participating in the CORS protocol + // as the `Origin` header is also included for all requests whose method is neither + // `GET` nor `HEAD`." + headers["Access-Control-Allow-Origin"] = request.headers.origin; + + // Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials, + // it doesn't look like there will be an HTTP Request header that tells you whether or not to + // include this. Since web3 now sets Request.withCredentials this header needs to be set. + // https://github.com/ethereum/web3.js/pull/1722 + headers["Access-Control-Allow-Credentials"] = "true"; + } else { + // There is no origin so we'll default to wildcard. + headers["Access-Control-Allow-Origin"] = "*"; + } switch (method) { + // The options request will always be used to handle the preflight request. case "OPTIONS": - if (request.headers.hasOwnProperty("access-control-request-headers")) { + if (isCORSPreflight) { + // Explicitly set the origin instead of using *, since credentials + // can't be used in conjunction with *. This will always be set + // for valid preflight requests. + headers["Access-Control-Allow-Origin"] = request.headers.origin; + + // From the spec: https://fetch.spec.whatwg.org/#http-responses + // "For a CORS-preflight request, request’s credentials mode is always "omit", + // but for any subsequent CORS requests it might not be. Support therefore + // needs to be indicated as part of the HTTP response to the CORS-preflight request as well." + headers["Access-Control-Allow-Credentials"] = "true"; + headers["Access-Control-Allow-Headers"] = request.headers["access-control-request-headers"]; headers["Vary"] = "Access-Control-Request-Headers"; + + headers["Access-Control-Allow-Methods"] = "POST"; + + headers["Content-Length"] = 0; + response.writeHead(204, headers); + response.end(); + } else { + let errorMessage = "OPTIONS preflight request is missing at least on of the required "; + errorMessage += "fields: Origin, Access-Control-Request-Headers, Access-Control-Request-Method"; + headers["Content-Length"] = errorMessage.length; + response.writeHead(400, headers); + response.end(errorMessage); } - headers["Access-Control-Allow-Methods"] = "POST"; - headers["Content-Length"] = 0; - response.writeHead(204, headers); - response.end(); + break; case "POST": // console.log("Request coming in:", body); diff --git a/test/cors.js b/test/cors.js index 8d5483399e..6b373428f3 100644 --- a/test/cors.js +++ b/test/cors.js @@ -7,12 +7,14 @@ const customRequestHeader = "X-PINGOTHER"; function test(host, port) { describe("CORS", () => { - it("should request headers equals to response headers in a preflight request", (done) => { + it("should set request headers equals to response headers in a preflight request", (done) => { let req = request.options( { url: "http://" + host + ":" + port, headers: { - "Access-Control-Request-Headers": customRequestHeader + "Access-Control-Request-Headers": customRequestHeader, + "Access-Control-Request-Method": "POST", + "Origin": "https://localhost:3000" } }, function(error, response) { @@ -38,6 +40,92 @@ function test(host, port) { req.destroy(); }); + + it("should return an error message if the OPTIONS request is not a valid preflight request.", (done) => { + let origin = "https://localhost:3000"; + let req = request.options( + { + url: "http://" + host + ":" + port, + headers: { + "Origin": origin + } + }, + function(error, response) { + if (error) { + return done(error); + } + + assert.strictEqual( + response.statusCode, + 400, + "A an OPTIONS request that isn't a preflight request should return an error." + ); + + done(); + } + ); + + req.destroy(); + }); + + it("should set response.Access-Control-Allow-Origin to equal request.Origin if request.Origin is set", (done) => { + let origin = "https://localhost:3000"; + let req = request.options( + { + url: "http://" + host + ":" + port, + headers: { + "Origin": origin + } + }, + function(error, response) { + if (error) { + return done(error); + } + + let accessControlAllowOrigin = ""; + + if (response.headers.hasOwnProperty("access-control-allow-origin")) { + accessControlAllowOrigin = response.headers["access-control-allow-origin"]; + } + + assert.strictEqual( + accessControlAllowOrigin, + origin, + "response.Access-Control-Allow-Origin should equal request.Origin if request.Origin is set." + ); + + done(); + }); + + req.destroy(); + }); + + it("should set Access-Control-Allow-Credentials=true if the Origin is set.", (done) => { + let origin = "https://localhost:3000"; + let req = request.options( + { + url: "http://" + host + ":" + port, + headers: { + "Origin": origin + } + }, + function(error, response) { + if (error) { + return done(error); + } + + assert.strictEqual( + response.headers["access-control-allow-credentials"], + "true", + "response.Access-Control-Allow-Origin should equal request.Origin if request.Origin is set." + ); + + done(); + } + ); + + req.destroy(); + }); }); }