├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── deps.edn ├── project.clj ├── src └── ring │ └── middleware │ └── defaults.clj └── test └── ring ├── assets ├── public1 │ └── foo.txt └── public2 │ ├── bar.txt │ └── foo.txt └── middleware └── defaults_test.clj /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v5 9 | 10 | - name: Prepare java 11 | uses: actions/setup-java@v5 12 | with: 13 | distribution: 'zulu' 14 | java-version: '11' 15 | 16 | - name: Install clojure tools 17 | uses: DeLaGuardo/setup-clojure@13.4 18 | with: 19 | lein: 2.12.0 20 | 21 | - name: Cache clojure dependencies 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.m2/repository 25 | key: cljdeps-${{ hashFiles('project.clj') }} 26 | restore-keys: cljdeps- 27 | 28 | - name: Run tests 29 | run: lein test-all 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | /.cpcache 11 | /.clj-kondo 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.7.0 (2025-09-13) 2 | 3 | * Added content-length middleware 4 | * Updated dependencies 5 | 6 | ## 0.6.0 (2025-01-24) 7 | 8 | * Added websocket-keepalive middleware 9 | * Added `:safe-header` option to `:anti-forgery` key in `site-defaults` 10 | * Updated dependencies 11 | 12 | ## 0.5.0 (2024-04-27) 13 | 14 | * Changed minimum Clojure version to 1.9.0 15 | * Updated dependencies 16 | * Removed deprecated servlet API dependency 17 | 18 | ## 0.4.0 (2023-09-09) 19 | 20 | * Added optional map syntax for :static files and resources (#27) 21 | * Changed default session store to cookie store (#34) 22 | * Changed SameSite cookie default to browser default (i.e. Lax) (#32) 23 | * Updated dependencies 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | **Do** follow [the seven rules of a great Git commit message][1]. 4 | 5 | **Do** follow [the Clojure Style Guide][2]. 6 | 7 | **Do** include tests for your change when appropriate. 8 | 9 | **Do** ensure that the CI checks pass. 10 | 11 | **Do** squash the commits in your PR to remove corrections 12 | irrelevant to the code history, once the PR has been reviewed. 13 | 14 | **Do** feel free to pester the project maintainers about the PR if it 15 | hasn't been responded to. Sometimes notifications can be missed. 16 | 17 | **Don't** overuse vertical whitespace; avoid multiple sequential blank 18 | lines. 19 | 20 | **Don't** include more than one feature or fix in a single PR. 21 | 22 | **Don't** include changes unrelated to the purpose of the PR. This 23 | includes changing the project version number, adding lines to the 24 | `.gitignore` file, or changing the indentation or formatting. 25 | 26 | **Don't** open a new PR if changes are requested. Just push to the 27 | same branch and the PR will be updated. 28 | 29 | [1]: https://chris.beams.io/posts/git-commit/#seven-rules 30 | [2]: https://github.com/bbatsov/clojure-style-guide 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010 Mark McGranaghan 2 | Copyright (c) 2009-2018 James Reeves 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ring-Defaults [![Build Status](https://github.com/ring-clojure/ring-defaults/actions/workflows/test.yml/badge.svg)](https://github.com/ring-clojure/ring-defaults/actions/workflows/test.yml) 2 | 3 | Knowing what middleware to add to a Ring application, and in what 4 | order, can be difficult and prone to error. 5 | 6 | This library attempts to automate the process, by providing sensible 7 | and secure default configurations of Ring middleware for both websites 8 | and HTTP APIs. 9 | 10 | ## Installation 11 | 12 | Add the following dependency to your deps.edn file: 13 | 14 | ring/ring-defaults {:mvn/version "0.7.0"} 15 | 16 | Or to your Leiningen project file: 17 | 18 | [ring/ring-defaults "0.7.0"] 19 | 20 | ## Basic Usage 21 | 22 | The `wrap-defaults` middleware sets up standard Ring middleware based 23 | on a supplied configuration: 24 | 25 | ```clojure 26 | (require '[ring.middleware.defaults :refer :all]) 27 | 28 | (def site 29 | (wrap-defaults handler site-defaults)) 30 | ``` 31 | 32 | There are four configurations included with the middleware 33 | 34 | - `api-defaults` 35 | - `site-defaults` 36 | - `secure-api-defaults` 37 | - `secure-site-defaults` 38 | 39 | The "api" defaults will add support for urlencoded parameters, but not 40 | much else. 41 | 42 | The "site" defaults add support for parameters, cookies, sessions, 43 | static resources, file uploads, and a bunch of browser-specific 44 | security headers. 45 | 46 | The "secure" defaults force SSL. Unencrypted HTTP URLs are redirected 47 | to the equivalent HTTPS URL, and various headers and flags are sent to 48 | prevent the browser sending sensitive information over insecure 49 | channels. 50 | 51 | ## Proxies 52 | 53 | If your app is sitting behind a load balancer or reverse proxy, as is 54 | often the case in cloud-based deployments, you'll want to set `:proxy` 55 | to `true`: 56 | 57 | ```clojure 58 | (assoc secure-site-defaults :proxy true) 59 | ``` 60 | 61 | This is particularly important when your site is secured with SSL, as 62 | the SSL redirect middleware will get caught in a redirect loop if it 63 | can't determine the correct URL scheme of the request. 64 | 65 | ## Customizing 66 | 67 | The default configurations are just maps of options, and can be 68 | customized to suit your needs. For example, if you wanted the normal 69 | site defaults, but without session support, you could use: 70 | 71 | ```clojure 72 | (wrap-defaults handler (assoc site-defaults :session false)) 73 | ``` 74 | 75 | The following configuration keys are supported: 76 | 77 | - `:cookies` - Set to true to parse cookies from the request. 78 | 79 | - `:params` - 80 | A map of options that describes how to parse parameters from the 81 | request. 82 | 83 | - `:keywordize` - 84 | Set to true to turn the parameter keys into keywords. 85 | 86 | - `:multipart` - 87 | Set to true to parse urlencoded parameters in the query string and 88 | the request body, or supply a map of options to pass to the 89 | standard Ring [multipart-params][1] middleware. 90 | 91 | - `:nested` - 92 | Set to true to allow nested parameters via the standard Ring 93 | [nested-params][2] middleware 94 | 95 | - `:urlencoded` - 96 | Set to true to parse urlencoded parameters in the query string and 97 | the request body. 98 | 99 | - `:proxy` - 100 | Set to true if the application is running behind a reverse proxy or 101 | load balancer. 102 | 103 | - `:responses` - 104 | A map of options to augment the responses from your application. 105 | 106 | - `:absolute-redirects` - 107 | Any redirects to relative URLs will be turned into redirects to 108 | absolute URLs, to better conform to the HTTP spec. 109 | 110 | - `:content-length` - 111 | Adds the standard Ring [content-length][17] middleware. 112 | 113 | - `:content-types` - 114 | Adds the standard Ring [content-type][3] middleware. 115 | 116 | - `:default-charset` - 117 | Adds a default charset to any text content-type lacking a charset. 118 | 119 | - `:not-modified-responses` - 120 | Adds the standard Ring [not-modified][4] middleware. 121 | 122 | - `:security` - 123 | Options for security related behaviors and headers. 124 | 125 | - `:anti-forgery` - 126 | Set to true to add CSRF protection via the [ring-anti-forgery][5] 127 | library, or supply a map of options to be passed to the middleware. 128 | 129 | - `:content-type-options` - 130 | Prevents attacks based around media-type confusion. See: 131 | [wrap-content-type-options][6]. 132 | 133 | - `:frame-options` - 134 | Prevents your site from being placed in frames or iframes. See: 135 | [wrap-frame-options][7]. 136 | 137 | - `:hsts` - 138 | If true, enable HTTP Strict Transport Security. See: [wrap-hsts][8]. 139 | 140 | - `:ssl-redirect` - 141 | If true, redirect all HTTP requests to the equivalent HTTPS URL. A 142 | map with an `:ssl-port` option may be set instead, if the HTTPS 143 | server is on a non-standard port. See: [wrap-ssl-redirect][9]. 144 | 145 | - `:xss-protection` - 146 | **Deprecated** Enable the X-XSS-Protection header. This is [no 147 | longer considered best practice][13] and should be avoided. 148 | See: [wrap-xss-protection][10]. 149 | 150 | - `:session` - 151 | A map of options for configuring session handling via the Ring 152 | [session][11] middleware. 153 | 154 | - `:flash` - If set to true, the Ring [flash][12] middleware is added. 155 | 156 | - `:store` - The Ring session store to use for storing sessions. 157 | 158 | - `:static` - 159 | A map of options to configure how to find static content. 160 | 161 | - `:files` - 162 | A string or a map of options to be passed to the [file][14] 163 | middleware, where the `:root` key is passed as the first argument, 164 | and the rest of the map is passed as options. May also be a 165 | collection of the above. Usually the `:resources` option below is 166 | more useful. 167 | 168 | - `:resources` - 169 | A string or a map of options to be passed to the [resource][15] 170 | middleware, where the `:root` key is passed as the first argument, 171 | and the rest of the map is passed as options. May also be a 172 | collection of the above. 173 | 174 | - `:websocket` 175 | A map of options to configure websocket behavior. 176 | 177 | - `:keepalive` - 178 | If true, periodically pings the client to keep the connection 179 | alive via the [websocket keepalive][16] middleware. A map of 180 | options may also be passed to set the `:period` of the keepalive in 181 | milliseconds. 182 | 183 | 184 | [1]: https://ring-clojure.github.io/ring/ring.middleware.multipart-params.html 185 | [2]: https://ring-clojure.github.io/ring/ring.middleware.nested-params.html 186 | [3]: https://ring-clojure.github.io/ring/ring.middleware.content-type.html 187 | [4]: https://ring-clojure.github.io/ring/ring.middleware.not-modified.html 188 | [5]: https://github.com/ring-clojure/ring-anti-forgery 189 | [6]: https://ring-clojure.github.io/ring-headers/ring.middleware.x-headers.html#var-wrap-content-type-options 190 | [7]: https://ring-clojure.github.io/ring-headers/ring.middleware.x-headers.html#var-wrap-frame-options 191 | [8]: https://ring-clojure.github.io/ring-ssl/ring.middleware.ssl.html#var-wrap-hsts 192 | [9]: https://ring-clojure.github.io/ring-ssl/ring.middleware.ssl.html#var-wrap-ssl-redirect 193 | [10]: https://ring-clojure.github.io/ring-headers/ring.middleware.x-headers.html#var-wrap-xss-protection 194 | [11]: https://ring-clojure.github.io/ring/ring.middleware.session.html 195 | [12]: https://ring-clojure.github.io/ring/ring.middleware.flash.html 196 | [13]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 197 | [14]: https://ring-clojure.github.io/ring/ring.middleware.file.html 198 | [15]: https://ring-clojure.github.io/ring/ring.middleware.resource.html 199 | [16]: https://ring-clojure.github.io/ring-websocket-middleware/ring.websocket.keepalive.html 200 | [17]: https://ring-clojure.github.io/ring/ring.middleware.content-length.html 201 | 202 | ## License 203 | 204 | Copyright © 2025 James Reeves 205 | 206 | Distributed under the MIT License, the same as Ring. 207 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"} 2 | ring/ring-core {:mvn/version "1.15.0"} 3 | ring/ring-ssl {:mvn/version "0.4.0"} 4 | ring/ring-headers {:mvn/version "0.4.0"} 5 | ring/ring-anti-forgery {:mvn/version "1.4.0"} 6 | org.ring-clojure/ring-websocket-middleware {:mvn/version "0.2.1"}}} 7 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject ring/ring-defaults "0.7.0" 2 | :description "Ring middleware that provides sensible defaults" 3 | :url "https://github.com/ring-clojure/ring-defaults" 4 | :license {:name "The MIT License" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.9.0"] 7 | [ring/ring-core "1.15.1"] 8 | [ring/ring-ssl "0.4.0"] 9 | [ring/ring-headers "0.4.0"] 10 | [ring/ring-anti-forgery "1.4.0"] 11 | [org.ring-clojure/ring-websocket-middleware "0.2.1"]] 12 | :aliases 13 | {"test-all" ["with-profile" "default:+1.10:+1.11:+1.12" "test"]} 14 | :profiles 15 | {:dev {:dependencies [[ring/ring-mock "0.6.2"]]} 16 | :1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]} 17 | :1.11 {:dependencies [[org.clojure/clojure "1.11.4"]]} 18 | :1.12 {:dependencies [[org.clojure/clojure "1.12.2"]]}}) 19 | -------------------------------------------------------------------------------- /src/ring/middleware/defaults.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.defaults 2 | "Middleware for providing a handler with sensible defaults." 3 | (:require [ring.middleware.x-headers :as x] 4 | [ring.middleware.flash :refer [wrap-flash]] 5 | [ring.middleware.session :refer [wrap-session]] 6 | [ring.middleware.session.cookie :refer [cookie-store]] 7 | [ring.middleware.keyword-params :refer [wrap-keyword-params]] 8 | [ring.middleware.nested-params :refer [wrap-nested-params]] 9 | [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] 10 | [ring.middleware.multipart-params :refer [wrap-multipart-params]] 11 | [ring.middleware.params :refer [wrap-params]] 12 | [ring.middleware.cookies :refer [wrap-cookies]] 13 | [ring.middleware.resource :refer [wrap-resource]] 14 | [ring.middleware.file :refer [wrap-file]] 15 | [ring.middleware.not-modified :refer [wrap-not-modified]] 16 | [ring.middleware.content-length :refer [wrap-content-length]] 17 | [ring.middleware.content-type :refer [wrap-content-type]] 18 | [ring.middleware.default-charset :refer [wrap-default-charset]] 19 | [ring.middleware.absolute-redirects :refer [wrap-absolute-redirects]] 20 | [ring.middleware.ssl :refer [wrap-ssl-redirect wrap-hsts wrap-forwarded-scheme]] 21 | [ring.middleware.proxy-headers :refer [wrap-forwarded-remote-addr]] 22 | [ring.websocket.keepalive :refer [wrap-websocket-keepalive]])) 23 | 24 | (def default-session-store (cookie-store)) 25 | 26 | (def api-defaults 27 | "A default configuration for a HTTP API." 28 | {:params {:urlencoded true 29 | :keywordize true} 30 | :responses {:not-modified-responses true 31 | :absolute-redirects false 32 | :content-length true 33 | :content-types true 34 | :default-charset "utf-8"}}) 35 | 36 | (def secure-api-defaults 37 | "A default configuration for a HTTP API that's accessed securely over HTTPS." 38 | (-> api-defaults 39 | (assoc-in [:security :ssl-redirect] true) 40 | (assoc-in [:security :hsts] true))) 41 | 42 | (def site-defaults 43 | "A default configuration for a browser-accessible website, based on current 44 | best practice." 45 | {:params {:urlencoded true 46 | :multipart true 47 | :nested true 48 | :keywordize true} 49 | :cookies true 50 | :session {:flash true 51 | :cookie-attrs {:http-only true} 52 | :store default-session-store} 53 | :security {:anti-forgery {:safe-header "X-Ring-Anti-Forgery"} 54 | :frame-options :sameorigin 55 | :content-type-options :nosniff} 56 | :static {:resources "public"} 57 | :responses {:not-modified-responses true 58 | :absolute-redirects false 59 | :content-length true 60 | :content-types true 61 | :default-charset "utf-8"} 62 | :websocket {:keepalive true}}) 63 | 64 | (def secure-site-defaults 65 | "A default configuration for a browser-accessible website that's accessed 66 | securely over HTTPS." 67 | (-> site-defaults 68 | (assoc-in [:session :cookie-attrs :secure] true) 69 | (assoc-in [:session :cookie-name] "secure-ring-session") 70 | (assoc-in [:security :ssl-redirect] true) 71 | (assoc-in [:security :hsts] true))) 72 | 73 | (defn- wrap [handler middleware options] 74 | (if (true? options) 75 | (middleware handler) 76 | (if options 77 | (middleware handler options) 78 | handler))) 79 | 80 | (defn- wrap-static [handler middleware args] 81 | (let [middleware' (fn [handler args] 82 | (if (map? args) 83 | (middleware handler (:root args) (dissoc args :root)) 84 | (middleware handler args)))] 85 | (wrap handler 86 | (fn [handler args] 87 | (if (and (coll? args) (not (map? args))) 88 | (reduce middleware' handler args) 89 | (middleware' handler args))) 90 | args))) 91 | 92 | (defn- wrap-xss-protection [handler options] 93 | (x/wrap-xss-protection handler (:enable? options true) (dissoc options :enable?))) 94 | 95 | (defn- wrap-x-headers [handler options] 96 | (-> handler 97 | (wrap wrap-xss-protection (:xss-protection options false)) 98 | (wrap x/wrap-frame-options (:frame-options options false)) 99 | (wrap x/wrap-content-type-options (:content-type-options options false)))) 100 | 101 | (defn wrap-defaults 102 | "Wraps a handler in default Ring middleware, as specified by the supplied 103 | configuration map. 104 | 105 | See: api-defaults 106 | site-defaults 107 | secure-api-defaults 108 | secure-site-defaults" 109 | [handler config] 110 | (-> handler 111 | (wrap wrap-websocket-keepalive (get-in config [:websocket :keepalive] false)) 112 | (wrap wrap-anti-forgery (get-in config [:security :anti-forgery] false)) 113 | (wrap wrap-flash (get-in config [:session :flash] false)) 114 | (wrap wrap-session (:session config false)) 115 | (wrap wrap-keyword-params (get-in config [:params :keywordize] false)) 116 | (wrap wrap-nested-params (get-in config [:params :nested] false)) 117 | (wrap wrap-multipart-params (get-in config [:params :multipart] false)) 118 | (wrap wrap-params (get-in config [:params :urlencoded] false)) 119 | (wrap wrap-cookies (get-in config [:cookies] false)) 120 | (wrap wrap-absolute-redirects (get-in config [:responses :absolute-redirects] false)) 121 | (wrap-static wrap-resource (get-in config [:static :resources] false)) 122 | (wrap-static wrap-file (get-in config [:static :files] false)) 123 | (wrap wrap-content-type (get-in config [:responses :content-types] false)) 124 | (wrap wrap-default-charset (get-in config [:responses :default-charset] false)) 125 | (wrap wrap-not-modified (get-in config [:responses :not-modified-responses] false)) 126 | (wrap wrap-content-length (get-in config [:responses :content-length] false)) 127 | (wrap wrap-x-headers (:security config)) 128 | (wrap wrap-hsts (get-in config [:security :hsts] false)) 129 | (wrap wrap-ssl-redirect (get-in config [:security :ssl-redirect] false)) 130 | (wrap wrap-forwarded-scheme (boolean (:proxy config))) 131 | (wrap wrap-forwarded-remote-addr (boolean (:proxy config))))) 132 | -------------------------------------------------------------------------------- /test/ring/assets/public1/foo.txt: -------------------------------------------------------------------------------- 1 | foo1 2 | -------------------------------------------------------------------------------- /test/ring/assets/public2/bar.txt: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /test/ring/assets/public2/foo.txt: -------------------------------------------------------------------------------- 1 | foo2 2 | -------------------------------------------------------------------------------- /test/ring/middleware/defaults_test.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.defaults-test 2 | (:require [clojure.test :refer :all] 3 | [ring.middleware.defaults :refer :all] 4 | [ring.util.response :refer [response content-type not-found]] 5 | [ring.mock.request :refer [request header]] 6 | [ring.websocket :as ws] 7 | [ring.websocket.protocols :as wsp])) 8 | 9 | (deftest test-wrap-defaults 10 | (testing "api defaults" 11 | (let [handler (-> (constantly (response "foo")) 12 | (wrap-defaults api-defaults)) 13 | resp (handler (request :get "/"))] 14 | (is (= resp {:status 200 15 | :headers {"Content-Type" "application/octet-stream"} 16 | :body "foo"})))) 17 | 18 | (testing "site defaults" 19 | (let [handler (-> (constantly (response "foo")) 20 | (wrap-defaults site-defaults)) 21 | resp (handler (request :get "/"))] 22 | (is (= (:status resp) 200)) 23 | (is (= (:body resp) "foo")) 24 | (is (= (set (keys (:headers resp))) 25 | #{"X-Frame-Options" 26 | "X-Content-Type-Options" 27 | "Content-Type" 28 | "Set-Cookie"})) 29 | (is (= (get-in resp [:headers "X-Frame-Options"]) "SAMEORIGIN")) 30 | (is (= (get-in resp [:headers "X-Content-Type-Options"]) "nosniff")) 31 | (is (= (get-in resp [:headers "Content-Type"]) "application/octet-stream")) 32 | (let [set-cookie (first (get-in resp [:headers "Set-Cookie"]))] 33 | (is (.startsWith set-cookie "ring-session=")) 34 | (is (.contains set-cookie "HttpOnly"))))) 35 | 36 | (testing "cookie round trip" 37 | (let [handler (-> (fn [{:keys [session]}] 38 | (-> (response (str (:x session 1))) 39 | (assoc :session (update session :x (fnil inc 1))))) 40 | (wrap-defaults site-defaults)) 41 | resp1 (handler (request :get "/"))] 42 | (is (= "1" (:body resp1))) 43 | (let [cookie (->> (get-in resp1 [:headers "Set-Cookie"]) 44 | (first) 45 | (re-find #"^ring-session=.*?;")) 46 | resp2 (handler (-> (request :get "/") 47 | (header "Cookie" cookie)))] 48 | (is (= "2" (:body resp2)))))) 49 | 50 | (testing "default charset" 51 | (let [handler (-> (constantly (-> (response "foo") (content-type "text/plain"))) 52 | (wrap-defaults site-defaults)) 53 | resp (handler (request :get "/"))] 54 | (is (= (get-in resp [:headers "Content-Type"]) "text/plain; charset=utf-8")))) 55 | 56 | (testing "middleware overrides" 57 | (let [handler (-> (constantly (response "foo")) 58 | (wrap-defaults 59 | (assoc-in site-defaults [:security :frame-options] :deny))) 60 | resp (handler (request :get "/"))] 61 | (is (= (get-in resp [:headers "X-Frame-Options"]) "DENY")) 62 | (is (= (get-in resp [:headers "X-Content-Type-Options"]) "nosniff")))) 63 | 64 | (testing "disabled middleware" 65 | (let [handler (-> (constantly (response "foo")) 66 | (wrap-defaults 67 | (assoc-in site-defaults [:security :frame-options] false))) 68 | resp (handler (request :get "/"))] 69 | (is (nil? (get-in resp [:headers "X-Frame-Options"]))) 70 | (is (= (get-in resp [:headers "X-Content-Type-Options"]) "nosniff")))) 71 | 72 | (testing "ssl redirect (site)" 73 | (let [handler (-> (constantly (response "foo")) 74 | (wrap-defaults secure-site-defaults)) 75 | resp (handler (request :get "/foo"))] 76 | (is (= resp {:status 301 77 | :headers {"Location" "https://localhost/foo"} 78 | :body ""})))) 79 | 80 | (testing "ssl redirect (api)" 81 | (let [handler (-> (constantly (response "foo")) 82 | (wrap-defaults secure-api-defaults)) 83 | resp (handler (request :get "/foo"))] 84 | (is (= resp {:status 301 85 | :headers {"Location" "https://localhost/foo"} 86 | :body ""})))) 87 | 88 | (testing "ssl proxy redirect" 89 | (let [handler (-> (constantly (response "foo")) 90 | (wrap-defaults (assoc secure-site-defaults :proxy true))) 91 | resp (handler (-> (request :get "/foo") 92 | (header "x-forwarded-proto" "https")))] 93 | (is (= (:status resp) 200)) 94 | (is (= (:body resp) "foo")))) 95 | 96 | (testing "secure api defaults" 97 | (let [handler (-> (constantly (response "foo")) 98 | (wrap-defaults secure-api-defaults)) 99 | resp (handler (request :get "https://localhost/foo"))] 100 | (is (= resp {:status 200 101 | :headers {"Content-Type" "application/octet-stream" 102 | "Strict-Transport-Security" 103 | "max-age=31536000; includeSubDomains"} 104 | :body "foo"})))) 105 | 106 | (testing "secure site defaults" 107 | (let [handler (-> (constantly (response "foo")) 108 | (wrap-defaults secure-site-defaults)) 109 | resp (handler (request :get "https://localhost/"))] 110 | (is (= (:status resp) 200)) 111 | (is (= (:body resp) "foo")) 112 | (is (= (set (keys (:headers resp))) 113 | #{"X-Frame-Options" 114 | "X-Content-Type-Options" 115 | "Strict-Transport-Security" 116 | "Content-Type" 117 | "Set-Cookie"})) 118 | (is (= (get-in resp [:headers "X-Frame-Options"]) "SAMEORIGIN")) 119 | (is (= (get-in resp [:headers "X-Content-Type-Options"]) "nosniff")) 120 | (is (= (get-in resp [:headers "Strict-Transport-Security"]) 121 | "max-age=31536000; includeSubDomains")) 122 | (is (= (get-in resp [:headers "Content-Type"]) "application/octet-stream")) 123 | (let [set-cookie (first (get-in resp [:headers "Set-Cookie"]))] 124 | (is (.startsWith set-cookie "secure-ring-session=")) 125 | (is (.contains set-cookie "HttpOnly")) 126 | (is (.contains set-cookie "Secure"))))) 127 | 128 | (testing "proxy headers" 129 | (let [handler (wrap-defaults response {:proxy true}) 130 | resp (handler (-> (request :get "/") 131 | (header "x-forwarded-proto" "https") 132 | (header "x-forwarded-for" "10.0.0.1, 1.2.3.4"))) 133 | body (:body resp)] 134 | (is (= (:scheme body) :https)) 135 | (is (= (:remote-addr body) "1.2.3.4")))) 136 | 137 | (testing "nil response" 138 | (let [handler (wrap-defaults (constantly nil) site-defaults)] 139 | (is (nil? (handler (request :get "/")))))) 140 | 141 | (testing "single resource path" 142 | (let [handler (wrap-defaults 143 | (constantly nil) 144 | (assoc-in site-defaults [:static :resources] "ring/assets/public1"))] 145 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo1\n")) 146 | (is (nil? (handler (request :get "/bar.txt")))))) 147 | 148 | (testing "multiple resource paths" 149 | (let [handler (wrap-defaults 150 | (constantly nil) 151 | (assoc-in site-defaults 152 | [:static :resources] 153 | ["ring/assets/public1" 154 | "ring/assets/public2"]))] 155 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo2\n")) 156 | (is (= (slurp (:body (handler (request :get "/bar.txt")))) "bar\n")))) 157 | 158 | (testing "resource paths with options" 159 | (let [handler (wrap-defaults 160 | (fn [{:keys [uri]}] 161 | (if (= uri "/foo.txt") 162 | (response "foo-custom") 163 | (not-found ""))) 164 | (assoc-in site-defaults [:static :resources] 165 | {:root "ring/assets/public2" 166 | :prefer-handler? true}))] 167 | (is (= (:body (handler (request :get "/foo.txt"))) "foo-custom")) 168 | (is (= (slurp (:body (handler (request :get "/bar.txt")))) "bar\n")))) 169 | 170 | (testing "multiple resource paths with options" 171 | (let [handler (wrap-defaults 172 | (fn [{:keys [uri]}] 173 | (if (= uri "/bar.txt") 174 | (response "bar-custom") 175 | (not-found ""))) 176 | (assoc-in site-defaults [:static :resources] 177 | [{:root "ring/assets/public2" 178 | :prefer-handler? true} 179 | {:root "ring/assets/public1" 180 | :prefer-handler? true}]))] 181 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo2\n")) 182 | (is (= (:body (handler (request :get "/bar.txt"))) "bar-custom")))) 183 | 184 | (testing "single file path" 185 | (let [handler (wrap-defaults 186 | (constantly nil) 187 | (assoc-in site-defaults [:static :files] "test/ring/assets/public1"))] 188 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo1\n")) 189 | (is (nil? (handler (request :get "/bar.txt")))))) 190 | 191 | (testing "multiple file paths" 192 | (let [handler (wrap-defaults 193 | (constantly nil) 194 | (assoc-in site-defaults 195 | [:static :files] 196 | ["test/ring/assets/public1" 197 | "test/ring/assets/public2"]))] 198 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo2\n")) 199 | (is (= (slurp (:body (handler (request :get "/bar.txt")))) "bar\n")))) 200 | 201 | (testing "file paths with options" 202 | (let [handler (wrap-defaults 203 | (fn [{:keys [uri]}] 204 | (if (= uri "/foo.txt") 205 | (response "foo-custom") 206 | (not-found ""))) 207 | (assoc-in site-defaults [:static :files] 208 | {:root "test/ring/assets/public2" 209 | :prefer-handler? true}))] 210 | (is (= (:body (handler (request :get "/foo.txt"))) "foo-custom")) 211 | (is (= (slurp (:body (handler (request :get "/bar.txt")))) "bar\n")))) 212 | 213 | (testing "multiple file paths with options" 214 | (let [handler (wrap-defaults 215 | (fn [{:keys [uri]}] 216 | (if (= uri "/bar.txt") 217 | (response "bar-custom") 218 | (not-found ""))) 219 | (assoc-in site-defaults [:static :files] 220 | [{:root "test/ring/assets/public2" 221 | :prefer-handler? true} 222 | {:root "test/ring/assets/public1" 223 | :prefer-handler? true}]))] 224 | (is (= (slurp (:body (handler (request :get "/foo.txt")))) "foo2\n")) 225 | (is (= (:body (handler (request :get "/bar.txt"))) "bar-custom")))) 226 | 227 | (testing "async handlers" 228 | (let [handler (-> (fn [_ respond _] (respond (response "foo"))) 229 | (wrap-defaults api-defaults)) 230 | resp (promise) 231 | ex (promise)] 232 | (handler (request :get "/") resp ex) 233 | (is (not (realized? ex))) 234 | (is (= @resp {:status 200 235 | :headers {"Content-Type" "application/octet-stream"} 236 | :body "foo"})))) 237 | 238 | (testing "XSS protection enabled" 239 | (let [handler (-> (constantly (response "foo")) 240 | (wrap-defaults 241 | (-> site-defaults 242 | (assoc-in [:security :xss-protection :enable?] true) 243 | (assoc-in [:security :xss-protection :mode] :block)))) 244 | resp (handler (request :get "/"))] 245 | (is (not (nil? (get-in resp [:headers "X-XSS-Protection"])))) 246 | (is (= (get-in resp [:headers "X-XSS-Protection"]) "1; mode=block")))) 247 | 248 | (testing "anti-forgery" 249 | (let [handler (-> (constantly (response "foo")) 250 | (wrap-defaults site-defaults))] 251 | (is (= 403 (:status (handler (request :post "/"))))) 252 | (is (= 200 (:status (handler (-> (request :post "/") 253 | (header "X-Ring-Anti-Forgery" "1")))))))) 254 | 255 | (testing "websocket pings" 256 | (let [ping-count (atom 0) 257 | socket (reify wsp/Socket 258 | (-open? [_] true) 259 | (-send [_ _]) 260 | (-ping [_ _] (swap! ping-count inc)) 261 | (-pong [_ _]) 262 | (-close [_ _ _])) 263 | response {::ws/listener {}} 264 | handler (wrap-defaults (constantly response) 265 | {:websocket {:keepalive {:period 10}}}) 266 | listener (::ws/listener (handler {}))] 267 | (wsp/on-open listener socket) 268 | (Thread/sleep 41) 269 | (wsp/on-close listener socket 1000 "") 270 | (Thread/sleep 20) 271 | (is (= 4 @ping-count)))) 272 | 273 | (testing "content-length" 274 | (let [handler (-> (constantly 275 | (-> (response "foobar") 276 | (content-type "text/plain; charset=UTF-8"))) 277 | (wrap-defaults api-defaults)) 278 | resp (handler (request :get "/"))] 279 | (is (= resp {:status 200 280 | :headers {"Content-Type" "text/plain; charset=UTF-8" 281 | "Content-Length" "6"} 282 | :body "foobar"}))) 283 | (let [handler (-> (constantly 284 | (-> (response "foobar") 285 | (content-type "text/plain; charset=UTF-8"))) 286 | (wrap-defaults site-defaults)) 287 | resp (handler (request :get "/"))] 288 | (is (= "6" (get-in resp [:headers "Content-Length"])))))) 289 | --------------------------------------------------------------------------------