├── .gitignore ├── deps.edn ├── LICENSE ├── README.md └── src └── reitit └── ring └── middleware └── defaults.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .calva/ 3 | .cpcache/ 4 | .lsp/ 5 | .portal/ 6 | 7 | target/ 8 | 9 | .nrepl-port 10 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {metosin/reitit-middleware {:mvn/version "0.5.18"} 3 | ring/ring-defaults {:mvn/version "0.3.4"}} 4 | :aliases 5 | {:build 6 | {:replace-deps {io.github.seancorfield/build-clj {:git/tag "v0.8.3" :git/sha "7ac1f8d"}} 7 | :ns-default build}}} 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ferdinand Beyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reitit-ring-defaults 2 | 3 | [![cljdoc](https://cljdoc.org/badge/com.fbeyer/reitit-ring-defaults)][cljdoc] 4 | [![Clojars](https://img.shields.io/clojars/v/com.fbeyer/reitit-ring-defaults.svg)][clojars] 5 | 6 | Lifts [ring-defaults] middleware into data-driven middleware, providing 7 | sensible defaults per route. 8 | 9 | ## Rationale 10 | 11 | Reitit provides advanced middleware capabilities and comes with a small number 12 | of "standard" middleware. For many common tasks such as session management, 13 | additional middleware is required. 14 | 15 | Many people use `wrap-defaults` from [ring-defaults] as a sensible default 16 | chain. However, some middleware included in `site-defaults` will duplicate 17 | or conflict with an idiomatic Reitit setup: 18 | 19 | - Static resources with `create-resource-handler` and `create-file-handler` 20 | will have no effect when ring-defaults installs a `wrap-resource` 21 | and `wrap-file` middleware 22 | - Duplicate parameter and multi-part handling 23 | 24 | The result is sub-optimal performance and developer's confusion when requests 25 | are not handled as expected. 26 | 27 | Furthermore, it is not obvious at which place in Reitit's configuration 28 | `wrap-defaults` should go: on the `ring-handler`, in global router data, 29 | per route? 30 | 31 | ## Installation 32 | 33 | deps.edn: 34 | 35 | ``` 36 | com.fbeyer/reitit-ring-defaults {:mvn/version "0.1.0"} 37 | ``` 38 | 39 | Leiningen/Boot: 40 | 41 | ``` 42 | [com.fbeyer/reitit-ring-defaults "0.1.0"] 43 | ``` 44 | 45 | ## Basic usage 46 | 47 | The `ring-defaults-middleware` is designed as a replacement for `wrap-defaults`. 48 | In order to have any effect, you will need to add a `:defaults` key to your 49 | route data. A good starting point is one of the curated configurations 50 | provided by ring-defaults, such as `api-defaults`: 51 | 52 | ```clojure 53 | (require '[reitit.ring :as ring] 54 | '[reitit.ring.middleware.defaults :refer [ring-defaults-middleware]] 55 | '[ring.middleware.defaults :refer [api-defaults]]) 56 | 57 | ;; ... 58 | 59 | (def app 60 | (ring/ring-handler 61 | (ring/router 62 | ["/api" 63 | {:middleware ring-defaults-middleware 64 | :defaults api-defaults} 65 | ["/ping" handler] 66 | ["/pong" handler]] 67 | routes))) 68 | ``` 69 | 70 | You can treat the `:defaults` data like any other route data: You can specify 71 | it globally or per-route, and route configurations will be merged with parent 72 | configurations. 73 | 74 | Any middleware not configured per route will not mount, and have zero runtime 75 | impact. 76 | 77 | ## Extended defaults 78 | 79 | Reitit provides excellent support for [content negotiation][reitit-format] 80 | and [coercion][reitit-coercion], 81 | as well as [exception handling][reitit-exception]. 82 | 83 | Since it is very common to use these, there is a `defaults-middleware` 84 | that includes additional Reitit middleware as a sensible default. 85 | 86 | ```clojure 87 | (require '[muuntaja.core :as muuntaja] 88 | '[reitit.coercion.malli] 89 | '[reitit.ring.middleware.defaults :refer [defaults-middleware]]) 90 | 91 | (def app 92 | (ring/ring-handler 93 | (ring/router 94 | ["/api" 95 | {:middleware defaults-middleware 96 | :defaults (-> api-defaults 97 | ;; Enable exception middleware. You can also add custom 98 | ;; handlers in the [:exception handlers] key and they 99 | ;; will be passed to create-exception-middleware. 100 | (assoc :exception true)) 101 | ;; Muuntaja instance for content negotiation 102 | :muuntaja muuntaja/instance 103 | ;; Request and response coercion -- using Malli in this case. 104 | :coercion reitit.coercion.malli/coercion} 105 | ["/ping" handler] 106 | ["/pong" handler]] 107 | routes))) 108 | ``` 109 | 110 | ## Warning on Session Middleware 111 | 112 | Like `wrap-defaults`, reitit-defaults-middleware includes Ring's `wrap-session`, 113 | and using its default configuration in Reitit route data will have surprising 114 | effects, as [Reitit will mount one instance per route][reitit-session-issue]. 115 | 116 | The recommended solution with reitit-defaults-middleware is to explicitly 117 | configure a session store. Since the default in-memory store will not be 118 | suitable for non-trivial production deployments, you will want to do that anyway. 119 | 120 | ```clojure 121 | (require '[ring.middleware.session.memory :as memory]) 122 | 123 | ;; single instance 124 | (def session-store (memory/memory-store)) 125 | 126 | ;; inside, with shared store 127 | (def app 128 | (ring/ring-handler 129 | (ring/router 130 | ["/api" 131 | {:middleware ring-defaults-middleware 132 | :defaults (-> site-defaults 133 | (assoc-in [:session :store] session-store))} 134 | ["/ping" handler] 135 | ["/pong" handler]]))) 136 | ``` 137 | 138 | ## License 139 | 140 | Copyright 2022 Ferdinand Beyer. 141 | Distributed under the [MIT License](LICENSE). 142 | 143 | [cljdoc]: https://cljdoc.org/jump/release/com.fbeyer/reitit-ring-defaults 144 | [clojars]: https://clojars.org/com.fbeyer/reitit-ring-defaults 145 | [reitit]: https://github.com/metosin/reitit 146 | [ring-defaults]: https://github.com/ring-clojure/ring-defaults 147 | [reitit-coercion]: https://cljdoc.org/d/metosin/reitit/CURRENT/doc/ring/pluggable-coercion 148 | [reitit-exception]: https://cljdoc.org/d/metosin/reitit/CURRENT/doc/ring/exception-handling-with-ring 149 | [reitit-format]: https://cljdoc.org/d/metosin/reitit/CURRENT/doc/ring/content-negotiation 150 | [reitit-session-issue]: https://github.com/metosin/reitit/issues/205 151 | -------------------------------------------------------------------------------- /src/reitit/ring/middleware/defaults.clj: -------------------------------------------------------------------------------- 1 | (ns reitit.ring.middleware.defaults 2 | (:refer-clojure :exclude [compile]) 3 | (:require [reitit.ring.coercion :as coercion] 4 | [reitit.ring.middleware.exception :as exception] 5 | [reitit.ring.middleware.multipart :as multipart] 6 | [reitit.ring.middleware.muuntaja :as format] 7 | [reitit.ring.middleware.parameters :as parameters] 8 | [ring.middleware.absolute-redirects :refer [wrap-absolute-redirects]] 9 | [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] 10 | [ring.middleware.content-type :refer [wrap-content-type]] 11 | [ring.middleware.cookies :refer [wrap-cookies]] 12 | [ring.middleware.default-charset :refer [wrap-default-charset]] 13 | [ring.middleware.flash :refer [wrap-flash]] 14 | [ring.middleware.keyword-params :refer [wrap-keyword-params]] 15 | [ring.middleware.nested-params :refer [wrap-nested-params]] 16 | [ring.middleware.not-modified :refer [wrap-not-modified]] 17 | [ring.middleware.proxy-headers :refer [wrap-forwarded-remote-addr]] 18 | [ring.middleware.session :refer [wrap-session]] 19 | [ring.middleware.ssl :refer [wrap-forwarded-scheme wrap-hsts 20 | wrap-ssl-redirect]] 21 | [ring.middleware.x-headers :refer [wrap-content-type-options 22 | wrap-frame-options 23 | wrap-xss-protection]])) 24 | 25 | (defn- compile 26 | "Returns a middleware compilation function for `wrap-fn`, only mounting 27 | the middleware when the `:defaults` route data is configured for this 28 | middleware. `opts-fn` takes the defaults configuration map for this route, 29 | and shall return options to pass as a second argument to `wrap-fn`. 30 | A return value of `true` means that the middleware takes no options, 31 | and falsy options will not mount the middleware." 32 | [wrap-fn opts-fn] 33 | (fn [data _route-opts] 34 | (when-let [opts (opts-fn (:defaults data))] 35 | (if (true? opts) 36 | wrap-fn 37 | #(wrap-fn % opts))))) 38 | 39 | (def forwarded-remote-addr-middleware 40 | {:name ::forwarded-remote-addr 41 | :compile (compile wrap-forwarded-remote-addr (comp boolean :proxy))}) 42 | 43 | (def forwarded-scheme-middleware 44 | {:name ::forwarded-scheme 45 | :compile (compile wrap-forwarded-scheme (comp boolean :proxy))}) 46 | 47 | (def ssl-redirect-middleware 48 | {:name ::ssl-redirect 49 | :compile (compile wrap-ssl-redirect #(get-in % [:security :ssl-redirect] false))}) 50 | 51 | (def hsts-middleware 52 | {:name ::hsts 53 | :compile (compile wrap-hsts #(get-in % [:security :hsts] false))}) 54 | 55 | (def content-type-options-middleware 56 | {:name ::content-type-options 57 | :compile (compile wrap-content-type-options #(get-in % [:security :content-type-options] false))}) 58 | 59 | (def frame-options-middleware 60 | {:name ::frame-options 61 | :compile (compile wrap-frame-options #(get-in % [:security :frame-options] false))}) 62 | 63 | (def xss-protection-middleware 64 | {:name ::xss-protection 65 | :compile (fn [data _] 66 | (let [opts (get-in data [:defaults :xss-protection])] 67 | (when (and opts (:enable? opts true)) 68 | (let [opts (dissoc opts :enable?)] 69 | #(wrap-xss-protection % true opts)))))}) 70 | 71 | (def not-modified-middleware 72 | {:name ::not-modified 73 | :compile (compile wrap-not-modified #(get-in % [:responses :not-modified-responses] false))}) 74 | 75 | (def default-charset-middleware 76 | {:name ::default-charset 77 | :compile (compile wrap-default-charset #(get-in % [:responses :default-charset] false))}) 78 | 79 | (def content-type-middleware 80 | {:name ::content-type 81 | :compile (compile wrap-content-type #(get-in % [:responses :content-types] false))}) 82 | 83 | (def absolute-redirects-middleware 84 | {:name ::absolute-redirects 85 | :compile (compile wrap-absolute-redirects #(get-in % [:responses :absolute-redirects] false))}) 86 | 87 | (def cookies-middleware 88 | {:name ::cookies 89 | :compile (compile wrap-cookies #(get-in % [:cookies] false))}) 90 | 91 | (def params-middleware 92 | {:name ::params 93 | :compile (fn [data _] 94 | (when (get-in data [:defaults :params :urlencoded]) 95 | parameters/parameters-middleware))}) 96 | 97 | (def multipart-middleware 98 | {:name ::multipart 99 | :compile (fn [data _] 100 | (when-let [opts (get-in data [:defaults :params :multipart])] 101 | (if (true? opts) 102 | multipart/multipart-middleware 103 | (multipart/create-multipart-middleware opts))))}) 104 | 105 | (def nested-params-middleware 106 | {:name ::nested-params 107 | :compile (compile wrap-nested-params #(get-in % [:params :nested] false))}) 108 | 109 | (def keyword-params-middleware 110 | {:name ::keyword-params 111 | :compile (compile wrap-keyword-params #(get-in % [:params :keywordize] false))}) 112 | 113 | (def session-middleware 114 | {:name ::session 115 | :compile (compile wrap-session #(:session % false))}) 116 | 117 | (def flash-middleware 118 | {:name ::flash 119 | :compile (compile wrap-flash #(get-in % [:session :flash] false))}) 120 | 121 | (def anti-forgery-middleware 122 | {:name ::anti-forgery 123 | :compile (compile wrap-anti-forgery #(get-in % [:security :anti-forgery] false))}) 124 | 125 | (def exception-middleware 126 | {:name ::exception 127 | :compile (fn [data _] 128 | (when-let [opts (get-in data [:defaults :exception])] 129 | (if-let [handlers (:handlers opts)] 130 | (exception/create-exception-middleware handlers) 131 | exception/exception-middleware)))}) 132 | 133 | (def ring-defaults-middleware 134 | "Applies the same middleware as `ring.middleware.defaults/wrap-defaults`, 135 | but as Reitit data-driven middleware with per-route compilation. 136 | 137 | To configure middleware per route, add `:defaults` route data, maybe using 138 | one of the curated configurations from `ring.middleware.defaults`." 139 | [forwarded-remote-addr-middleware 140 | forwarded-scheme-middleware 141 | 142 | ssl-redirect-middleware 143 | hsts-middleware 144 | 145 | content-type-options-middleware 146 | frame-options-middleware 147 | xss-protection-middleware 148 | 149 | not-modified-middleware 150 | default-charset-middleware 151 | content-type-middleware 152 | 153 | ;; We don't add `wrap-file` or `wrap-resource` as this is better done with 154 | ;; Reitit's `create-resource-handler` and `create-file-handler`. 155 | 156 | absolute-redirects-middleware 157 | cookies-middleware 158 | 159 | params-middleware 160 | multipart-middleware 161 | nested-params-middleware 162 | keyword-params-middleware 163 | 164 | session-middleware 165 | flash-middleware 166 | 167 | anti-forgery-middleware]) 168 | 169 | (def defaults-middleware 170 | "All of `ring-defaults-middleware`, plus Reitit's content negotiation 171 | (using Muuntaja), exception and coercion middleware." 172 | (conj 173 | ring-defaults-middleware 174 | 175 | format/format-negotiate-middleware 176 | format/format-response-middleware 177 | 178 | exception-middleware 179 | 180 | format/format-request-middleware 181 | 182 | coercion/coerce-exceptions-middleware 183 | coercion/coerce-request-middleware 184 | coercion/coerce-response-middleware)) 185 | --------------------------------------------------------------------------------