diff --git a/README.md b/README.md index 2d238af..c901d22 100644 --- a/README.md +++ b/README.md @@ -833,6 +833,10 @@ settings include: (default: 120000) - `closedMode` - Enable closed mode (requires authentication) - `rootIdentities` - Array of root identity DIDs for authentication +- `corsOrigins` - Array of allowed CORS origins (default: `["*"]` accepts all). + Can be exact URLs (e.g., `"https://app.example.com"`), regex patterns + (e.g., `"^https://.*\\.example\\.com$"`), or `"*"` wildcard. For production + deployments, restrict to specific origins for security #### Example Configuration Here's a minimal standalone server configuration: @@ -865,12 +869,50 @@ Here's a minimal standalone server configuration: { "@id": "http", "@type": "API", - "httpPort": 8090 + "httpPort": 8090, + "corsOrigins": ["*"] } ] } ``` +#### CORS Configuration Examples + +For development, accept all origins: +```json +{ + "@id": "http", + "@type": "API", + "httpPort": 8090, + "corsOrigins": ["*"] +} +``` + +For production, restrict to specific origins: +```json +{ + "@id": "http", + "@type": "API", + "httpPort": 8090, + "corsOrigins": [ + "https://app.example.com", + "https://dashboard.example.com" + ] +} +``` + +Use regex patterns to match multiple subdomains: +```json +{ + "@id": "http", + "@type": "API", + "httpPort": 8090, + "corsOrigins": [ + "^https://.*\\.example\\.com$" + ] +} +``` + #### Profiles Configuration files can include profiles that override base settings. For example, a "dev" profile might use less memory and local file paths: diff --git a/src/fluree/server/handler.clj b/src/fluree/server/handler.clj index e0a943e..36adc23 100644 --- a/src/fluree/server/handler.clj +++ b/src/fluree/server/handler.clj @@ -208,13 +208,32 @@ :fluree/subscriptions subscriptions) handler))) +(def standard-request-headers + "Standard HTTP headers that browsers may send in cross-origin requests." + ["content-type" "accept" "origin" "authorization" "cache-control" "x-requested-with"]) + +(def fluree-request-header-keys + "Fluree-specific request headers that are consumed by wrap-request-header-opts." + ["fluree-track-meta" "fluree-track-fuel" "fluree-track-policy" "fluree-ledger" + "fluree-max-fuel" "fluree-identity" "fluree-policy-identity" "fluree-policy" + "fluree-policy-class" "fluree-policy-values" "fluree-format" "fluree-output" + "fluree-ledger-opts"]) + +(def fluree-response-header-keys + "Fluree-specific response headers that browsers may need to read in cross-origin requests." + fluree-request-header-keys) + (defn wrap-cors [cors-origins handler] - (let [origins (or cors-origins [#".*"])] + (let [origins (or cors-origins [#".*"]) + allow-headers (concat standard-request-headers fluree-request-header-keys) + expose-headers fluree-response-header-keys] (rmc/wrap-cors handler :access-control-allow-origin origins :access-control-allow-methods [:get :post :options] - :access-control-allow-headers :any + :access-control-allow-headers allow-headers + :access-control-expose-headers expose-headers + :access-control-max-age 86400 :access-control-allow-credentials false))) (defn unwrap-credential @@ -269,12 +288,6 @@ (handler req*)))) (handler req))))) -(def fluree-request-header-keys - ["fluree-track-meta" "fluree-track-fuel" "fluree-track-policy" "fluree-ledger" - "fluree-max-fuel" "fluree-identity" "fluree-policy-identity" "fluree-policy" - "fluree-policy-class" "fluree-policy-values" "fluree-format" "fluree-output" - "fluree-ledger-opts"]) - (defn parse-boolean-header [header name] (case (str/lower-case header) @@ -497,99 +510,125 @@ (def fluree-create-routes ["/create" - {:post {:summary "Endpoint for creating new ledgers" - :parameters {:body CreateRequestBody} - :responses {201 {:body CreateResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :handler #'create/default}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Endpoint for creating new ledgers" + :parameters {:body CreateRequestBody} + :responses {201 {:body CreateResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :handler #'create/default}}]) (def fluree-drop-route ["/drop" - {:post {:summary "Drop the specified ledger and delete all persisted artifacts." - :parameters {:body DropRequestBody} - :responses {200 {:body DropResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :coercion (rcm/create {:transformers {:body {:default rcm/json-transformer-provider}}}) - :handler #'drop/drop-handler}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Drop the specified ledger and delete all persisted artifacts." + :parameters {:body DropRequestBody} + :responses {200 {:body DropResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :coercion (rcm/create {:transformers {:body {:default rcm/json-transformer-provider}}}) + :handler #'drop/drop-handler}}]) (def fluree-transact-routes ["/transact" - {:post {:summary "Endpoint for submitting transactions" - :parameters {:body TransactRequestBody} - :responses {200 {:body TransactResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :handler #'srv-tx/update}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Endpoint for submitting transactions" + :parameters {:body TransactRequestBody} + :responses {200 {:body TransactResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :handler #'srv-tx/update}}]) (def fluree-update-route ["/update" - {:post {:summary "Endpoint for submitting transactions" - :parameters {:body TransactRequestBody} - :responses {200 {:body TransactResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :handler #'srv-tx/update}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Endpoint for submitting transactions" + :parameters {:body TransactRequestBody} + :responses {200 {:body TransactResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :handler #'srv-tx/update}}]) (def fluree-insert-route ["/insert" - {:post {:summary "Endpoint for inserting into the specified ledger." - :parameters {:body :any} - :responses {200 {:body TransactResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :handler #'srv-tx/insert}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Endpoint for inserting into the specified ledger." + :parameters {:body :any} + :responses {200 {:body TransactResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :handler #'srv-tx/insert}}]) (def fluree-upsert-route ["/upsert" - {:post {:summary "Endpoint for upserting into the specified ledger." - :parameters {:body :any} - :responses {200 {:body TransactResponseBody} - 400 {:body ErrorResponse} - 409 {:body ErrorResponse} - 500 {:body ErrorResponse}} - :handler #'srv-tx/upsert}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Endpoint for upserting into the specified ledger." + :parameters {:body :any} + :responses {200 {:body TransactResponseBody} + 400 {:body ErrorResponse} + 409 {:body ErrorResponse} + 500 {:body ErrorResponse}} + :handler #'srv-tx/upsert}}]) (def fluree-query-routes ["/query" - {:get query-endpoint - :post query-endpoint}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :get query-endpoint + :post query-endpoint}]) (def fluree-history-routes ["/history" - {:get history-endpoint - :post history-endpoint}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :get history-endpoint + :post history-endpoint}]) (def fluree-remote-routes ["/remote" ["/latestCommit" - {:post {:summary "Read latest commit for a ledger" - :parameters {:body LatestCommitRequestBody} - :handler #'remote/latest-commit}}] + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Read latest commit for a ledger" + :parameters {:body LatestCommitRequestBody} + :handler #'remote/latest-commit}}] ["/resource" - {:post {:summary "Read resource from address" - :parameters {:body AddressRequestBody} - :handler #'remote/read-resource-address}}] + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Read resource from address" + :parameters {:body AddressRequestBody} + :handler #'remote/read-resource-address}}] ["/hash" - {:post {:summary "Parse content hash from address" - :parameters {:body HashRequestBody} - :handler #'remote/parse-address-hash}}] + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Parse content hash from address" + :parameters {:body HashRequestBody} + :handler #'remote/parse-address-hash}}] ["/addresses" - {:post {:summary "Retrieve ledger address from alias" - :parameters {:body AliasRequestBody} - :handler #'remote/published-ledger-addresses}}]]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :post {:summary "Retrieve ledger address from alias" + :parameters {:body AliasRequestBody} + :handler #'remote/published-ledger-addresses}}]]) (def fluree-subscription-routes ["/subscribe" - {:get {:summary "Subscribe to ledger updates" - :parameters {:body SubscriptionRequestBody} - :handler #'subscription/default}}]) + {:options {:summary "CORS preflight" + :handler (fn [_] {:status 204})} + :get {:summary "Subscribe to ledger updates" + :parameters {:body SubscriptionRequestBody} + :handler #'subscription/default}}]) (def default-fluree-route-map {:create fluree-create-routes diff --git a/test/fluree/server/handler_cors_test.clj b/test/fluree/server/handler_cors_test.clj index bf67411..3eee0e1 100644 --- a/test/fluree/server/handler_cors_test.clj +++ b/test/fluree/server/handler_cors_test.clj @@ -1,5 +1,6 @@ (ns fluree.server.handler-cors-test (:require [clojure.test :refer [deftest is testing]] + [fluree.server.handler :as handler] [fluree.server.system :as system])) (deftest test-cors-origin-parsing @@ -18,4 +19,30 @@ (testing "Plain string origin" (let [origins (system/parse-cors-origins ["http://localhost:3000"])] - (is (= ["http://localhost:3000"] origins)))))) \ No newline at end of file + (is (= ["http://localhost:3000"] origins)))))) + +(deftest test-header-constants + (testing "Header constants are properly defined" + (testing "standard-request-headers includes common HTTP headers" + (is (some #{"content-type"} handler/standard-request-headers)) + (is (some #{"accept"} handler/standard-request-headers)) + (is (some #{"authorization"} handler/standard-request-headers))) + + (testing "fluree-request-header-keys includes Fluree-specific headers" + (is (some #{"fluree-track-meta"} handler/fluree-request-header-keys)) + (is (some #{"fluree-identity"} handler/fluree-request-header-keys)) + (is (some #{"fluree-policy"} handler/fluree-request-header-keys)) + (is (some #{"fluree-ledger"} handler/fluree-request-header-keys))) + + (testing "fluree-response-header-keys is defined" + (is (seq handler/fluree-response-header-keys))))) + +(deftest test-wrap-cors-composition + (testing "wrap-cors function properly composes headers from constants" + (let [test-handler (fn [_] {:status 200 :body "OK"}) + wrapped-handler (handler/wrap-cors nil test-handler) + ;; Call with a basic request to verify the middleware doesn't error + request {:request-method :get :uri "/test"} + response (wrapped-handler request)] + (is (= 200 (:status response))) + (is (= "OK" (:body response)))))) \ No newline at end of file