From 7b8ea958571b402e534be9b3dd19e5b77017a615 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Thu, 11 Jun 2026 15:21:37 +1000 Subject: [PATCH 01/25] include jetty-openid dependency --- project.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/project.clj b/project.clj index 73024767..aa4b3bb6 100644 --- a/project.clj +++ b/project.clj @@ -34,6 +34,7 @@ [org.eclipse.jetty/jetty-server "12.1.10"] [org.eclipse.jetty/jetty-session "12.1.10"] [org.eclipse.jetty/jetty-security "12.1.10"] + [org.eclipse.jetty/jetty-openid "12.1.10"] [org.eclipse.jetty.compression/jetty-compression-server "12.1.10"] [org.eclipse.jetty.compression/jetty-compression-gzip "12.1.10"]] From 7541551d6d18aa48f1ba2fbb238c7f6aaef9c2f8 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Thu, 11 Jun 2026 15:45:16 +1000 Subject: [PATCH 02/25] make slipway.websockets/path-spec correctly configurable --- src/slipway/websockets.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slipway/websockets.clj b/src/slipway/websockets.clj index 957742b1..12f87c24 100644 --- a/src/slipway/websockets.clj +++ b/src/slipway/websockets.clj @@ -72,4 +72,4 @@ (some->> max-frame-bytes (.setMaxFrameSize container)) (some->> max-outgoing-frames (.setMaxOutgoingFrames container)) (some->> auto-fragment (.setAutoFragment container)) - (.addMapping container "/chsk" (reify-ws-creator ring-handler))))))) \ No newline at end of file + (.addMapping container path-spec (reify-ws-creator ring-handler))))))) \ No newline at end of file From 3c0fd31b6dd863a7f0d54a7ac5d671be9b268ba5 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Fri, 12 Jun 2026 14:20:16 +1000 Subject: [PATCH 03/25] move construction of login-handler and server-handler to within handler ns --- src/slipway.clj | 9 +++------ src/slipway/handler.clj | 3 ++- src/slipway/server.clj | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/slipway.clj b/src/slipway.clj index 73ef0320..6d4fc3cd 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -3,11 +3,11 @@ [slipway.connector.http] [slipway.connector.https] [slipway.handler] - [slipway.security :as security] + [slipway.security] [slipway.server :as server] [slipway.user] [slipway.websockets]) - (:import (org.eclipse.jetty.server Handler Server))) + (:import (org.eclipse.jetty.server Server))) (comment #:slipway.handler.compression{:enabled? "is compression handler enabled? default true" @@ -106,10 +106,7 @@ (defn start ^Server [ring-handler {::keys [join?] :as opts}] (log/debugf "starting jetty server %s" opts) - (let [server (server/create-server opts) - login-service (security/login-service opts) - handler (server/handler server ring-handler login-service opts)] - (.setHandler server ^Handler handler) + (let [server (server/create-server ring-handler opts)] (.start server) (when join? (log/debug "joining jetty thread") diff --git a/src/slipway/handler.clj b/src/slipway/handler.clj index dfd4e3c0..d83993a8 100644 --- a/src/slipway/handler.clj +++ b/src/slipway/handler.clj @@ -15,7 +15,7 @@ :null-path-info? "true if /path is not redirected to /path/, default true"}) (defmethod server/handler :default - [server ring-handler login-service {::keys [context-path null-path-info?] :or {context-path "/"} :as opts}] + [server ring-handler {::keys [context-path null-path-info?] :or {context-path "/"} :as opts}] (log/debugf "creating default server handler, context path %s, null-path-info? %s" context-path null-path-info?) (let [context-handler (doto (ContextHandler.) (.setContextPath context-path) @@ -23,6 +23,7 @@ app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) (SyncHandler. ring-handler nil)) + login-service (security/login-service opts) auth-handler (if login-service (let [security-handler (security/handler login-service opts) session-handler (session/handler opts)] diff --git a/src/slipway/server.clj b/src/slipway/server.clj index 3b26d5bb..5e036b27 100644 --- a/src/slipway/server.clj +++ b/src/slipway/server.clj @@ -1,10 +1,10 @@ (ns slipway.server (:require [clojure.tools.logging :as log]) (:import (org.eclipse.jetty.io ByteBufferPool) - (org.eclipse.jetty.server Connector Server) + (org.eclipse.jetty.server Connector Handler Server) (org.eclipse.jetty.util.thread Scheduler ThreadPool))) -(defmulti handler (fn [_server _ring_handler _login_service opts] (::handler opts))) +(defmulti handler (fn [_server _ring_handler opts] (::handler opts))) (defmulti connector (fn [_server opts] (keyword (namespace (first (keys opts))) "connector"))) @@ -17,11 +17,11 @@ :error-handler "the error-handler used by this server for Jetty level errors"}) (defn create-server ^Server - [{::keys [connectors thread-pool scheduler buffer-pool error-handler] :as opts}] + [ring-handler {::keys [connectors thread-pool scheduler buffer-pool error-handler] :as opts}] {:pre [connectors]} (log/debugf "creating server %s" opts) (let [server (Server. ^ThreadPool thread-pool ^Scheduler scheduler ^ByteBufferPool buffer-pool)] (.setConnectors server (into-array Connector (map #(connector server %) connectors))) - (when error-handler - (.setErrorHandler server error-handler)) + (some->> error-handler (.setErrorHandler server)) + (.setHandler server ^Handler (handler server ring-handler opts)) server)) \ No newline at end of file From 084731c4f9d55e0a1efc5c093f53420a4aeca1c7 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Fri, 12 Jun 2026 15:20:41 +1000 Subject: [PATCH 04/25] minor refactor --- src/slipway/handler.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/slipway/handler.clj b/src/slipway/handler.clj index d83993a8..e044c0dc 100644 --- a/src/slipway/handler.clj +++ b/src/slipway/handler.clj @@ -23,8 +23,7 @@ app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) (SyncHandler. ring-handler nil)) - login-service (security/login-service opts) - auth-handler (if login-service + auth-handler (if-let [login-service (security/login-service opts)] (let [security-handler (security/handler login-service opts) session-handler (session/handler opts)] (.addBean server login-service) From 6af8aa410cad50f6d02189920037e4821ade6ef6 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Fri, 12 Jun 2026 16:31:52 +1000 Subject: [PATCH 05/25] moving towards a generalized security-handler (vs login-handler) approach --- src/slipway/handler.clj | 19 +++++++++---------- src/slipway/security.clj | 23 ++++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/slipway/handler.clj b/src/slipway/handler.clj index e044c0dc..e0ebe083 100644 --- a/src/slipway/handler.clj +++ b/src/slipway/handler.clj @@ -5,7 +5,7 @@ [slipway.server :as server] [slipway.session :as session] [slipway.websockets :as websockets]) - (:import (org.eclipse.jetty.server Handler) + (:import (org.eclipse.jetty.server Handler Server) (org.eclipse.jetty.server.handler ContextHandler) (slipway.handler SyncHandler))) @@ -15,7 +15,9 @@ :null-path-info? "true if /path is not redirected to /path/, default true"}) (defmethod server/handler :default - [server ring-handler {::keys [context-path null-path-info?] :or {context-path "/"} :as opts}] + [^Server server ring-handler {::keys [context-path null-path-info?] + :or {context-path "/"} + :as opts}] (log/debugf "creating default server handler, context path %s, null-path-info? %s" context-path null-path-info?) (let [context-handler (doto (ContextHandler.) (.setContextPath context-path) @@ -23,16 +25,13 @@ app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) (SyncHandler. ring-handler nil)) - auth-handler (if-let [login-service (security/login-service opts)] - (let [security-handler (security/handler login-service opts) - session-handler (session/handler opts)] - (.addBean server login-service) + auth-handler (when-let [security-handler (security/handler opts)] + (let [session-handler (session/handler opts)] (.setHandler security-handler ^Handler app-handler) (.setHandler session-handler security-handler) - session-handler) - app-handler) + session-handler)) handler (if-let [compression-handler (compression/handler opts)] - (doto compression-handler (.setHandler auth-handler)) - auth-handler)] + (doto compression-handler (.setHandler (or auth-handler app-handler))) + (or auth-handler app-handler))] (.setHandler context-handler ^Handler handler) context-handler)) \ No newline at end of file diff --git a/src/slipway/security.clj b/src/slipway/security.clj index cdaad19b..f90c612b 100644 --- a/src/slipway/security.clj +++ b/src/slipway/security.clj @@ -44,15 +44,16 @@ :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]"}) (defn handler ^SecurityHandler - [^LoginService login-service {::keys [realm authenticator constraint-mappings identity-service]}] + [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) - (let [security-handler (doto (SecurityHandler$PathMapped.) - (.setAuthenticator ^Authenticator authenticator) - (.setLoginService login-service) - (.setRealmName realm))] - (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] - (.put security-handler path-spec constraint)) - (when identity-service - (log/debugf "identity service %s" (type identity-service)) - (.setIdentityService security-handler identity-service)) - security-handler)) \ No newline at end of file + (when-let [^LoginService login-service (login-service opts)] + (let [security-handler (doto (SecurityHandler$PathMapped.) + (.setAuthenticator ^Authenticator authenticator) + (.setLoginService login-service) + (.setRealmName realm))] + (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] + (.put security-handler path-spec constraint)) + (when identity-service + (log/debugf "identity service %s" (type identity-service)) + (.setIdentityService security-handler identity-service)) + security-handler))) \ No newline at end of file From 5d3260a8cb520879174164f9b35586e9051afa23 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 15:07:11 +1000 Subject: [PATCH 06/25] rework security handler implementations into separate namespaces --- README.md | 67 +++++++++++++------------- project.clj | 2 +- src/slipway.clj | 11 +++-- src/slipway/connector/http.clj | 13 ++--- src/slipway/connector/https.clj | 17 +++---- src/slipway/handler.clj | 8 +-- src/slipway/security.clj | 54 +++------------------ src/slipway/security/hash.clj | 51 ++++++++++++++++++++ src/slipway/security/jaas.clj | 36 ++++++++++++++ src/slipway/security/openid.clj | 31 ++++++++++++ src/slipway/session.clj | 3 +- test/slipway/security/hash_test.clj | 15 ++++++ test/slipway/server_forwarded_test.clj | 2 +- test/slipway/server_http_test.clj | 2 +- test/slipway/server_https_test.clj | 2 +- test/slipway/server_proxied_test.clj | 2 +- test/slipway/session_test.clj | 12 +++++ test/slipway/test_server.clj | 40 +++++++-------- test/slipway/websockets_http_test.clj | 4 +- test/slipway/websockets_https_test.clj | 4 +- 20 files changed, 242 insertions(+), 134 deletions(-) create mode 100644 src/slipway/security/hash.clj create mode 100644 src/slipway/security/jaas.clj create mode 100644 src/slipway/security/openid.clj create mode 100644 test/slipway/security/hash_test.clj create mode 100644 test/slipway/session_test.clj diff --git a/README.md b/README.md index 782e057b..835c6a1c 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ The stateful `start!`/`stop!` functions are a convenience for integration tests (test-server/start! [:http]) ;; Start with hash-based form authentication -(test-server/start! [:http] :hash-auth) +(test-server/start! [:http] :hash-form) ;; Start with basic authentication (test-server/start! [:http] :basic-auth) @@ -255,7 +255,6 @@ Configuration of the default server handler. ```clojure #:slipway.handler{:context-path "the root context path, default '/'" - :ws-path "the path serving the websocket upgrade handler, default '/chsk'" :null-path-info? "true if /path is not redirected to /path/, default true"} ``` @@ -325,17 +324,18 @@ Example constraint mapping: Configuration of an HTTP server connector. ```clojure -#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or hostname. Default null (all interfaces)" - :port "port this connector listens on. If 0 a random port is assigned, default 80" - :idle-timeout-ms "max idle time for a connection in ms, default 200000" - :http-forwarded? "if true, add the ForwardedRequestCustomizer. See Jetty Forward HTTP docs" +#:slipway.connector.http{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80" + :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 30000 ms" + :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" :proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs" :http-config "a concrete HttpConfiguration object to replace the default config entirely" :configurator "a fn taking the final connector as argument, allowing further configuration" - :send-server-version? "if true, send the Server header in responses (default false)" - :send-date-header? "if true, send the Date header in responses (default false)" - :relative-redirect-allowed? "if true, allow relative redirects (default false)" - :http-compliance "set the HttpCompliance mode, e.g. 'RFC2616' or 'RFC7230' (default RFC9110)"} + :send-server-version? "if true, send the Server header in responses" + :send-date-header? "if true, send the Date header in responses" + :relative-redirect-allowed? "if true, allow relative redirects, default false" + :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} ``` ### :slipway.connector.https @@ -343,36 +343,37 @@ Configuration of an HTTP server connector. Configuration of an HTTPS server connector. ```clojure -#:slipway.connector.https{:host "the network interface this connector binds to as an IP address or hostname. Default null (all interfaces)" - :port "port this connector listens on. If 0 a random port is assigned, default 443" - :idle-timeout-ms "max idle time for a connection in ms, default 200000" - :http-forwarded? "if true, add the ForwardedRequestCustomizer. See Jetty Forward HTTP docs" +#:slipway.connector.https{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(). default 443" + :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 30000 ms" + :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" :proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs" :http-config "a concrete HttpConfiguration object to replace the default config entirely" :configurator "a fn taking the final connector as argument, allowing further configuration" :keystore "keystore to use, either path (String) or concrete KeyStore" - :keystore-type "type of keystore, e.g. JKS or PKCS12" + :keystore-type "type of keystore, e.g. JKS" :keystore-password "password of the keystore" :key-manager-password "password for the specific key within the keystore" :truststore "truststore to use, either path (String) or concrete KeyStore" :truststore-password "password of the truststore" - :truststore-type "type of the truststore, e.g. JKS or PKCS12" + :truststore-type "type of the truststore, eg. JKS" :include-protocols "a list of protocol name patterns to include in SSLEngine" :exclude-protocols "a list of protocol name patterns to exclude from SSLEngine" - :replace-exclude-protocols? "if true will replace existing exclude-protocols, otherwise adds them" + :replace-exclude-protocols? "if true will replace existing exclude-protocols, otherwise will add them" :exclude-ciphers "a list of cipher suite names to exclude from SSLEngine" - :replace-exclude-ciphers? "if true will replace existing exclude-ciphers, otherwise adds them" + :replace-exclude-ciphers? "if true will replace existing exclude-ciphers, otherwise will add them" :security-provider "the security provider name" :client-auth "either :need or :want to set the corresponding need/wantClientAuth field" :ssl-context "a concrete pre-configured SslContext" - :sni-required? "if true SNI is required, else requests are rejected with 400, default false" - :sni-host-check? "if true the SNI host name must match when there is an SNI certificate, default false" - :sts-max-age-s "set the Strict-Transport-Security max age in seconds (default -1, disabled)" - :sts-include-subdomains? "true if includeSubDomains is sent with any Strict-Transport-Security header" - :send-server-version? "if true, send the Server header in responses (default false)" - :send-date-header? "if true, send the Date header in responses (default false)" - :relative-redirect-allowed? "if true, allow relative redirects (default false)" - :http-compliance "set the HttpCompliance mode, e.g. 'RFC2616' or 'RFC7230' (default RFC9110)"} + :sni-required? "if true SNI is required, else requests will be rejected with 400 response, default false" + :sni-host-check? "if true the SNI Host name must match when there is an SNI certificate, default false" + :sts-max-age-s "set the Strict-Transport-Security max age in seconds, default -1" + :sts-include-subdomains? "true if a include subdomain property is sent with any Strict-Transport-Security header" + :send-server-version? "if true, send the Server header in responses" + :send-date-header? "if true, send the Date header in responses" + :relative-redirect-allowed? "if true, allow relative redirects, default false" + :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} ``` ### :slipway.handler.compression @@ -380,21 +381,21 @@ Configuration of an HTTPS server connector. Configuration of the compression handler. Replaces the former `:slipway.handler.gzip` namespace from Slipway 1.x. ```clojure -#:slipway.handler.compression{:enabled? "is the compression handler enabled? default true" - :path-spec "the compression path-spec, default '/*'" - :format "compression format dispatch key, defaults to :gzip (GzipCompression)" - :compress-min-bytes "min response size to trigger compression in bytes (default 1024)" - :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} +#:slipway.handler.compression{:enabled? "is compression handler enabled? default true" + :path-spec "the compression path-spec, default '/*'" + :format "compression format, defaults to :gzip" + :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" + :compression-config "a concrete Jetty CompressConfig instance (nil for default configuration)"} ``` The `:format` key dispatches via `defmulti` — extend it to add custom compression formats: ```clojure (require '[slipway.handler.compression :as compression]) -(import '[org.eclipse.jetty.compression.gzip GzipCompression]) +(import '[your.org.YourCompression]) (defmethod compression/format :my-format [_opts] - (GzipCompression.)) ; substitute your own compression implementation + (YourCompression.)) ; substitute your own compression implementation ``` ## Sente Websockets diff --git a/project.clj b/project.clj index aa4b3bb6..523ab61f 100644 --- a/project.clj +++ b/project.clj @@ -15,7 +15,7 @@ [ring/ring-anti-forgery "1.4.0"] [metosin/reitit-ring "0.10.1"]] :resource-paths ["dev-resources"] - :plugins [[dev.weavejester/lein-cljfmt "0.16.3"]]} + :plugins [[dev.weavejester/lein-cljfmt "0.16.4"]]} :pedantic {:pedantic? :abort}} :aliases {"check" ["with-profile" "+pedantic" "check"] diff --git a/src/slipway.clj b/src/slipway.clj index 6d4fc3cd..650d045a 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -16,7 +16,8 @@ :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" :compression-config "a concrete Jetty CompressConfig instance (nil for default configuration)"} - #:slipway.connector.https{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + #:slipway.connector.https{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(). default 443" :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms" :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" @@ -45,9 +46,10 @@ :send-server-version? "if true, send the Server header in responses" :send-date-header? "if true, send the Date header in responses" :relative-redirect-allowed? "if true, allow relative redirects, default false" - :http-compliance "set 'RFC2616' to support reduced HttpCompliance, default is Jetty HttpCompliance/default"} + :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} - #:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + #:slipway.connector.http{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80" :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms" :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" @@ -57,7 +59,7 @@ :send-server-version? "if true, send the Server header in responses" :send-date-header? "if true, send the Date header in responses" :relative-redirect-allowed? "if true, allow relative redirects, default false" - :http-compliance "set 'RFC2616' to support reduced HttpCompliance, default is Jetty HttpCompliance/default"} + :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} #:slipway.security{:realm "the Jetty authentication realm" :hash-user-file "the path to a Jetty Hash User File" @@ -91,7 +93,6 @@ :auto-fragment "websocket auto fragment (boolean), default true"} #:slipway.handler{:context-path "the root context path, default '/'" - :ws-path "the path serving the websocket upgrade handler, default '/chsk'" :null-path-info? "true if /path is not redirected to /path/, default true"} #:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)" diff --git a/src/slipway/connector/http.clj b/src/slipway/connector/http.clj index bcb086b4..44877f9f 100644 --- a/src/slipway/connector/http.clj +++ b/src/slipway/connector/http.clj @@ -27,9 +27,10 @@ config)) (comment - #:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + #:slipway.connector.http{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80" - :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms" + :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 30000 ms" :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" :proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs" :http-config "a concrete HttpConfiguration object to replace the default config entirely" @@ -40,9 +41,8 @@ :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"}) (defmethod server/connector ::connector - [^Server server {::keys [host port idle-timeout-ms proxy-protocol? http-forwarded? configurator http-config] - :or {idle-timeout-ms 200000 - port 80} + [^Server server {::keys [name host port idle-timeout-ms proxy-protocol? http-forwarded? configurator http-config] + :or {port 80} :as opts}] (log/debugf (str "starting " (when proxy-protocol? "proxied ") "HTTP connector on %s:%s" (when http-forwarded? " with http-forwarded support")) (or host "all-interfaces") port) (let [http-factory (HttpConnectionFactory. (or http-config (default-config opts))) @@ -51,6 +51,7 @@ connector (ServerConnector. ^Server server ^"[Lorg.eclipse.jetty.server.ConnectionFactory;" factories)] (.setHost connector host) (.setPort connector port) - (.setIdleTimeout connector idle-timeout-ms) + (some->> name (.setName connector)) + (some->> idle-timeout-ms (.setIdleTimeout connector)) (when configurator (configurator connector)) connector)) \ No newline at end of file diff --git a/src/slipway/connector/https.clj b/src/slipway/connector/https.clj index 4839e686..c7cf20a7 100644 --- a/src/slipway/connector/https.clj +++ b/src/slipway/connector/https.clj @@ -50,10 +50,8 @@ (when key-manager-password (.setKeyManagerPassword context-factory key-manager-password)) (cond - (string? truststore) - (.setTrustStorePath context-factory truststore) - (instance? KeyStore truststore) - (.setTrustStore context-factory ^KeyStore truststore)) + (string? truststore) (.setTrustStorePath context-factory truststore) + (instance? KeyStore truststore) (.setTrustStore context-factory ^KeyStore truststore)) (when truststore-password (.setTrustStorePassword context-factory truststore-password)) (when truststore-type @@ -91,9 +89,10 @@ (ServerConnector. server (context-factory opts) ^"[Lorg.eclipse.jetty.server.ConnectionFactory;" (into-array ConnectionFactory [http-factory]))) (comment - #:slipway.connector.https{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" + #:slipway.connector.https{:name "the name of this connector (useful for VirtualHosts configuration)" + :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" :port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(). default 443" - :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms" + :idle-timeout-ms "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 30000 ms" :http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs" :proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs" :http-config "a concrete HttpConfiguration object to replace the default config entirely" @@ -124,13 +123,13 @@ (defmethod server/connector ::connector [^Server server {::keys [host port idle-timeout-ms proxy-protocol? http-config configurator] - :or {idle-timeout-ms 200000 - port 443} + :or {port 443} :as opts}] (let [http-factory (HttpConnectionFactory. (or http-config (default-config opts))) connector (if proxy-protocol? (proxied-connector server http-factory opts) (standard-connector server http-factory opts))] (.setHost connector host) (.setPort connector port) - (.setIdleTimeout connector idle-timeout-ms) + (some->> name (.setName connector)) + (some->> idle-timeout-ms (.setIdleTimeout connector)) (when configurator (configurator connector)) connector)) diff --git a/src/slipway/handler.clj b/src/slipway/handler.clj index e0ebe083..ad2a425a 100644 --- a/src/slipway/handler.clj +++ b/src/slipway/handler.clj @@ -5,27 +5,27 @@ [slipway.server :as server] [slipway.session :as session] [slipway.websockets :as websockets]) - (:import (org.eclipse.jetty.server Handler Server) + (:import (org.eclipse.jetty.security SecurityHandler) + (org.eclipse.jetty.server Handler Server) (org.eclipse.jetty.server.handler ContextHandler) (slipway.handler SyncHandler))) (comment #:slipway.handler{:context-path "the root context path, default '/'" - :ws-path "the path serving the websocket upgrade handler, default '/chsk'" :null-path-info? "true if /path is not redirected to /path/, default true"}) (defmethod server/handler :default [^Server server ring-handler {::keys [context-path null-path-info?] :or {context-path "/"} :as opts}] - (log/debugf "creating default server handler, context path %s, null-path-info? %s" context-path null-path-info?) + (log/debugf "creating default server context-handler, context path %s, null-path-info? %s" context-path null-path-info?) (let [context-handler (doto (ContextHandler.) (.setContextPath context-path) (.setAllowNullPathInContext (not (false? null-path-info?)))) app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) (SyncHandler. ring-handler nil)) - auth-handler (when-let [security-handler (security/handler opts)] + auth-handler (when-let [^SecurityHandler security-handler (security/handler opts)] (let [session-handler (session/handler opts)] (.setHandler security-handler ^Handler app-handler) (.setHandler session-handler security-handler) diff --git a/src/slipway/security.clj b/src/slipway/security.clj index f90c612b..98cd8f88 100644 --- a/src/slipway/security.clj +++ b/src/slipway/security.clj @@ -1,33 +1,11 @@ (ns slipway.security - (:require [clojure.core.protocols :as p] - [clojure.tools.logging :as log]) - (:import (javax.security.auth.login Configuration) - (org.eclipse.jetty.security AuthenticationState AuthenticationState$Succeeded Authenticator Constraint - HashLoginService LoginService SecurityHandler SecurityHandler$PathMapped) - (org.eclipse.jetty.security.jaas JAASLoginService) - (org.eclipse.jetty.server Request) - (org.eclipse.jetty.util.resource ResourceFactory))) + (:require [clojure.core.protocols :as p]) + (:import (org.eclipse.jetty.security AuthenticationState AuthenticationState$Succeeded) + (org.eclipse.jetty.server Request))) -(defmulti login-service ::login-service) +(defmulti handler ::login-service) -(defmethod login-service :default [_] nil) - -(defmethod login-service "jaas" - [{::keys [realm]}] - (let [config (System/getProperty "java.security.auth.login.config")] - (log/debugf "initializing JAASLoginService - realm: %s, java.security.auth.login.config: %s " realm config) - (if config - (when (slurp config) - (doto (JAASLoginService. realm) (.setConfiguration (Configuration/getConfiguration)))) - (throw (ex-info "start with -Djava.security.auth.login.config=/some/path/to/jaas.config to use Jetty/JAAS auth provider" {}))))) - -(defmethod login-service "hash" - [{::keys [realm hash-user-file]}] - (log/debugf "initializing HashLoginService - realm: %s, realm file: %s" realm hash-user-file) - (if hash-user-file - (when (slurp hash-user-file) - (HashLoginService. realm (.newResource (ResourceFactory/root) ^String hash-user-file))) - (throw (ex-info "set the path to your hash user realm properties file" {})))) +(defmethod handler :default [_] nil) (defn user [^Request request] @@ -36,24 +14,4 @@ (p/datafy authentication-state)))) (comment - #:slipway.security{:realm "the Jetty authentication realm" - :hash-user-file "the path to a Jetty Hash User File" - :login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default" - :identity-service "a concrete Jetty IdentityService" - :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" - :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]"}) - -(defn handler ^SecurityHandler - [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] - (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) - (when-let [^LoginService login-service (login-service opts)] - (let [security-handler (doto (SecurityHandler$PathMapped.) - (.setAuthenticator ^Authenticator authenticator) - (.setLoginService login-service) - (.setRealmName realm))] - (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] - (.put security-handler path-spec constraint)) - (when identity-service - (log/debugf "identity service %s" (type identity-service)) - (.setIdentityService security-handler identity-service)) - security-handler))) \ No newline at end of file + #:slipway.security{:login-service "a Jetty LoginService identifier, 'jaas', 'hash', and 'openid' supported by default"}) \ No newline at end of file diff --git a/src/slipway/security/hash.clj b/src/slipway/security/hash.clj new file mode 100644 index 00000000..bc6ae276 --- /dev/null +++ b/src/slipway/security/hash.clj @@ -0,0 +1,51 @@ +(ns slipway.security.hash + (:require [clojure.tools.logging :as log] + [slipway.security :as security]) + (:import (org.eclipse.jetty.security Authenticator Constraint HashLoginService LoginService SecurityHandler$PathMapped UserStore) + (org.eclipse.jetty.util.resource ResourceFactory) + (org.eclipse.jetty.util.security Credential))) + +(defn property-file-service + [realm user-file] + (when (slurp user-file) + (HashLoginService. realm (.newResource (ResourceFactory/root) ^String user-file)))) + +(defn in-memory-service + [realm users] + (let [user-store (UserStore.) + hash-service (HashLoginService. realm)] + (doseq [[user-name credential roles] users] + (.addUser user-store user-name (Credential/getCredential credential) (into-array String roles))) + (.setUserStore hash-service user-store) + hash-service)) + +(defn login-service ^HashLoginService + [{::keys [realm user-file users]}] + (log/debugf "initializing HashLoginService - realm: %s, realm file: %s, users: %s realm" realm user-file (count users)) + (cond + user-file (property-file-service realm user-file) + users (in-memory-service realm users) + :else (throw (ex-info "provide a :realm and either :user-file or :users configuration" {})))) + +(comment + #:slipway.security.hash{:realm "optional Jetty authentication realm" + :user-file "the path to a Jetty hash-user file" + :users "a sequence of [user-name, credential, [roles]]" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "a concrete Jetty IdentityService"}) + +(defmethod security/handler "hash" + [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] + (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) + (when-let [^LoginService login-service (login-service opts)] + (let [security-handler (doto (SecurityHandler$PathMapped.) + (.setAuthenticator ^Authenticator authenticator) + (.setLoginService login-service) + (.setRealmName realm))] + (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] + (.put security-handler path-spec constraint)) + (when identity-service + (log/debugf "identity service %s" (type identity-service)) + (.setIdentityService security-handler identity-service)) + security-handler))) \ No newline at end of file diff --git a/src/slipway/security/jaas.clj b/src/slipway/security/jaas.clj new file mode 100644 index 00000000..36c352b5 --- /dev/null +++ b/src/slipway/security/jaas.clj @@ -0,0 +1,36 @@ +(ns slipway.security.jaas + (:require [clojure.tools.logging :as log] + [slipway.security :as security]) + (:import (javax.security.auth.login Configuration) + (org.eclipse.jetty.security Authenticator Constraint SecurityHandler$PathMapped) + (org.eclipse.jetty.security.jaas JAASLoginService))) + +(defn login-service ^JAASLoginService + [{::keys [realm]}] + (let [config (System/getProperty "java.security.auth.login.config")] + (log/debugf "initializing JAASLoginService - realm: %s, java.security.auth.login.config: %s " realm config) + (if config + (when (slurp config) + (doto (JAASLoginService. realm) (.setConfiguration (Configuration/getConfiguration)))) + (throw (ex-info "start with -Djava.security.auth.login.config=/some/path/to/jaas.config to use Jetty/JAAS auth provider" {}))))) + +(comment + #:slipway.security.jaas{:realm "the Jetty authentication realm" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "a concrete Jetty IdentityService"}) + +(defmethod security/handler "jaas" + [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] + (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) + (when-let [login-service (login-service opts)] + (let [security-handler (doto (SecurityHandler$PathMapped.) + (.setAuthenticator ^Authenticator authenticator) + (.setLoginService login-service) + (.setRealmName realm))] + (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] + (.put security-handler path-spec constraint)) + (when identity-service + (log/debugf "identity service %s" (type identity-service)) + (.setIdentityService security-handler identity-service)) + security-handler))) \ No newline at end of file diff --git a/src/slipway/security/openid.clj b/src/slipway/security/openid.clj new file mode 100644 index 00000000..1e648e23 --- /dev/null +++ b/src/slipway/security/openid.clj @@ -0,0 +1,31 @@ +(ns slipway.security.openid + (:require [clojure.tools.logging :as log]) + (:import (org.eclipse.jetty.security Authenticator Constraint + LoginService SecurityHandler SecurityHandler$PathMapped))) + +(defn login-service "openid" + [{::keys []}] + ) + +(comment + #:slipway.security{:realm "the Jetty authentication realm" + :hash-user-file "the path to a Jetty Hash User File" + :login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default" + :identity-service "a concrete Jetty IdentityService" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]"}) + +(defn handler ^SecurityHandler + [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] + (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) + (when-let [^LoginService login-service (login-service opts)] + (let [security-handler (doto (SecurityHandler$PathMapped.) + (.setAuthenticator ^Authenticator authenticator) + (.setLoginService login-service) + (.setRealmName realm))] + (doseq [[^String path-spec ^Constraint constraint] constraint-mappings] + (.put security-handler path-spec constraint)) + (when identity-service + (log/debugf "identity service %s" (type identity-service)) + (.setIdentityService security-handler identity-service)) + security-handler))) \ No newline at end of file diff --git a/src/slipway/session.clj b/src/slipway/session.clj index 2946b304..23fa8d60 100644 --- a/src/slipway/session.clj +++ b/src/slipway/session.clj @@ -8,7 +8,8 @@ (case same-site :none HttpCookie$SameSite/NONE :lax HttpCookie$SameSite/LAX - :strict HttpCookie$SameSite/STRICT)) + :strict HttpCookie$SameSite/STRICT + HttpCookie$SameSite/STRICT)) (comment #:slipway.session{:secure-request-only? "set the secure flag on session cookies" diff --git a/test/slipway/security/hash_test.clj b/test/slipway/security/hash_test.clj new file mode 100644 index 00000000..05cd945f --- /dev/null +++ b/test/slipway/security/hash_test.clj @@ -0,0 +1,15 @@ +(ns slipway.security.hash-test + (:require [clojure.test :refer [deftest is]] + [slipway.security.hash :as hash]) + (:import (clojure.lang ExceptionInfo) + (org.eclipse.jetty.security HashLoginService))) + +(deftest login-service + + (is (thrown? ExceptionInfo (hash/login-service nil))) + + (is (thrown? ExceptionInfo (hash/login-service {}))) + + (is (= HashLoginService + (type (hash/login-service {::hash/realm "test-realm" + ::hash/users [["user-1" "password-1" ["role1" "role2"]]]}))))) \ No newline at end of file diff --git a/test/slipway/server_forwarded_test.clj b/test/slipway/server_forwarded_test.clj index e6647418..15212098 100644 --- a/test/slipway/server_forwarded_test.clj +++ b/test/slipway/server_forwarded_test.clj @@ -100,7 +100,7 @@ (deftest form-authentication (try - (server/start! [:http+https+forwarded] :hash-auth) + (server/start! [:http+https+forwarded] :hash-form) (testing "constraints http" diff --git a/test/slipway/server_http_test.clj b/test/slipway/server_http_test.clj index bd6f4300..d6770248 100644 --- a/test/slipway/server_http_test.clj +++ b/test/slipway/server_http_test.clj @@ -129,7 +129,7 @@ (deftest form-authentication (try - (server/start! [:http] :hash-auth) + (server/start! [:http] :hash-form) (testing "constraints" diff --git a/test/slipway/server_https_test.clj b/test/slipway/server_https_test.clj index 1c9b1d2b..e4f3ea8b 100644 --- a/test/slipway/server_https_test.clj +++ b/test/slipway/server_https_test.clj @@ -133,7 +133,7 @@ (deftest form-authentication (try - (server/start! [:https] :hash-auth) + (server/start! [:https] :hash-form) (testing "constraints" diff --git a/test/slipway/server_proxied_test.clj b/test/slipway/server_proxied_test.clj index 35fd3343..d3193a75 100644 --- a/test/slipway/server_proxied_test.clj +++ b/test/slipway/server_proxied_test.clj @@ -104,7 +104,7 @@ (deftest form-authentication (try - (server/start! [:http+https+proxied] :hash-auth) + (server/start! [:http+https+proxied] :hash-form) (testing "constraints http" diff --git a/test/slipway/session_test.clj b/test/slipway/session_test.clj new file mode 100644 index 00000000..18afc015 --- /dev/null +++ b/test/slipway/session_test.clj @@ -0,0 +1,12 @@ +(ns slipway.session-test + (:require [clojure.test :refer [deftest is]] + [slipway.session :as session]) + (:import (org.eclipse.jetty.http HttpCookie$SameSite))) + +(deftest cookie-same-site + + (is (= HttpCookie$SameSite/STRICT (session/cookie-same-site nil))) + (is (= HttpCookie$SameSite/NONE (session/cookie-same-site :none))) + (is (= HttpCookie$SameSite/LAX (session/cookie-same-site :lax))) + (is (= HttpCookie$SameSite/STRICT (session/cookie-same-site :strict))) + (is (= HttpCookie$SameSite/STRICT (session/cookie-same-site :bad-input)))) \ No newline at end of file diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 3c63fc48..6fe7baae 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -6,7 +6,9 @@ [slipway.connector.https :as https] [slipway.example.app :as app] [slipway.handler.compression :as compression] - [slipway.security :as security] + [slipway.security] + [slipway.security.hash :as hash] + [slipway.security.hash :as jaas] [slipway.sente] [slipway.server :as server] [slipway.session :as session] @@ -93,28 +95,28 @@ [_] {}) -(defmethod authentication :jaas-auth +(defmethod authentication :jaas-form [_] - #::security{:realm "slipway" - :login-service "jaas" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints}) + #::jaas{:realm "slipway" + :login-service "jaas" + :authenticator (FormAuthenticator. "/login" "/login-retry" false) + :constraint-mappings app/constraints}) -(defmethod authentication :hash-auth +(defmethod authentication :hash-form [_] - #::security{:realm "slipway" - :login-service "hash" - :hash-user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints}) + #::hash{:realm "slipway" + :login-service "hash" + :hash-user-file "dev-resources/jaas/hash-realm.properties" + :authenticator (FormAuthenticator. "/login" "/login-retry" false) + :constraint-mappings app/constraints}) -(defmethod authentication :basic-auth +(defmethod authentication :hash-basic [_] - #::security{:realm "slipway" - :login-service "hash" - :hash-user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (BasicAuthenticator.) - :constraint-mappings app/constraints}) + #::hash{:realm "slipway" + :login-service "hash" + :hash-user-file "dev-resources/jaas/hash-realm.properties" + :authenticator (BasicAuthenticator.) + :constraint-mappings app/constraints}) (defn stop! [] @@ -124,7 +126,7 @@ "To run a JAAS authenticated server, start a REPL with the following JVM JAAS parameter: - Hash User Auth -> -Djava.security.auth.login.config=/dev-resources/jaas/hash-jaas.conf - LDAP Auth -> -Djava.security.auth.login.config=/dev-resources/jaas/ldap-jaas.conf - Then: (start! [:http] :jaas-auth) + Then: (start! [:http] :jaas-form) Note: Authentication loginHandlers are stateful, so they must be created fresh for each server" (defn start! diff --git a/test/slipway/websockets_http_test.clj b/test/slipway/websockets_http_test.clj index 82baf788..95092e18 100644 --- a/test/slipway/websockets_http_test.clj +++ b/test/slipway/websockets_http_test.clj @@ -95,7 +95,7 @@ (deftest full-connection-form-auth (try - (server/start! [:http] :hash-auth) + (server/start! [:http] :hash-form) (let [{:keys [csrf-token cookies]} (client/do-login "http" "localhost" 3000 "/" "admin" "admin") client-id (str (random-uuid)) @@ -297,7 +297,7 @@ (deftest ws-connection-upgrade-with-form-auth (try - (server/start! [:http :websockets] :hash-auth) + (server/start! [:http :websockets] :hash-form) (let [{:keys [csrf-token cookies]} (client/do-login "http" "localhost" 3000 "/" "admin" "admin") client-id (str (random-uuid)) diff --git a/test/slipway/websockets_https_test.clj b/test/slipway/websockets_https_test.clj index 4f2fd6b5..95a3792d 100644 --- a/test/slipway/websockets_https_test.clj +++ b/test/slipway/websockets_https_test.clj @@ -101,7 +101,7 @@ (deftest full-connection-with-form-auth (try - (server/start! [:https :websockets] :hash-auth) + (server/start! [:https :websockets] :hash-form) (let [{:keys [csrf-token cookies]} (client/do-login "https" "localhost" 3443 "/" "admin" "admin" {:insecure? true}) client-id (str (random-uuid)) @@ -306,7 +306,7 @@ (deftest ws-connection-upgrade-with-form-auth (try - (server/start! [:https :websockets] :hash-auth) + (server/start! [:https :websockets] :hash-form) (let [{:keys [csrf-token cookies]} (client/do-login "https" "localhost" 3443 "/" "admin" "admin" {:insecure? true}) client-id (str (random-uuid)) From 6981a1cda7ac9dc838bae3c0dd0cb955d1e1307c Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 15:37:16 +1000 Subject: [PATCH 07/25] fix tests --- README.md | 33 ++++++++++++++++++++----- src/slipway.clj | 19 ++++++++++----- src/slipway/connector/http.clj | 2 +- src/slipway/connector/https.clj | 2 +- src/slipway/security.clj | 4 ++-- src/slipway/security/hash.clj | 5 ++-- src/slipway/security/jaas.clj | 2 +- test/slipway/connector/http_test.clj | 2 +- test/slipway/test_server.clj | 36 ++++++++++++++++------------ 9 files changed, 69 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 835c6a1c..3cd7b504 100644 --- a/README.md +++ b/README.md @@ -299,12 +299,33 @@ Configuration of HTTP session options. Configuration of Jetty auth options. See [JAAS Authentication](#jaas-authentication) below for configuration guides. ```clojure -#:slipway.security{:realm "the Jetty authentication realm" - :hash-user-file "the path to a Jetty Hash User File" - :login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default" - :identity-service "a concrete Jetty IdentityService" - :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" - :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint] pairs"} +#:slipway.security{:handler "identifies a SecurityHandler impl, 'jaas', 'hash', and 'openid' supported by default"} +``` + +Three auth implementations are provided by default. + +#### :slipway.security.hash + +Configure simple authentication with Jetty's built in HashLoginService + +```clojure +#:slipway.security.hash{:realm "optional Jetty authentication realm" + :user-file "the path to a Jetty hash-user file" + :users "a sequence of [^String user-name, ^String credential, ^String[] [roles]]" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "a concrete Jetty IdentityService"} +``` + +#### :slipway.security.jaas + +Configure JAAS authentication with Jetty's built in [JAAS compatible login-modules](https://jetty.org/docs/jetty/12.1/operations-guide/security/jaas-support.html) + +```clojure +#:slipway.security.jaas{:realm "the Jetty authentication realm" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "an (optional) concrete Jetty IdentityService"} ``` Example constraint mapping: diff --git a/src/slipway.clj b/src/slipway.clj index 650d045a..37be6256 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -61,12 +61,19 @@ :relative-redirect-allowed? "if true, allow relative redirects, default false" :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} - #:slipway.security{:realm "the Jetty authentication realm" - :hash-user-file "the path to a Jetty Hash User File" - :login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default" - :identity-service "a concrete Jetty IdentityService" - :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" - :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]"} + #:slipway.security{:handler "identifies a SecurityHandler impl, 'jaas', 'hash', and 'openid' supported by default"} + + #:slipway.security.hash{:realm "optional Jetty authentication realm" + :user-file "the path to a Jetty hash-user file" + :users "a sequence of [^String user-name, ^String credential, ^String[] [roles]]" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "an (optional) concrete Jetty IdentityService"} + + #:slipway.security.jaas{:realm "the Jetty authentication realm" + :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" + :identity-service "an (optional) concrete Jetty IdentityService"} #:slipway.session{:secure-request-only? "set the secure flag on session cookies" :http-only? "set the http-only flag on session cookies" diff --git a/src/slipway/connector/http.clj b/src/slipway/connector/http.clj index 44877f9f..2a62ae0a 100644 --- a/src/slipway/connector/http.clj +++ b/src/slipway/connector/http.clj @@ -41,7 +41,7 @@ :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"}) (defmethod server/connector ::connector - [^Server server {::keys [name host port idle-timeout-ms proxy-protocol? http-forwarded? configurator http-config] + [^Server server {::keys [host port name idle-timeout-ms proxy-protocol? http-forwarded? configurator http-config] :or {port 80} :as opts}] (log/debugf (str "starting " (when proxy-protocol? "proxied ") "HTTP connector on %s:%s" (when http-forwarded? " with http-forwarded support")) (or host "all-interfaces") port) diff --git a/src/slipway/connector/https.clj b/src/slipway/connector/https.clj index c7cf20a7..6ff93089 100644 --- a/src/slipway/connector/https.clj +++ b/src/slipway/connector/https.clj @@ -122,7 +122,7 @@ :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"}) (defmethod server/connector ::connector - [^Server server {::keys [host port idle-timeout-ms proxy-protocol? http-config configurator] + [^Server server {::keys [host port name idle-timeout-ms proxy-protocol? http-config configurator] :or {port 443} :as opts}] (let [http-factory (HttpConnectionFactory. (or http-config (default-config opts))) diff --git a/src/slipway/security.clj b/src/slipway/security.clj index 98cd8f88..8795aec0 100644 --- a/src/slipway/security.clj +++ b/src/slipway/security.clj @@ -3,7 +3,7 @@ (:import (org.eclipse.jetty.security AuthenticationState AuthenticationState$Succeeded) (org.eclipse.jetty.server Request))) -(defmulti handler ::login-service) +(defmulti handler ::handler) (defmethod handler :default [_] nil) @@ -14,4 +14,4 @@ (p/datafy authentication-state)))) (comment - #:slipway.security{:login-service "a Jetty LoginService identifier, 'jaas', 'hash', and 'openid' supported by default"}) \ No newline at end of file + #:slipway.security{:handler "identifies a SecurityHandler impl, 'jaas', 'hash', and 'openid' supported by default"}) \ No newline at end of file diff --git a/src/slipway/security/hash.clj b/src/slipway/security/hash.clj index bc6ae276..5509aae0 100644 --- a/src/slipway/security/hash.clj +++ b/src/slipway/security/hash.clj @@ -30,11 +30,10 @@ (comment #:slipway.security.hash{:realm "optional Jetty authentication realm" :user-file "the path to a Jetty hash-user file" - :users "a sequence of [user-name, credential, [roles]]" + :users "a sequence of [^String user-name, ^String credential, ^String[] [roles]]" :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" - :identity-service "a concrete Jetty IdentityService"}) - + :identity-service "an (optional) concrete Jetty IdentityService"}) (defmethod security/handler "hash" [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] (log/debugf "creating security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) diff --git a/src/slipway/security/jaas.clj b/src/slipway/security/jaas.clj index 36c352b5..571d429b 100644 --- a/src/slipway/security/jaas.clj +++ b/src/slipway/security/jaas.clj @@ -18,7 +18,7 @@ #:slipway.security.jaas{:realm "the Jetty authentication realm" :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]" - :identity-service "a concrete Jetty IdentityService"}) + :identity-service "an (optional) concrete Jetty IdentityService"}) (defmethod security/handler "jaas" [{::keys [realm authenticator constraint-mappings identity-service] :as opts}] diff --git a/test/slipway/connector/http_test.clj b/test/slipway/connector/http_test.clj index c68c74ad..f4f6218b 100644 --- a/test/slipway/connector/http_test.clj +++ b/test/slipway/connector/http_test.clj @@ -4,7 +4,7 @@ (:import (org.eclipse.jetty.http HttpCompliance) (org.eclipse.jetty.server HttpConfiguration))) -(deftest http-ompliance-mode +(deftest http-compliance-mode (testing "default mode as documented" diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 6fe7baae..0cd4b1cd 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -6,7 +6,7 @@ [slipway.connector.https :as https] [slipway.example.app :as app] [slipway.handler.compression :as compression] - [slipway.security] + [slipway.security :as security] [slipway.security.hash :as hash] [slipway.security.hash :as jaas] [slipway.sente] @@ -97,26 +97,32 @@ (defmethod authentication :jaas-form [_] - #::jaas{:realm "slipway" - :login-service "jaas" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints}) + (merge + #::security{:handler "jaas"} + #::jaas{:realm "slipway" + :login-service "jaas" + :authenticator (FormAuthenticator. "/login" "/login-retry" false) + :constraint-mappings app/constraints})) (defmethod authentication :hash-form [_] - #::hash{:realm "slipway" - :login-service "hash" - :hash-user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints}) + (merge + #::security{:handler "hash"} + #::hash{:realm "slipway" + :login-service "hash" + :user-file "dev-resources/jaas/hash-realm.properties" + :authenticator (FormAuthenticator. "/login" "/login-retry" false) + :constraint-mappings app/constraints})) (defmethod authentication :hash-basic [_] - #::hash{:realm "slipway" - :login-service "hash" - :hash-user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (BasicAuthenticator.) - :constraint-mappings app/constraints}) + (merge + #::security{:handler "hash"} + #::hash{:realm "slipway" + :login-service "hash" + :user-file "dev-resources/jaas/hash-realm.properties" + :authenticator (BasicAuthenticator.) + :constraint-mappings app/constraints})) (defn stop! [] From 9e99dbcb26fbb58a22f8f420ad2f09d657321f20 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 15:42:39 +1000 Subject: [PATCH 08/25] update documentated configuration --- README.md | 4 ++-- src/slipway.clj | 4 ++-- src/slipway/handler/compression.clj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3cd7b504..56ce3cc5 100644 --- a/README.md +++ b/README.md @@ -402,11 +402,11 @@ Configuration of an HTTPS server connector. Configuration of the compression handler. Replaces the former `:slipway.handler.gzip` namespace from Slipway 1.x. ```clojure -#:slipway.handler.compression{:enabled? "is compression handler enabled? default true" +#:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" :path-spec "the compression path-spec, default '/*'" :format "compression format, defaults to :gzip" :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressConfig instance (nil for default configuration)"} + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} ``` The `:format` key dispatches via `defmulti` — extend it to add custom compression formats: diff --git a/src/slipway.clj b/src/slipway.clj index 37be6256..6cc2fc9d 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -10,11 +10,11 @@ (:import (org.eclipse.jetty.server Server))) (comment - #:slipway.handler.compression{:enabled? "is compression handler enabled? default true" + #:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" :path-spec "the compression path-spec, default '/*'" :format "compression format, defaults to :gzip" :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressConfig instance (nil for default configuration)"} + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} #:slipway.connector.https{:name "the name of this connector (useful for VirtualHosts configuration)" :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" diff --git a/src/slipway/handler/compression.clj b/src/slipway/handler/compression.clj index 8b9735e7..4c1d9de0 100644 --- a/src/slipway/handler/compression.clj +++ b/src/slipway/handler/compression.clj @@ -5,11 +5,11 @@ (org.eclipse.jetty.compression.server CompressionConfig CompressionHandler))) (comment - #:slipway.handler.compression{:enabled? "is compression handler enabled? default true" + #:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" :path-spec "the compression path-spec, default '/*'" :format "compression format, defaults to :gzip" :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressConfig instance (nil for default configuration)"}) + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"}) (defmulti format ::format) From a061f9bb24e8a7e2cc6cd9a17aba49bce812c61a Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 17:30:35 +1000 Subject: [PATCH 09/25] fix format --- src/slipway/security/openid.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/slipway/security/openid.clj b/src/slipway/security/openid.clj index 1e648e23..0de43586 100644 --- a/src/slipway/security/openid.clj +++ b/src/slipway/security/openid.clj @@ -4,8 +4,7 @@ LoginService SecurityHandler SecurityHandler$PathMapped))) (defn login-service "openid" - [{::keys []}] - ) + [{::keys []}]) (comment #:slipway.security{:realm "the Jetty authentication realm" From caa00353b8dd55fd16687a033dfac37d68b802e5 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 22:59:35 +1000 Subject: [PATCH 10/25] fix kondo --- test/slipway/test_server.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 0cd4b1cd..3ea95727 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -8,7 +8,7 @@ [slipway.handler.compression :as compression] [slipway.security :as security] [slipway.security.hash :as hash] - [slipway.security.hash :as jaas] + [slipway.security.jaas :as jaas] [slipway.sente] [slipway.server :as server] [slipway.session :as session] From 98ea40f0ae9ff2fb3fbbc02bb622476736bb02aa Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Mon, 15 Jun 2026 23:07:50 +1000 Subject: [PATCH 11/25] fix tests --- README.md | 2 +- test/slipway/server_forwarded_test.clj | 4 ++-- test/slipway/server_http_test.clj | 2 +- test/slipway/server_https_test.clj | 2 +- test/slipway/server_proxied_test.clj | 4 ++-- test/slipway/websockets_http_test.clj | 4 ++-- test/slipway/websockets_https_test.clj | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 56ce3cc5..64f158fe 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ The stateful `start!`/`stop!` functions are a convenience for integration tests (test-server/start! [:http] :hash-form) ;; Start with basic authentication -(test-server/start! [:http] :basic-auth) +(test-server/start! [:http] :hash-basic) ;; Start with HTTP + HTTPS (test-server/start! [:http+https]) diff --git a/test/slipway/server_forwarded_test.clj b/test/slipway/server_forwarded_test.clj index 15212098..eb96a553 100644 --- a/test/slipway/server_forwarded_test.clj +++ b/test/slipway/server_forwarded_test.clj @@ -349,7 +349,7 @@ (deftest basic-authentication-http (try - (server/start! [:http+https+forwarded] :basic-auth) + (server/start! [:http+https+forwarded] :hash-basic) (testing "constraints" @@ -411,7 +411,7 @@ (deftest basic-authentication-https (try - (server/start! [:http+https+forwarded] :basic-auth) + (server/start! [:http+https+forwarded] :hash-basic) (testing "constraints" diff --git a/test/slipway/server_http_test.clj b/test/slipway/server_http_test.clj index d6770248..7dac0cb7 100644 --- a/test/slipway/server_http_test.clj +++ b/test/slipway/server_http_test.clj @@ -279,7 +279,7 @@ (deftest basic-authentication (try - (server/start! [:http] :basic-auth) + (server/start! [:http] :hash-basic) (testing "constraints" diff --git a/test/slipway/server_https_test.clj b/test/slipway/server_https_test.clj index e4f3ea8b..34720bd4 100644 --- a/test/slipway/server_https_test.clj +++ b/test/slipway/server_https_test.clj @@ -284,7 +284,7 @@ (deftest basic-authentication (try - (server/start! [:https] :basic-auth) + (server/start! [:https] :hash-basic) (testing "constraints" diff --git a/test/slipway/server_proxied_test.clj b/test/slipway/server_proxied_test.clj index d3193a75..42b9dd2b 100644 --- a/test/slipway/server_proxied_test.clj +++ b/test/slipway/server_proxied_test.clj @@ -338,7 +338,7 @@ (deftest basic-authentication-http (try - (server/start! [:http+https+proxied] :basic-auth) + (server/start! [:http+https+proxied] :hash-basic) (testing "constraints" @@ -400,7 +400,7 @@ (deftest basic-authentication-https (try - (server/start! [:http+https+proxied] :basic-auth) + (server/start! [:http+https+proxied] :hash-basic) (testing "constraints" diff --git a/test/slipway/websockets_http_test.clj b/test/slipway/websockets_http_test.clj index 95092e18..ce353590 100644 --- a/test/slipway/websockets_http_test.clj +++ b/test/slipway/websockets_http_test.clj @@ -136,7 +136,7 @@ (deftest full-connection-basic-auth (try - (server/start! [:http] :basic-auth) + (server/start! [:http] :hash-basic) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "admin:admin@localhost" 3000) client-id (str (random-uuid)) @@ -415,7 +415,7 @@ (deftest ws-connection-upgrade-with-basic-auth (try - (server/start! [:http :websockets] :basic-auth) + (server/start! [:http :websockets] :hash-basic) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "admin:admin@localhost" 3000) client-id (str (random-uuid)) diff --git a/test/slipway/websockets_https_test.clj b/test/slipway/websockets_https_test.clj index 95a3792d..613d4e0f 100644 --- a/test/slipway/websockets_https_test.clj +++ b/test/slipway/websockets_https_test.clj @@ -147,7 +147,7 @@ (deftest full-connection-with-basic-auth (try - (server/start! [:https :websockets] :basic-auth) + (server/start! [:https :websockets] :hash-basic) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "admin:admin@localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) @@ -437,7 +437,7 @@ (deftest ws-connection-upgrade-with-basic-auth (try - (server/start! [:https :websockets] :basic-auth) + (server/start! [:https :websockets] :hash-basic) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "admin:admin@localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) From 4b41800998615522b1b89d368fdbbac44245289d Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 15:24:35 +1000 Subject: [PATCH 12/25] better use of mixed namespace maps --- test/slipway/test_server.clj | 37 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 3ea95727..e18f0c55 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -97,32 +97,29 @@ (defmethod authentication :jaas-form [_] - (merge - #::security{:handler "jaas"} - #::jaas{:realm "slipway" - :login-service "jaas" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints})) + {::security/handler "jaas" + ::jaas/realm "slipway" + ::jaas/login-service "jaas" + ::jaas/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::jaas/constraint-mappings app/constraints}) (defmethod authentication :hash-form [_] - (merge - #::security{:handler "hash"} - #::hash{:realm "slipway" - :login-service "hash" - :user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints})) + {::security/handler "hash" + ::hash/realm "slipway" + ::hash/login-service "hash" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints}) (defmethod authentication :hash-basic [_] - (merge - #::security{:handler "hash"} - #::hash{:realm "slipway" - :login-service "hash" - :user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (BasicAuthenticator.) - :constraint-mappings app/constraints})) + {::security/handler "hash" + ::hash/realm "slipway" + ::hash/login-service "hash" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints}) (defn stop! [] From 681665746b0ade0f30dbec2a4e8262add64f6438 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 15:47:20 +1000 Subject: [PATCH 13/25] rm :pre check --- src/slipway/server.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/slipway/server.clj b/src/slipway/server.clj index 5e036b27..47cf2fa2 100644 --- a/src/slipway/server.clj +++ b/src/slipway/server.clj @@ -18,7 +18,6 @@ (defn create-server ^Server [ring-handler {::keys [connectors thread-pool scheduler buffer-pool error-handler] :as opts}] - {:pre [connectors]} (log/debugf "creating server %s" opts) (let [server (Server. ^ThreadPool thread-pool ^Scheduler scheduler ^ByteBufferPool buffer-pool)] (.setConnectors server (into-array Connector (map #(connector server %) connectors))) From 61fcd7b1329258a0c5c976c6df13c4531c42bc18 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 21:17:07 +1000 Subject: [PATCH 14/25] explicit context-handler as default, and support ContextHandlerCollection --- src/slipway.clj | 17 ++++++---- src/slipway/{handler.clj => context.clj} | 42 +++++++++++++++--------- src/slipway/server.clj | 8 ++--- test/slipway/test_server.clj | 7 ++-- 4 files changed, 45 insertions(+), 29 deletions(-) rename src/slipway/{handler.clj => context.clj} (53%) diff --git a/src/slipway.clj b/src/slipway.clj index 6cc2fc9d..965f1838 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -2,7 +2,7 @@ (:require [clojure.tools.logging :as log] [slipway.connector.http] [slipway.connector.https] - [slipway.handler] + [slipway.context] [slipway.security] [slipway.server :as server] [slipway.user] @@ -63,7 +63,7 @@ #:slipway.security{:handler "identifies a SecurityHandler impl, 'jaas', 'hash', and 'openid' supported by default"} - #:slipway.security.hash{:realm "optional Jetty authentication realm" + #:slipway.security.hash{:realm "an (optional) Jetty authentication realm" :user-file "the path to a Jetty hash-user file" :users "a sequence of [^String user-name, ^String credential, ^String[] [roles]]" :authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)" @@ -99,10 +99,13 @@ :max-outgoing-frames "max websocket frames waiting to be sent per session, default -1" :auto-fragment "websocket auto fragment (boolean), default true"} - #:slipway.handler{:context-path "the root context path, default '/'" - :null-path-info? "true if /path is not redirected to /path/, default true"} + #:slipway.context{:ring-handler "the ring-handler descendant of this context-handler" + :context-path "the root context path, default '/'" + :null-path-info? "true if /path is not redirected to /path/, default true" + :virtual-hosts "a list of ^String virtual hosts for the context" + :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"} - #:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)" + #:slipway.server{:handler "the handler impl dispatch-val (:default defmethod found in slipway.context)" :connectors "the connectors supported by this server" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" @@ -112,9 +115,9 @@ #:slipway{:join? "join the Jetty threadpool, blocks the calling thread until jetty exits, default false"}) (defn start ^Server - [ring-handler {::keys [join?] :as opts}] + [{::keys [join?] :as opts}] (log/debugf "starting jetty server %s" opts) - (let [server (server/create-server ring-handler opts)] + (let [server (server/create-server opts)] (.start server) (when join? (log/debug "joining jetty thread") diff --git a/src/slipway/handler.clj b/src/slipway/context.clj similarity index 53% rename from src/slipway/handler.clj rename to src/slipway/context.clj index ad2a425a..1e0e4ad4 100644 --- a/src/slipway/handler.clj +++ b/src/slipway/context.clj @@ -1,4 +1,4 @@ -(ns slipway.handler +(ns slipway.context (:require [clojure.tools.logging :as log] [slipway.handler.compression :as compression] [slipway.security :as security] @@ -7,21 +7,19 @@ [slipway.websockets :as websockets]) (:import (org.eclipse.jetty.security SecurityHandler) (org.eclipse.jetty.server Handler Server) - (org.eclipse.jetty.server.handler ContextHandler) + (org.eclipse.jetty.server.handler ContextHandler ContextHandlerCollection) (slipway.handler SyncHandler))) -(comment - #:slipway.handler{:context-path "the root context path, default '/'" - :null-path-info? "true if /path is not redirected to /path/, default true"}) - -(defmethod server/handler :default - [^Server server ring-handler {::keys [context-path null-path-info?] - :or {context-path "/"} - :as opts}] - (log/debugf "creating default server context-handler, context path %s, null-path-info? %s" context-path null-path-info?) - (let [context-handler (doto (ContextHandler.) - (.setContextPath context-path) - (.setAllowNullPathInContext (not (false? null-path-info?)))) +(defn handler + [^Server server {::keys [ring-handler context-path null-path-info? virtual-hosts] + :or {context-path "/"} + :as opts}] + (log/debugf "creating context-handler, context-path %s, null-path-info? %s" context-path null-path-info?) + (let [context-handler (let [ctx (ContextHandler.)] + (.setContextPath ctx context-path) + (.setAllowNullPathInContext ctx (not (false? null-path-info?))) + (some->> virtual-hosts (.setVirtualHosts ctx)) + ctx) app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) (SyncHandler. ring-handler nil)) @@ -34,4 +32,18 @@ (doto compression-handler (.setHandler (or auth-handler app-handler))) (or auth-handler app-handler))] (.setHandler context-handler ^Handler handler) - context-handler)) \ No newline at end of file + context-handler)) + +(comment + #:slipway.context{:ring-handler "the ring-handler descendant of this context-handler" + :context-path "the root context path, default '/'" + :null-path-info? "true if /path is not redirected to /path/, default true" + :virtual-hosts "a list of ^String virtual hosts for the context" + :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"}) + +(defmethod server/handler :default + [^Server server {::keys [handlers] + :as opts}] + (if handlers + (ContextHandlerCollection. (map handler handlers)) + (handler server opts))) \ No newline at end of file diff --git a/src/slipway/server.clj b/src/slipway/server.clj index 47cf2fa2..adfec816 100644 --- a/src/slipway/server.clj +++ b/src/slipway/server.clj @@ -4,12 +4,12 @@ (org.eclipse.jetty.server Connector Handler Server) (org.eclipse.jetty.util.thread Scheduler ThreadPool))) -(defmulti handler (fn [_server _ring_handler opts] (::handler opts))) +(defmulti handler (fn [_server opts] (::handler opts))) (defmulti connector (fn [_server opts] (keyword (namespace (first (keys opts))) "connector"))) (comment - #:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)" + #:slipway.server{:handler "the handler impl dispatch-val (:default defmethod found in slipway.context)" :connectors "the connectors supported by this server" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" @@ -17,10 +17,10 @@ :error-handler "the error-handler used by this server for Jetty level errors"}) (defn create-server ^Server - [ring-handler {::keys [connectors thread-pool scheduler buffer-pool error-handler] :as opts}] + [{::keys [connectors thread-pool scheduler buffer-pool error-handler] :as opts}] (log/debugf "creating server %s" opts) (let [server (Server. ^ThreadPool thread-pool ^Scheduler scheduler ^ByteBufferPool buffer-pool)] (.setConnectors server (into-array Connector (map #(connector server %) connectors))) + (.setHandler server ^Handler (handler server opts)) (some->> error-handler (.setErrorHandler server)) - (.setHandler server ^Handler (handler server ring-handler opts)) server)) \ No newline at end of file diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index e18f0c55..26410db4 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -137,6 +137,7 @@ (start! keys nil)) ([keys auth] (stop!) - (reset! state (slipway/start (app/handler) - (merge (reduce (fn [ret k] (merge ret (get options k))) {} keys) - (authentication auth)))))) \ No newline at end of file + (reset! state (slipway/start + (merge {:slipway.context/ring-handler (app/handler)} + (reduce (fn [ret k] (merge ret (get options k))) {} keys) + (authentication auth)))))) \ No newline at end of file From 57f54d702e95788877bcedb0d95c9f78c1d02072 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 21:31:48 +1000 Subject: [PATCH 15/25] fix kondo --- src/slipway/context.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slipway/context.clj b/src/slipway/context.clj index 1e0e4ad4..3d827bdf 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -45,5 +45,5 @@ [^Server server {::keys [handlers] :as opts}] (if handlers - (ContextHandlerCollection. (map handler handlers)) + (ContextHandlerCollection. (map (partial handler server) handlers)) (handler server opts))) \ No newline at end of file From e5e25787a965c2d342946ba1f757c5414c373284 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 21:45:04 +1000 Subject: [PATCH 16/25] refactor compression ns --- README.md | 126 +++++++++++------- src/slipway.clj | 10 +- src/slipway/{handler => }/compression.clj | 4 +- src/slipway/context.clj | 2 +- .../{handler => }/compression_test.clj | 4 +- test/slipway/test_server.clj | 2 +- 6 files changed, 86 insertions(+), 62 deletions(-) rename src/slipway/{handler => }/compression.clj (92%) rename test/slipway/{handler => }/compression_test.clj (97%) diff --git a/README.md b/README.md index 64f158fe..f076afac 100644 --- a/README.md +++ b/README.md @@ -10,29 +10,29 @@ * [Introduction](#introduction) * [Why Jetty?](#why-jetty) * [Why Slipway?](#why-slipway) - * [Requirements](#requirements) - * [Primary Goals](#primary-goals) - * [Secondary Goals](#secondary-goals) - * [Out of Scope](#out-of-scope) - * [Non-Goals](#non-goals) + * [Requirements](#requirements) + * [Primary Goals](#primary-goals) + * [Secondary Goals](#secondary-goals) + * [Out of Scope](#out-of-scope) + * [Non-Goals](#non-goals) * [Using Slipway](#using-slipway) - * [Quick Start](#quick-start) - * [Example Servers](#example-servers) + * [Quick Start](#quick-start) + * [Example Servers](#example-servers) * [Configuring Slipway](#configuring-slipway) - * [:slipway](#slipway) - * [:slipway.server](#slipwayserver) - * [:slipway.handler](#slipwayhandler) - * [:slipway.websockets](#slipwaywebsockets) - * [:slipway.session](#slipwaysession) - * [:slipway.security](#slipwaysecurity) - * [:slipway.connector.http](#slipwayconnectorhttp) - * [:slipway.connector.https](#slipwayconnectorhttps) - * [:slipway.handler.compression](#slipwayhandlercompression) + * [:slipway](#slipway) + * [:slipway.server](#slipwayserver) + * [:slipway.context](#slipwaycontext) + * [:slipway.websockets](#slipwaywebsockets) + * [:slipway.session](#slipwaysession) + * [:slipway.security](#slipwaysecurity) + * [:slipway.connector.http](#slipwayconnectorhttp) + * [:slipway.connector.https](#slipwayconnectorhttps) + * [:slipway.compression](#slipwaycompression) * [Sente Websockets](#sente-websockets) * [JAAS Authentication](#jaas-authentication) - * [-Djava.security.auth.login.config](#-djavasecurityauthloginconfig) - * [Hash Authentication](#hash-authentication) - * [LDAP Authentication](#ldap-authentication) + * [-Djava.security.auth.login.config](#-djavasecurityauthloginconfig) + * [Hash Authentication](#hash-authentication) + * [LDAP Authentication](#ldap-authentication) * [License](#license) * [Contributing](#contributing) @@ -42,19 +42,25 @@ # Introduction -[Eclipse Jetty](https://www.eclipse.org/jetty/) is the web server at the heart of our product, [Kpow for Apache Kafka®](https://factorhouse.io/kpow). +[Eclipse Jetty](https://www.eclipse.org/jetty/) is the web server at the heart of our +product, [Kpow for Apache Kafka®](https://factorhouse.io/kpow). Slipway is a [Clojure](https://clojure.org/) companion to embedded Jetty 12.1 with WebSocket support. -Slipway configuration models Jetty instead of exposing a simplified DSL. This approach allows leverage of all Jetty capabilities while providing sensible defaults for basic behaviour. If in doubt, read the [Jetty docs](https://jetty.org/docs/). +Slipway configuration models Jetty instead of exposing a simplified DSL. This approach allows leverage of all Jetty +capabilities while providing sensible defaults for basic behaviour. If in doubt, read +the [Jetty docs](https://jetty.org/docs/). -Use the [Community Edition](https://kpow.io/get-started/) of Kpow with our [local-repo](https://github.com/factorhouse/kpow-local) to see Slipway in action. +Use the [Community Edition](https://kpow.io/get-started/) of Kpow with +our [local-repo](https://github.com/factorhouse/kpow-local) to see Slipway in action. -> **Archived versions**: Previous support for Jetty 9, 10, and 11 is preserved in the [`archive/`](archive/) directory but is no longer maintained. Slipway 2.x targets Jetty 12.1 exclusively. +> **Archived versions**: Previous support for Jetty 9, 10, and 11 is preserved in the [`archive/`](archive/) directory +> but is no longer maintained. Slipway 2.x targets Jetty 12.1 exclusively. ## Why Jetty? -Jetty is a mature, stable, commercially supported project with an [active, experienced](https://github.com/eclipse/jetty.project/graphs/contributors) team of core contributors. +Jetty is a mature, stable, commercially supported project with +an [active, experienced](https://github.com/eclipse/jetty.project/graphs/contributors) team of core contributors. Ubiquitous in the enterprise Java world, Jetty has many eyes raising issues and driving improvement. @@ -117,7 +123,9 @@ Add `io.factorhouse/slipway-jetty12` to your project dependencies: [io.factorhouse/slipway-jetty12 "2.0.6"] ``` -Requires Java 17+. +## JVM Requirement + +Slipway (and Jetty 12.1) Requires Java 17+. ### Quick Start @@ -145,9 +153,11 @@ To stop the server: ### Example Servers -The [`test/slipway/test_server.clj`](test/slipway/test_server.clj) namespace contains a range of example server configurations for use in development and testing. +The [`test/slipway/test_server.clj`](test/slipway/test_server.clj) namespace contains a range of example server +configurations for use in development and testing. -The stateful `start!`/`stop!` functions are a convenience for integration tests and local development, not canonical Slipway usage. +The stateful `start!`/`stop!` functions are a convenience for integration tests and local development, not canonical +Slipway usage. ```clojure (require '[slipway.test-server :as test-server]) @@ -167,7 +177,8 @@ The stateful `start!`/`stop!` functions are a convenience for integration tests Your sample application is available on [http://localhost:3000](http://localhost:3000). -For hash auth, login with `jetty/jetty`, `admin/admin`, `plain/plain`, `other/other`, or `user/password` as defined in [hash-realm.properties](dev-resources/jaas/hash-realm.properties). +For hash auth, login with `jetty/jetty`, `admin/admin`, `plain/plain`, `other/other`, or `user/password` as defined +in [hash-realm.properties](dev-resources/jaas/hash-realm.properties). After login, the default home page presents useful links for user info and error pages. @@ -181,7 +192,8 @@ Jetty is sophisticated as it addresses a complex domain with flexibility and con Slipway holds close to Jetty idioms for configuration rather than presenting a simplified DSL. -Slipway takes a single map of namespaced configuration. Namespaces correspond to Jetty domain models and can be considered as separate maps then merged. +Slipway takes a single map of namespaced configuration. Namespaces correspond to Jetty domain models and can be +considered as separate maps then merged. ### :slipway @@ -196,7 +208,7 @@ The top-level namespace determines whether Slipway joins the Jetty thread pool. Configuration of core server options. ```clojure -#:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)" +#:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.context)" :connectors "the connectors supported by this server" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" @@ -206,9 +218,11 @@ Configuration of core server options. #### :slipway.server/handler -Slipway provides a default server-handler implementation via a `defmethod` dispatch in [`src/slipway/handler.clj`](src/slipway/handler.clj). +Slipway provides a default server-handler implementation via a `defmethod` dispatch in [ +`src/slipway/handler.clj`](src/slipway/handler.clj). -Use a custom server-handler by implementing a new `server/handler` defmethod and providing its dispatch key as `::server/handler`. +Use a custom server-handler by implementing a new `server/handler` defmethod and providing its dispatch key as +`::server/handler`. #### :slipway.server/connectors @@ -235,7 +249,8 @@ Slipway accepts a list of server connectors, allowing multi-connector setups, e. #### :slipway.server/error-handler -Provide a concrete `org.eclipse.jetty.server.handler.ErrorHandler` to manage Jetty-level errors (not to be confused with ring/application-level errors handled within your application). +Provide a concrete `org.eclipse.jetty.server.handler.ErrorHandler` to manage Jetty-level errors (not to be confused with +ring/application-level errors handled within your application). Slipway provides a utility for creating custom error handlers in [`src/slipway/error.clj`](src/slipway/error.clj): @@ -249,13 +264,16 @@ Slipway provides a utility for creating custom error handlers in [`src/slipway/e (def my-error-handler (error/handler body-fn)) ``` -### :slipway.handler +### :slipway.context -Configuration of the default server handler. +Configuration of the default server context-handler. ```clojure -#:slipway.handler{:context-path "the root context path, default '/'" - :null-path-info? "true if /path is not redirected to /path/, default true"} +#:slipway.context{:ring-handler "the ring-handler descendant of this context-handler" + :context-path "the root context path, default '/'" + :null-path-info? "true if /path is not redirected to /path/, default true" + :virtual-hosts "a list of ^String virtual hosts for the context" + :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"} ``` ### :slipway.websockets @@ -319,7 +337,8 @@ Configure simple authentication with Jetty's built in HashLoginService #### :slipway.security.jaas -Configure JAAS authentication with Jetty's built in [JAAS compatible login-modules](https://jetty.org/docs/jetty/12.1/operations-guide/security/jaas-support.html) +Configure JAAS authentication with Jetty's built +in [JAAS compatible login-modules](https://jetty.org/docs/jetty/12.1/operations-guide/security/jaas-support.html) ```clojure #:slipway.security.jaas{:realm "the Jetty authentication realm" @@ -397,22 +416,22 @@ Configuration of an HTTPS server connector. :http-compliance "set the HttpCompliance mode, defaults to HttpCompliance/RFC9110"} ``` -### :slipway.handler.compression +### :slipway.compression -Configuration of the compression handler. Replaces the former `:slipway.handler.gzip` namespace from Slipway 1.x. +Configuration of the compression handler. ```clojure -#:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" - :path-spec "the compression path-spec, default '/*'" - :format "compression format, defaults to :gzip" - :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} +#:slipway.compression{:enabled? "is a compression handler enabled? default true" + :path-spec "the compression path-spec, default '/*'" + :format "compression format, defaults to :gzip" + :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} ``` The `:format` key dispatches via `defmulti` — extend it to add custom compression formats: ```clojure -(require '[slipway.handler.compression :as compression]) +(require '[slipway.compression :as compression]) (import '[your.org.YourCompression]) (defmethod compression/format :my-format [_opts] @@ -453,11 +472,13 @@ JAAS implements a Java version of the standard Pluggable Authentication Module ( JAAS can be used for two purposes: * for authentication of users, to reliably and securely determine who is currently executing Java code -* for authorization of users to ensure they have the access control rights (permissions) required to do the actions performed. +* for authorization of users to ensure they have the access control rights (permissions) required to do the actions + performed. For more information visit the [Jetty documentation](https://jetty.org/docs/jetty/12/operations-guide/jaas/index.html). -Various configurations of Slipway with JAAS auth can be found in the [`test_server.clj`](test/slipway/test_server.clj) namespace. +Various configurations of Slipway with JAAS auth can be found in the [`test_server.clj`](test/slipway/test_server.clj) +namespace. #### -Djava.security.auth.login.config @@ -465,7 +486,8 @@ Start your application (JAR or REPL session) with the additional JVM option: `-Djava.security.auth.login.config=/some/path/to/jaas.config` -For example configurations refer to [this tutorial](https://wiki.eclipse.org/Jetty/Tutorial/JAAS#Configuring_a_JAASLoginService). +For example configurations refer +to [this tutorial](https://wiki.eclipse.org/Jetty/Tutorial/JAAS#Configuring_a_JAASLoginService). #### Hash Authentication @@ -526,9 +548,11 @@ my-realm { ## Contributing -We are very welcoming of any bug tickets and/or minor fixes, but we do not currently welcome larger functional contributions. +We are very welcoming of any bug tickets and/or minor fixes, but we do not currently welcome larger functional +contributions. -Slipway is at the heart of our commercial software and as such we take a conservative approach to modelling Jetty's capabilities. +Slipway is at the heart of our commercial software and as such we take a conservative approach to modelling Jetty's +capabilities. ## License diff --git a/src/slipway.clj b/src/slipway.clj index 965f1838..ef81d0ec 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -10,11 +10,11 @@ (:import (org.eclipse.jetty.server Server))) (comment - #:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" - :path-spec "the compression path-spec, default '/*'" - :format "compression format, defaults to :gzip" - :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} + #:slipway.compression{:enabled? "is a compression handler enabled? default true" + :path-spec "the compression path-spec, default '/*'" + :format "compression format, defaults to :gzip" + :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"} #:slipway.connector.https{:name "the name of this connector (useful for VirtualHosts configuration)" :host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces" diff --git a/src/slipway/handler/compression.clj b/src/slipway/compression.clj similarity index 92% rename from src/slipway/handler/compression.clj rename to src/slipway/compression.clj index 4c1d9de0..b49f1f75 100644 --- a/src/slipway/handler/compression.clj +++ b/src/slipway/compression.clj @@ -1,11 +1,11 @@ -(ns slipway.handler.compression +(ns slipway.compression (:refer-clojure :exclude [format]) (:require [clojure.tools.logging :as log]) (:import (org.eclipse.jetty.compression.gzip GzipCompression) (org.eclipse.jetty.compression.server CompressionConfig CompressionHandler))) (comment - #:slipway.handler.compression{:enabled? "is a compression handler enabled? default true" + #:slipway.compression{:enabled? "is a compression handler enabled? default true" :path-spec "the compression path-spec, default '/*'" :format "compression format, defaults to :gzip" :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" diff --git a/src/slipway/context.clj b/src/slipway/context.clj index 3d827bdf..f1ac8c01 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -1,6 +1,6 @@ (ns slipway.context (:require [clojure.tools.logging :as log] - [slipway.handler.compression :as compression] + [slipway.compression :as compression] [slipway.security :as security] [slipway.server :as server] [slipway.session :as session] diff --git a/test/slipway/handler/compression_test.clj b/test/slipway/compression_test.clj similarity index 97% rename from test/slipway/handler/compression_test.clj rename to test/slipway/compression_test.clj index cf44a1d0..09e210d3 100644 --- a/test/slipway/handler/compression_test.clj +++ b/test/slipway/compression_test.clj @@ -1,6 +1,6 @@ -(ns slipway.handler.compression-test +(ns slipway.compression-test (:require [clojure.test :refer [deftest is testing]] - [slipway.handler.compression :as compression]) + [slipway.compression :as compression]) (:import (org.eclipse.jetty.compression.gzip GzipCompression) (org.eclipse.jetty.compression.server CompressionConfig CompressionHandler))) diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 26410db4..6aa0cf65 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -5,7 +5,7 @@ [slipway.connector.http :as http] [slipway.connector.https :as https] [slipway.example.app :as app] - [slipway.handler.compression :as compression] + [slipway.compression :as compression] [slipway.security :as security] [slipway.security.hash :as hash] [slipway.security.jaas :as jaas] From 3f36706b397d8e85db2ab6db3924aca0680a31dd Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 21:46:45 +1000 Subject: [PATCH 17/25] fix format --- src/slipway/compression.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/slipway/compression.clj b/src/slipway/compression.clj index b49f1f75..f4774b0b 100644 --- a/src/slipway/compression.clj +++ b/src/slipway/compression.clj @@ -6,10 +6,10 @@ (comment #:slipway.compression{:enabled? "is a compression handler enabled? default true" - :path-spec "the compression path-spec, default '/*'" - :format "compression format, defaults to :gzip" - :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" - :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"}) + :path-spec "the compression path-spec, default '/*'" + :format "compression format, defaults to :gzip" + :compress-min-bytes "min response size to trigger compression (default 1024 bytes)" + :compression-config "a concrete Jetty CompressionConfig instance (nil for default configuration)"}) (defmulti format ::format) From 57cfc43970c367b31f5b2fd869f281107a656104 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 21:54:03 +1000 Subject: [PATCH 18/25] update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f076afac..cbe1018a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ## Slipway by [Factor House](https://factorhouse.io) * [Introduction](#introduction) + * [Archived Versions](#archived-versions) * [Why Jetty?](#why-jetty) * [Why Slipway?](#why-slipway) * [Requirements](#requirements) @@ -16,6 +17,7 @@ * [Out of Scope](#out-of-scope) * [Non-Goals](#non-goals) * [Using Slipway](#using-slipway) + * [JVM Version Support](#jvm-version-support) * [Quick Start](#quick-start) * [Example Servers](#example-servers) * [Configuring Slipway](#configuring-slipway) @@ -54,8 +56,10 @@ the [Jetty docs](https://jetty.org/docs/). Use the [Community Edition](https://kpow.io/get-started/) of Kpow with our [local-repo](https://github.com/factorhouse/kpow-local) to see Slipway in action. -> **Archived versions**: Previous support for Jetty 9, 10, and 11 is preserved in the [`archive/`](archive/) directory -> but is no longer maintained. Slipway 2.x targets Jetty 12.1 exclusively. +### Archived Versions + +Previous support for Jetty 9, 10, and 11 is preserved in the [`archive/`](archive/) directory +but is no longer maintained. Slipway 2.x targets Jetty 12.1 exclusively. ## Why Jetty? @@ -123,7 +127,7 @@ Add `io.factorhouse/slipway-jetty12` to your project dependencies: [io.factorhouse/slipway-jetty12 "2.0.6"] ``` -## JVM Requirement +### JVM Version Support Slipway (and Jetty 12.1) Requires Java 17+. @@ -353,10 +357,10 @@ Example constraint mapping: (import '[org.eclipse.jetty.security Constraint]) (def constraints - [["/up" Constraint/ALLOWED] + [["/up" Constraint/ALLOWED] ["/css/*" Constraint/ALLOWED] ["/img/*" Constraint/ALLOWED] - ["/*" Constraint/ANY_USER]]) + ["/*" Constraint/ANY_USER]]) ``` ### :slipway.connector.http From 124cb7e3345de59af15d5068d768f60d706d0026 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Tue, 16 Jun 2026 23:17:16 +1000 Subject: [PATCH 19/25] add error-handler to context-handler --- src/slipway.clj | 1 + src/slipway/context.clj | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slipway.clj b/src/slipway.clj index ef81d0ec..61cc38c4 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -103,6 +103,7 @@ :context-path "the root context path, default '/'" :null-path-info? "true if /path is not redirected to /path/, default true" :virtual-hosts "a list of ^String virtual hosts for the context" + :error-handler "the error-handler used by this context-handler for context level errors" :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"} #:slipway.server{:handler "the handler impl dispatch-val (:default defmethod found in slipway.context)" diff --git a/src/slipway/context.clj b/src/slipway/context.clj index f1ac8c01..9c680964 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -11,7 +11,7 @@ (slipway.handler SyncHandler))) (defn handler - [^Server server {::keys [ring-handler context-path null-path-info? virtual-hosts] + [^Server server {::keys [ring-handler context-path null-path-info? virtual-hosts error-handler] :or {context-path "/"} :as opts}] (log/debugf "creating context-handler, context-path %s, null-path-info? %s" context-path null-path-info?) @@ -19,6 +19,7 @@ (.setContextPath ctx context-path) (.setAllowNullPathInContext ctx (not (false? null-path-info?))) (some->> virtual-hosts (.setVirtualHosts ctx)) + (some->> error-handler (.setErrorHandler ctx)) ctx) app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) @@ -39,6 +40,7 @@ :context-path "the root context path, default '/'" :null-path-info? "true if /path is not redirected to /path/, default true" :virtual-hosts "a list of ^String virtual hosts for the context" + :error-handler "the error-handler used by this context-handler for context level errors" :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"}) (defmethod server/handler :default From 785efbedc0715f139aaf13d2d1fa37533c299b91 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 17:15:58 +1000 Subject: [PATCH 20/25] refactor SyncHandler to simpler ns --- project.clj | 2 +- src/slipway/context.clj | 2 +- src/slipway/{handler => }/sync_handler.clj | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/slipway/{handler => }/sync_handler.clj (92%) diff --git a/project.clj b/project.clj index 523ab61f..c031ebb6 100644 --- a/project.clj +++ b/project.clj @@ -23,7 +23,7 @@ "fmt" ["with-profile" "+pedantic" "cljfmt" "check"] "fmtfix" ["with-profile" "+pedantic" "cljfmt" "fix"]} - :aot [slipway.handler.sync-handler] + :aot [slipway.sync-handler] :dependencies [[org.clojure/clojure "1.12.5"] [org.clojure/tools.logging "1.3.1"] diff --git a/src/slipway/context.clj b/src/slipway/context.clj index 9c680964..d0277166 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -8,7 +8,7 @@ (:import (org.eclipse.jetty.security SecurityHandler) (org.eclipse.jetty.server Handler Server) (org.eclipse.jetty.server.handler ContextHandler ContextHandlerCollection) - (slipway.handler SyncHandler))) + (slipway SyncHandler))) (defn handler [^Server server {::keys [ring-handler context-path null-path-info? virtual-hosts error-handler] diff --git a/src/slipway/handler/sync_handler.clj b/src/slipway/sync_handler.clj similarity index 92% rename from src/slipway/handler/sync_handler.clj rename to src/slipway/sync_handler.clj index 2fca2fbf..44380051 100644 --- a/src/slipway/handler/sync_handler.clj +++ b/src/slipway/sync_handler.clj @@ -1,4 +1,4 @@ -(ns slipway.handler.sync-handler +(ns slipway.sync-handler (:require [clojure.tools.logging :as log] [slipway.request :as request] [slipway.response :as response]) @@ -6,9 +6,9 @@ (org.eclipse.jetty.http.pathmap PathSpec) (org.eclipse.jetty.server Request Response) (org.eclipse.jetty.util Callback) - (slipway.handler SyncHandler)) + (slipway SyncHandler)) (:gen-class - :name slipway.handler.SyncHandler + :name slipway.SyncHandler :extends org.eclipse.jetty.server.Handler$Abstract :state state :init init From fba449c0b876ac24c9c16ae88e4eee52316e2613 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 20:20:00 +1000 Subject: [PATCH 21/25] ::websockets/enabled? false by default, refactor context ns --- README.md | 2 +- src/slipway.clj | 2 +- src/slipway/context.clj | 76 ++++++++++++++++++++++---------- src/slipway/websockets.clj | 9 +++- test/slipway/websockets_test.clj | 24 ++++++++++ 5 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 test/slipway/websockets_test.clj diff --git a/README.md b/README.md index cbe1018a..479dabb8 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ Configuration of the default server context-handler. Configuration of WebSocket options. ```clojure -#:slipway.websockets{:enabled? "are websockets enabled? default true" +#:slipway.websockets{:enabled? "are websockets enabled? default false" :path-spec "the websocket path-spec, default '/chsk'" :idle-timeout-ms "max websocket idle time in ms, default 300000" :input-buffer-bytes "max websocket input buffer size in bytes" diff --git a/src/slipway.clj b/src/slipway.clj index 61cc38c4..2e300e57 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -88,7 +88,7 @@ #:slipway.sente{:options "A map of options passed directly to sente/make-channel-socket-server!"} - #:slipway.websockets{:enabled? "are websockets enabled? default true" + #:slipway.websockets{:enabled? "are websockets enabled? default false" :path-spec "the websocket path-spec, default '/chsk'" :idle-timeout-ms "max websocket idle time, default 300000" :input-buffer-bytes "max websocket input buffer size" diff --git a/src/slipway/context.clj b/src/slipway/context.clj index d0277166..5ce70844 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -10,29 +10,60 @@ (org.eclipse.jetty.server.handler ContextHandler ContextHandlerCollection) (slipway SyncHandler))) +(defn app-handler + [ring-handler opts] + (SyncHandler. ring-handler (websockets/path-spec opts))) + +(defn wrap-websockets + [handler context-handler server ring-handler opts] + (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] + (doto ws-handler (.setHandler handler)) + handler)) + +(defn wrap-auth + [handler opts] + (if-let [^SecurityHandler security-handler (security/handler opts)] + (let [session-handler (session/handler opts)] + (.setHandler security-handler ^Handler app-handler) + (.setHandler session-handler security-handler) + session-handler) + handler)) + +(defn wrap-compression + [handler opts] + (if-let [compression-handler (compression/handler opts)] + (doto compression-handler (.setHandler handler)) + handler)) + +(defn base-handler + [{::keys [context-path null-path-info? virtual-hosts error-handler] + :or {context-path "/"}}] + (log/debugf "creating context-handler, context-path %s, null-path-info? %s" context-path null-path-info?) + (let [context-handler (ContextHandler.)] + (.setContextPath context-handler context-path) + (.setAllowNullPathInContext context-handler (not (false? null-path-info?))) + (some->> virtual-hosts (.setVirtualHosts context-handler)) + (some->> error-handler (.setErrorHandler context-handler)) + context-handler)) + (defn handler - [^Server server {::keys [ring-handler context-path null-path-info? virtual-hosts error-handler] - :or {context-path "/"} + "Request routing is handled in the following order: + -> Request + -> ContextHandler + -> CompressionHandler (optional) + -> SessionHandler (optional) + -> SecurityHandler (optional) + -> WebsocketHandler (optional) + -> SyncHandler + -> ring-handler" + [^Server server {::keys [ring-handler] :as opts}] - (log/debugf "creating context-handler, context-path %s, null-path-info? %s" context-path null-path-info?) - (let [context-handler (let [ctx (ContextHandler.)] - (.setContextPath ctx context-path) - (.setAllowNullPathInContext ctx (not (false? null-path-info?))) - (some->> virtual-hosts (.setVirtualHosts ctx)) - (some->> error-handler (.setErrorHandler ctx)) - ctx) - app-handler (if-let [ws-handler (websockets/handler server context-handler ring-handler opts)] - (doto ws-handler (.setHandler (SyncHandler. ring-handler (::websockets/path-spec opts)))) - (SyncHandler. ring-handler nil)) - auth-handler (when-let [^SecurityHandler security-handler (security/handler opts)] - (let [session-handler (session/handler opts)] - (.setHandler security-handler ^Handler app-handler) - (.setHandler session-handler security-handler) - session-handler)) - handler (if-let [compression-handler (compression/handler opts)] - (doto compression-handler (.setHandler (or auth-handler app-handler))) - (or auth-handler app-handler))] - (.setHandler context-handler ^Handler handler) + (let [context-handler (base-handler opts) + application-handler (-> (app-handler ring-handler opts) + (wrap-websockets context-handler server ring-handler opts) + (wrap-auth opts) + (wrap-compression opts))] + (.setHandler context-handler ^Handler application-handler) context-handler)) (comment @@ -44,8 +75,7 @@ :handlers "an (optional) sequence of [#:slipway.context] for a ContextHandlerCollection"}) (defmethod server/handler :default - [^Server server {::keys [handlers] - :as opts}] + [^Server server {::keys [handlers] :as opts}] (if handlers (ContextHandlerCollection. (map (partial handler server) handlers)) (handler server opts))) \ No newline at end of file diff --git a/src/slipway/websockets.clj b/src/slipway/websockets.clj index 12f87c24..724ed89e 100644 --- a/src/slipway/websockets.clj +++ b/src/slipway/websockets.clj @@ -40,8 +40,13 @@ (do (response/update-response request response handshake) (.succeeded cb))))))) +(defn path-spec + [{::keys [enabled? path-spec] + :or {path-spec "/chsk"}}] + (when enabled? path-spec)) + (comment - #:slipway.websockets{:enabled? "are websockets enabled? default true" + #:slipway.websockets{:enabled? "are websockets enabled? default false" :path-spec "the websocket path-spec, default '/chsk'" :idle-timeout-ms "max websocket idle time, default 300000" :input-buffer-bytes "max websocket input buffer size" @@ -58,7 +63,7 @@ max-binary-message-bytes max-frame-bytes max-outgoing-frames auto-fragment] :or {path-spec "/chsk" idle-timeout-ms 300000}} opts] - (when (not (false? enabled?)) + (when enabled? (log/debugf "configuring websockets at %s with %s" path-spec opts) (WebSocketUpgradeHandler/from server diff --git a/test/slipway/websockets_test.clj b/test/slipway/websockets_test.clj new file mode 100644 index 00000000..1d9c159a --- /dev/null +++ b/test/slipway/websockets_test.clj @@ -0,0 +1,24 @@ +(ns slipway.websockets-test + (:require [clojure.test :refer [deftest is testing]] + [slipway.websockets :as websockets])) + +(deftest path-spec + + (testing "not set" + (is (= nil (websockets/path-spec nil))) + (is (= nil (websockets/path-spec {})))) + + (testing "not enabled" + (is (= nil (websockets/path-spec {::websockets/path-spec "/some-path"}))) + (is (= nil (websockets/path-spec {::websockets/enabled? nil + ::websockets/path-spec "/some-path"}))) + (is (= nil (websockets/path-spec {::websockets/enabled? false + ::websockets/path-spec "/some-path"})))) + + (testing "default path-spec" + (is (= "/chsk" (websockets/path-spec {::websockets/enabled? true})))) + + (testing "specific path-spec" + (is (= "/some-path" (websockets/path-spec {::websockets/enabled? true + ::websockets/path-spec "/some-path"}))))) + From 24fba00a13dfa146df3b277a61a011f458ddb488 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 20:20:30 +1000 Subject: [PATCH 22/25] minor nit --- src/slipway/context.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/slipway/context.clj b/src/slipway/context.clj index 5ce70844..3f55a058 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -56,8 +56,7 @@ -> WebsocketHandler (optional) -> SyncHandler -> ring-handler" - [^Server server {::keys [ring-handler] - :as opts}] + [^Server server {::keys [ring-handler] :as opts}] (let [context-handler (base-handler opts) application-handler (-> (app-handler ring-handler opts) (wrap-websockets context-handler server ring-handler opts) From b578a8bf7e6853cbfa6f280de1468716143387df Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 20:24:46 +1000 Subject: [PATCH 23/25] fix tests --- src/slipway/context.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slipway/context.clj b/src/slipway/context.clj index 3f55a058..40f0a56e 100644 --- a/src/slipway/context.clj +++ b/src/slipway/context.clj @@ -24,7 +24,7 @@ [handler opts] (if-let [^SecurityHandler security-handler (security/handler opts)] (let [session-handler (session/handler opts)] - (.setHandler security-handler ^Handler app-handler) + (.setHandler security-handler ^Handler handler) (.setHandler session-handler security-handler) session-handler) handler)) From 79361d32e9de1da2566a90fdfc3230e314160f46 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 20:29:41 +1000 Subject: [PATCH 24/25] fix tests --- test/slipway/test_server.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj index 6aa0cf65..49da1351 100644 --- a/test/slipway/test_server.clj +++ b/test/slipway/test_server.clj @@ -2,10 +2,10 @@ "This ns contains helper functions for stopping/starting test servers. Feel free to add any further configuration in the same style." (:require [slipway :as slipway] + [slipway.compression :as compression] [slipway.connector.http :as http] [slipway.connector.https :as https] [slipway.example.app :as app] - [slipway.compression :as compression] [slipway.security :as security] [slipway.security.hash :as hash] [slipway.security.jaas :as jaas] @@ -47,7 +47,8 @@ {:http #::server{:connectors [http-connector] :error-handler app/server-error-handler} - :websockets #::websockets{:path-spec "/chsk"} + :websockets #::websockets{:enabled? true + :path-spec "/chsk"} :https #::server{:connectors [https-connector] :error-handler app/server-error-handler} From 4f341dd0e52a53dc8bea9d6ad870647d59553332 Mon Sep 17 00:00:00 2001 From: Derek Troy-West Date: Wed, 17 Jun 2026 22:00:43 +1000 Subject: [PATCH 25/25] split tests --- project.clj | 2 +- test/{ => integration}/slipway/example/app.clj | 0 test/{ => integration}/slipway/example/html.clj | 0 test/{ => integration}/slipway/server_forwarded_test.clj | 0 test/{ => integration}/slipway/server_http_test.clj | 0 test/{ => integration}/slipway/server_https_test.clj | 0 test/{ => integration}/slipway/server_proxied_test.clj | 0 test/{ => integration}/slipway/test_client.clj | 0 test/{ => integration}/slipway/test_server.clj | 0 test/{ => integration}/slipway/websockets_http_test.clj | 0 test/{ => integration}/slipway/websockets_https_test.clj | 0 test/{ => unit}/slipway/compression_test.clj | 0 test/{ => unit}/slipway/connector/http_test.clj | 0 test/{ => unit}/slipway/security/hash_test.clj | 0 test/{ => unit}/slipway/session_test.clj | 0 test/{ => unit}/slipway/websockets_test.clj | 0 16 files changed, 1 insertion(+), 1 deletion(-) rename test/{ => integration}/slipway/example/app.clj (100%) rename test/{ => integration}/slipway/example/html.clj (100%) rename test/{ => integration}/slipway/server_forwarded_test.clj (100%) rename test/{ => integration}/slipway/server_http_test.clj (100%) rename test/{ => integration}/slipway/server_https_test.clj (100%) rename test/{ => integration}/slipway/server_proxied_test.clj (100%) rename test/{ => integration}/slipway/test_client.clj (100%) rename test/{ => integration}/slipway/test_server.clj (100%) rename test/{ => integration}/slipway/websockets_http_test.clj (100%) rename test/{ => integration}/slipway/websockets_https_test.clj (100%) rename test/{ => unit}/slipway/compression_test.clj (100%) rename test/{ => unit}/slipway/connector/http_test.clj (100%) rename test/{ => unit}/slipway/security/hash_test.clj (100%) rename test/{ => unit}/slipway/session_test.clj (100%) rename test/{ => unit}/slipway/websockets_test.clj (100%) diff --git a/project.clj b/project.clj index c031ebb6..98128203 100644 --- a/project.clj +++ b/project.clj @@ -39,6 +39,6 @@ [org.eclipse.jetty.compression/jetty-compression-gzip "12.1.10"]] :source-paths ["src"] - :test-paths ["test"] + :test-paths ["test/unit" "test/integration"] :javac-options ["--release" "17"]) diff --git a/test/slipway/example/app.clj b/test/integration/slipway/example/app.clj similarity index 100% rename from test/slipway/example/app.clj rename to test/integration/slipway/example/app.clj diff --git a/test/slipway/example/html.clj b/test/integration/slipway/example/html.clj similarity index 100% rename from test/slipway/example/html.clj rename to test/integration/slipway/example/html.clj diff --git a/test/slipway/server_forwarded_test.clj b/test/integration/slipway/server_forwarded_test.clj similarity index 100% rename from test/slipway/server_forwarded_test.clj rename to test/integration/slipway/server_forwarded_test.clj diff --git a/test/slipway/server_http_test.clj b/test/integration/slipway/server_http_test.clj similarity index 100% rename from test/slipway/server_http_test.clj rename to test/integration/slipway/server_http_test.clj diff --git a/test/slipway/server_https_test.clj b/test/integration/slipway/server_https_test.clj similarity index 100% rename from test/slipway/server_https_test.clj rename to test/integration/slipway/server_https_test.clj diff --git a/test/slipway/server_proxied_test.clj b/test/integration/slipway/server_proxied_test.clj similarity index 100% rename from test/slipway/server_proxied_test.clj rename to test/integration/slipway/server_proxied_test.clj diff --git a/test/slipway/test_client.clj b/test/integration/slipway/test_client.clj similarity index 100% rename from test/slipway/test_client.clj rename to test/integration/slipway/test_client.clj diff --git a/test/slipway/test_server.clj b/test/integration/slipway/test_server.clj similarity index 100% rename from test/slipway/test_server.clj rename to test/integration/slipway/test_server.clj diff --git a/test/slipway/websockets_http_test.clj b/test/integration/slipway/websockets_http_test.clj similarity index 100% rename from test/slipway/websockets_http_test.clj rename to test/integration/slipway/websockets_http_test.clj diff --git a/test/slipway/websockets_https_test.clj b/test/integration/slipway/websockets_https_test.clj similarity index 100% rename from test/slipway/websockets_https_test.clj rename to test/integration/slipway/websockets_https_test.clj diff --git a/test/slipway/compression_test.clj b/test/unit/slipway/compression_test.clj similarity index 100% rename from test/slipway/compression_test.clj rename to test/unit/slipway/compression_test.clj diff --git a/test/slipway/connector/http_test.clj b/test/unit/slipway/connector/http_test.clj similarity index 100% rename from test/slipway/connector/http_test.clj rename to test/unit/slipway/connector/http_test.clj diff --git a/test/slipway/security/hash_test.clj b/test/unit/slipway/security/hash_test.clj similarity index 100% rename from test/slipway/security/hash_test.clj rename to test/unit/slipway/security/hash_test.clj diff --git a/test/slipway/session_test.clj b/test/unit/slipway/session_test.clj similarity index 100% rename from test/slipway/session_test.clj rename to test/unit/slipway/session_test.clj diff --git a/test/slipway/websockets_test.clj b/test/unit/slipway/websockets_test.clj similarity index 100% rename from test/slipway/websockets_test.clj rename to test/unit/slipway/websockets_test.clj