diff --git a/src/ring/middleware/cors.clj b/src/ring/middleware/cors.clj index d0c2b91..4504eb9 100644 --- a/src/ring/middleware/cors.clj +++ b/src/ring/middleware/cors.clj @@ -157,18 +157,47 @@ % [%])))) -(defn handle-cors [handler request access-control response-handler] - (if (and (preflight? request) (allow-request? request access-control)) - (let [blank-response {:status 200 - :headers {} - :body "preflight complete"}] - (response-handler request access-control blank-response)) - (if (origin request) - (if (allow-request? request access-control) - (if-let [response (handler request)] - (response-handler request access-control response)) - (handler request)) - (handler request)))) +(defn- make-preflight-handler [access-control] + (fn [args] + (let [[request respond _] args + response (add-access-control request + access-control + {:status 200 + :headers {} + :body "preflight complete"})] + (if (= (count args) 1) + response + (respond response))))) + +(defn- wrap-handler [handler access-control] + (fn + ([args] + (let [request (first args)] + (if (origin request) + (if (allow-request? request access-control) + (if (= (count args) 1) + ;; synchronous request + (if-let [response (handler request)] + (add-access-control request access-control response)) + ;; asynchronous request + (let [[_ respond raise] args] + (handler request + #(respond (if % + (add-access-control request + access-control + %))) + raise))) + (apply handler args)) + (apply handler args)))))) + +(defn- handle-cors [handler access-control handler-args] + (let [preflight-handler (make-preflight-handler access-control) + wrapped-handler (wrap-handler handler access-control) + request (first handler-args)] + (if (and (preflight? request) + (allow-request? request access-control)) + (preflight-handler handler-args) + (wrapped-handler handler-args)))) (defn wrap-cors "Middleware that adds Cross-Origin Resource Sharing headers. @@ -181,5 +210,8 @@ " [handler & access-control] (let [access-control (normalize-config access-control)] - (fn [request] - (handle-cors handler request access-control add-access-control)))) + (fn + ([request] + (handle-cors handler access-control [request])) + ([request respond raise] + (handle-cors handler access-control [request respond raise]))))) diff --git a/test/ring/middleware/cors_test.clj b/test/ring/middleware/cors_test.clj index e541307..f5f2204 100644 --- a/test/ring/middleware/cors_test.clj +++ b/test/ring/middleware/cors_test.clj @@ -6,7 +6,7 @@ (testing "with empty vector" (is (not (allow-request? {:headers {"origin" "http://eample.com"}} {:access-control-allow-origin []})))) - (testing "with one regular expressions" + (testing "with one regular expression" (are [origin expected] (is (= expected (allow-request? @@ -35,12 +35,17 @@ "http://api.burningswell.com" true "http://dev.burningswell.com" true))) -(defn handler [request] - ((wrap-cors (fn [_] {}) - :access-control-allow-origin #"http://example.com" - :access-control-allow-headers #{:accept :content-type} - :access-control-allow-methods #{:get :put :post}) - request)) +(def sync-handler + (wrap-cors (fn [_] {}) + :access-control-allow-origin #"http://example.com" + :access-control-allow-headers #{:accept :content-type} + :access-control-allow-methods #{:get :put :post})) + +(def async-handler + (wrap-cors (fn [_ respond _] (respond {})) + :access-control-allow-origin #"http://example.com" + :access-control-allow-headers #{:accept :content-type} + :access-control-allow-methods #{:get :put :post})) (deftest test-preflight (testing "whitelist concrete headers" @@ -52,9 +57,9 @@ "Access-Control-Allow-Headers" "Accept, Content-Type" "Access-Control-Allow-Methods" "GET, POST, PUT"} :body "preflight complete"} - (handler {:request-method :options - :uri "/" - :headers headers}))))) + (sync-handler {:request-method :options + :uri "/" + :headers headers}))))) (testing "whitelist any headers" (is (= {:status 200, @@ -72,12 +77,12 @@ "access-control-request-headers" "x-foo, x-bar"}})))) (testing "whitelist headers ignore case" - (is (= (handler {:request-method :options - :uri "/" - :headers {"origin" "http://example.com" - "access-control-request-method" "POST" - "access-control-request-headers" - "ACCEPT, CONTENT-TYPE"}}) + (is (= (sync-handler {:request-method :options + :uri "/" + :headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" + "ACCEPT, CONTENT-TYPE"}}) {:status 200 :headers {"Access-Control-Allow-Origin" "http://example.com" "Access-Control-Allow-Headers" "Accept, Content-Type" @@ -85,7 +90,7 @@ :body "preflight complete"}))) (testing "method not allowed" - (is (empty? (handler + (is (empty? (sync-handler {:request-method :options :uri "/" :headers {"origin" "http://example.com" @@ -95,34 +100,163 @@ (let [headers {"origin" "http://example.com" "access-control-request-method" "GET" "access-control-request-headers" "x-another-custom-header"}] - (is (empty? (handler + (is (empty? (sync-handler {:request-method :options :uri "/" :headers headers})))))) +(deftest test-preflight-async + (testing "whitelist concrete headers" + (let [headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" "Accept, Content-Type"} + response (promise) + exception (promise)] + (async-handler {:request-method :options + :uri "/" + :headers headers} + response + exception) + (is (= @response + {:status 200 + :headers {"Access-Control-Allow-Origin" "http://example.com" + "Access-Control-Allow-Headers" "Accept, Content-Type" + "Access-Control-Allow-Methods" "GET, POST, PUT"} + :body "preflight complete"})) + (is (not (realized? exception))))) + + (testing "whitelist any headers" + (let [handler (wrap-cors (fn [_ respond _] (respond {})) + :access-control-allow-origin #"http://example.com" + :access-control-allow-methods #{:get :put :post}) + response (promise) + exception (promise)] + (handler {:request-method :options + :uri "/" + :headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" "x-foo, x-bar"}} + response + exception) + (is (= @response + {:status 200 + :headers {"Access-Control-Allow-Origin" "http://example.com" + "Access-Control-Allow-Headers" "X-Bar, X-Foo" + "Access-Control-Allow-Methods" "GET, POST, PUT"} + :body "preflight complete"})) + (is (not (realized? exception))))) + + (testing "whitelist headers ignore case" + (let [headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" + "ACCEPT, CONTENT-TYPE"} + response (promise) + exception (promise)] + (async-handler {:request-method :options + :uri "/" + :headers headers} + response + exception) + (is (= @response + {:status 200 + :headers {"Access-Control-Allow-Origin" "http://example.com" + "Access-Control-Allow-Headers" "Accept, Content-Type" + "Access-Control-Allow-Methods" "GET, POST, PUT"} + :body "preflight complete"})) + (is (not (realized? exception))))) + + (testing "method not allowed" + (let [response (promise) + exception (promise)] + (async-handler {:request-method :options + :uri "/" + :headers {"origin" "http://example.com" + "access-control-request-method" "DELETE"}} + response + exception) + (is (empty? @response)) + (is (not (realized? exception))))) + + (testing "header not allowed" + (let [headers {"origin" "http://example.com" + "access-control-request-method" "GET" + "access-control-request-headers" "x-another-custom-header"} + response (promise) + exception (promise)] + (async-handler {:request-method :options + :uri "/" + :headers headers} + response + exception) + (is (empty? @response)) + (is (not (realized? exception)))))) + (deftest test-preflight-header-subset - (is (= (handler {:request-method :options - :uri "/" - :headers {"origin" "http://example.com" - "access-control-request-method" "POST" - "access-control-request-headers" "Accept"}}) + (is (= (sync-handler {:request-method :options + :uri "/" + :headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" "Accept"}}) {:status 200 :headers {"Access-Control-Allow-Origin" "http://example.com" "Access-Control-Allow-Headers" "Accept, Content-Type" "Access-Control-Allow-Methods" "GET, POST, PUT"} :body "preflight complete"}))) +(deftest test-preflight-header-subset-async + (let [response (promise) + exception (promise)] + (async-handler {:request-method :options + :uri "/" + :headers {"origin" "http://example.com" + "access-control-request-method" "POST" + "access-control-request-headers" "Accept"}} + response + exception) + (is (= @response + {:status 200 + :headers {"Access-Control-Allow-Origin" "http://example.com" + "Access-Control-Allow-Headers" "Accept, Content-Type" + "Access-Control-Allow-Methods" "GET, POST, PUT"} + :body "preflight complete"})) + (is (not (realized? exception))))) + (deftest test-cors (testing "success" (is (= {:headers {"Access-Control-Allow-Methods" "GET, POST, PUT", "Access-Control-Allow-Origin" "http://example.com"}} - (handler {:request-method :post - :uri "/" - :headers {"origin" "http://example.com"}})))) - (testing "failure" - (is (empty? (handler {:request-method :get + (sync-handler {:request-method :post :uri "/" - :headers {"origin" "http://foo.com"}}))))) + :headers {"origin" "http://example.com"}})))) + (testing "failure" + (is (empty? (sync-handler {:request-method :get + :uri "/" + :headers {"origin" "http://foo.com"}}))))) + +(deftest test-cors-async + (testing "success" + (let [response (promise) + exception (promise)] + (async-handler {:request-method :post + :uri "/" + :headers {"origin" "http://example.com"}} + response + exception) + (is (= @response + {:headers {"Access-Control-Allow-Methods" "GET, POST, PUT", + "Access-Control-Allow-Origin" "http://example.com"}})) + (is (not (realized? exception))))) + (testing "failure" + (let [response (promise) + exception (promise)] + (async-handler {:request-method :get + :uri "/" + :headers {"origin" "http://foo.com"}} + response + exception) + (is (empty? @response)) + (is (not (realized? exception)))))) (deftest test-no-cors-header-when-handler-returns-nil (is (nil? ((wrap-cors (fn [_] nil) @@ -132,12 +266,38 @@ :get :uri "/" :headers {"origin" "http://example.com"}})))) +(deftest test-no-cors-header-when-handler-returns-nil-async + (let [handler (wrap-cors (fn [_ respond _] (respond nil)) + :access-control-allow-origin #".*example.com" + :access-control-allow-methods [:get]) + response (promise) + exception (promise)] + (handler {:request-method + :get :uri "/" + :headers {"origin" "http://example.com"}} + response + exception) + (is (nil? @response)) + (is (not (realized? exception))))) + (deftest test-options-without-cors-header (is (empty? ((wrap-cors (fn [_] {}) :access-control-allow-origin #".*example.com") {:request-method :options :uri "/"})))) +(deftest test-options-without-cors-header-async + (let [handler (wrap-cors + (fn [_ respond _] (respond {})) + :access-control-allow-origin #".*example.com") + response (promise) + exception (promise)] + (handler {:request-method :options :uri "/"} + response + exception) + (is (empty? @response)) + (is (not (realized? exception))))) + (deftest test-method-not-allowed (is (empty? ((wrap-cors (fn [_] {}) @@ -147,6 +307,21 @@ :headers {"origin" "http://foo.com"} :uri "/"})))) +(deftest test-method-not-allowed-async + (let [allowed-methods [:get :post :patch :put :delete] + handler (wrap-cors (fn [_ respond _] (respond {})) + :access-control-allow-origin #".*" + :access-control-allow-methods allowed-methods) + response (promise) + exception (promise)] + (handler {:request-method :options + :headers {"origin" "http://foo.com"} + :uri "/"} + response + exception) + (is (empty? @response)) + (is (not (realized? exception))))) + (deftest additional-headers (let [response ((wrap-cors (fn [_] {:status 200}) :access-control-allow-credentials "true" @@ -164,6 +339,27 @@ "Access-Control-Expose-Headers" "Etag"}} response)))) +(deftest additional-headers-async + (let [handler (wrap-cors (fn [_ respond _] (respond {:status 200})) + :access-control-allow-credentials "true" + :access-control-allow-origin #".*" + :access-control-allow-methods [:get] + :access-control-expose-headers "Etag") + response (promise) + exception (promise)] + (handler {:request-method :get + :uri "/" + :headers {"origin" "http://example.com"}} + response + exception) + (is (= @response + {:status 200 + :headers + {"Access-Control-Allow-Credentials" "true" + "Access-Control-Allow-Methods" "GET" + "Access-Control-Allow-Origin" "http://example.com" + "Access-Control-Expose-Headers" "Etag"}})))) + (deftest test-parse-headers (are [headers expected] (= (parse-headers headers) expected) @@ -216,3 +412,42 @@ :headers {"origin" "http://foo.com"} :uri "/"})] (is (empty? response))))) + +(deftest test-dynamic-allow-origin-async + (testing "Testing an allow-origin with a callback function + for dynamic checks, where it returns true (allowed)" + (let [handler (wrap-cors + (fn [_ respond _] (respond {})) + :access-control-allow-origin + (fn my-callback [req] true) + :access-control-allow-methods + [:get :post :patch :put :delete]) + response (promise) + exception (promise)] + (handler {:request-method :get + :headers {"origin" "http://foo.com"} + :uri "/"} + response + exception) + (is (= @response + {:headers + {"Access-Control-Allow-Methods" "DELETE, GET, PATCH, POST, PUT" + "Access-Control-Allow-Origin" "http://foo.com"}})) + (is (not (realized? exception))))) + (testing "Testing an allow-origin with a callback function for dynamic checks, + where it returns false (not allowed)" + (let [handler (wrap-cors + (fn [_ respond _] (respond {})) + :access-control-allow-origin + (fn my-callback [req] false) + :access-control-allow-methods + [:get :post :patch :put :delete]) + response (promise) + exception (promise)] + (handler {:request-method :get + :headers {"origin" "http://foo.com"} + :uri "/"} + response + exception) + (is (empty? @response)) + (is (not (realized? exception))))))