Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
179 changes: 109 additions & 70 deletions src/fluree/server/handler.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion test/fluree/server/handler_cors_test.clj
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,4 +19,30 @@

(testing "Plain string origin"
(let [origins (system/parse-cors-origins ["http://localhost:3000"])]
(is (= ["http://localhost:3000"] origins))))))
(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))))))