diff --git a/README.md b/README.md index 782e057b..479dabb8 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,33 @@ ## Slipway by [Factor House](https://factorhouse.io) * [Introduction](#introduction) + * [Archived Versions](#archived-versions) * [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) + * [JVM Version Support](#jvm-version-support) + * [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 +44,27 @@ # 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 +127,9 @@ Add `io.factorhouse/slipway-jetty12` to your project dependencies: [io.factorhouse/slipway-jetty12 "2.0.6"] ``` -Requires Java 17+. +### JVM Version Support + +Slipway (and Jetty 12.1) Requires Java 17+. ### Quick Start @@ -145,9 +157,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]) @@ -156,10 +170,10 @@ 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) +(test-server/start! [:http] :hash-basic) ;; Start with HTTP + HTTPS (test-server/start! [:http+https]) @@ -167,7 +181,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 +196,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 +212,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 +222,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 +253,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,14 +268,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 '/'" - :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.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 @@ -264,7 +285,7 @@ Configuration of the default server 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" @@ -300,12 +321,34 @@ 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: @@ -314,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 @@ -325,17 +368,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,58 +387,59 @@ 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 +### :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 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.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]) -(import '[org.eclipse.jetty.compression.gzip GzipCompression]) +(require '[slipway.compression :as compression]) +(import '[your.org.YourCompression]) (defmethod compression/format :my-format [_opts] - (GzipCompression.)) ; substitute your own compression implementation + (YourCompression.)) ; substitute your own compression implementation ``` ## Sente Websockets @@ -431,11 +476,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 @@ -443,7 +490,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 @@ -504,9 +552,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/project.clj b/project.clj index 73024767..98128203 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"] @@ -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"] @@ -34,10 +34,11 @@ [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"]] :source-paths ["src"] - :test-paths ["test"] + :test-paths ["test/unit" "test/integration"] :javac-options ["--release" "17"]) diff --git a/src/slipway.clj b/src/slipway.clj index 73ef0320..2e300e57 100644 --- a/src/slipway.clj +++ b/src/slipway.clj @@ -2,21 +2,22 @@ (:require [clojure.tools.logging :as log] [slipway.connector.http] [slipway.connector.https] - [slipway.handler] - [slipway.security :as security] + [slipway.context] + [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" - :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)"} + #: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{: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,14 +59,21 @@ :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" - :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 "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)" + :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" @@ -79,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" @@ -90,11 +99,14 @@ :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 '/'" - :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.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" + :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 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)" @@ -104,12 +116,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 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 opts)] (.start server) (when join? (log/debug "joining jetty thread") diff --git a/src/slipway/handler/compression.clj b/src/slipway/compression.clj similarity index 68% rename from src/slipway/handler/compression.clj rename to src/slipway/compression.clj index 8b9735e7..f4774b0b 100644 --- a/src/slipway/handler/compression.clj +++ b/src/slipway/compression.clj @@ -1,15 +1,15 @@ -(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 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)"}) + #: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)"}) (defmulti format ::format) diff --git a/src/slipway/connector/http.clj b/src/slipway/connector/http.clj index bcb086b4..2a62ae0a 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 [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) (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..6ff93089 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" @@ -123,14 +122,14 @@ :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] - :or {idle-timeout-ms 200000 - port 443} + [^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))) 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/context.clj b/src/slipway/context.clj new file mode 100644 index 00000000..40f0a56e --- /dev/null +++ b/src/slipway/context.clj @@ -0,0 +1,80 @@ +(ns slipway.context + (:require [clojure.tools.logging :as log] + [slipway.compression :as compression] + [slipway.security :as security] + [slipway.server :as server] + [slipway.session :as session] + [slipway.websockets :as websockets]) + (:import (org.eclipse.jetty.security SecurityHandler) + (org.eclipse.jetty.server Handler Server) + (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 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 + "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}] + (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 + #: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" + :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 + [^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/handler.clj b/src/slipway/handler.clj deleted file mode 100644 index dfd4e3c0..00000000 --- a/src/slipway/handler.clj +++ /dev/null @@ -1,38 +0,0 @@ -(ns slipway.handler - (:require [clojure.tools.logging :as log] - [slipway.handler.compression :as compression] - [slipway.security :as security] - [slipway.server :as server] - [slipway.session :as session] - [slipway.websockets :as websockets]) - (:import (org.eclipse.jetty.server Handler) - (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 ring-handler login-service {::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) - (.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 (if login-service - (let [security-handler (security/handler login-service opts) - session-handler (session/handler opts)] - (.addBean server login-service) - (.setHandler security-handler ^Handler app-handler) - (.setHandler session-handler security-handler) - session-handler) - app-handler) - handler (if-let [compression-handler (compression/handler opts)] - (doto compression-handler (.setHandler auth-handler)) - auth-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..8795aec0 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 ::handler) -(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,23 +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 - [^LoginService login-service {::keys [realm authenticator constraint-mappings identity-service]}] - (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 + #: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 new file mode 100644 index 00000000..5509aae0 --- /dev/null +++ b/src/slipway/security/hash.clj @@ -0,0 +1,50 @@ +(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 [^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"}) +(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..571d429b --- /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 "an (optional) 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..0de43586 --- /dev/null +++ b/src/slipway/security/openid.clj @@ -0,0 +1,30 @@ +(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/server.clj b/src/slipway/server.clj index 3b26d5bb..adfec816 100644 --- a/src/slipway/server.clj +++ b/src/slipway/server.clj @@ -1,15 +1,15 @@ (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 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)" @@ -18,10 +18,9 @@ (defn create-server ^Server [{::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)) + (.setHandler server ^Handler (handler server opts)) + (some->> error-handler (.setErrorHandler server)) server)) \ 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/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 diff --git a/src/slipway/websockets.clj b/src/slipway/websockets.clj index 957742b1..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 @@ -72,4 +77,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 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 99% rename from test/slipway/server_forwarded_test.clj rename to test/integration/slipway/server_forwarded_test.clj index e6647418..eb96a553 100644 --- a/test/slipway/server_forwarded_test.clj +++ b/test/integration/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" @@ -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/integration/slipway/server_http_test.clj similarity index 99% rename from test/slipway/server_http_test.clj rename to test/integration/slipway/server_http_test.clj index bd6f4300..7dac0cb7 100644 --- a/test/slipway/server_http_test.clj +++ b/test/integration/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" @@ -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/integration/slipway/server_https_test.clj similarity index 99% rename from test/slipway/server_https_test.clj rename to test/integration/slipway/server_https_test.clj index 1c9b1d2b..34720bd4 100644 --- a/test/slipway/server_https_test.clj +++ b/test/integration/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" @@ -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/integration/slipway/server_proxied_test.clj similarity index 99% rename from test/slipway/server_proxied_test.clj rename to test/integration/slipway/server_proxied_test.clj index 35fd3343..42b9dd2b 100644 --- a/test/slipway/server_proxied_test.clj +++ b/test/integration/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" @@ -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/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 75% rename from test/slipway/test_server.clj rename to test/integration/slipway/test_server.clj index 3c63fc48..49da1351 100644 --- a/test/slipway/test_server.clj +++ b/test/integration/slipway/test_server.clj @@ -2,11 +2,13 @@ "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.handler.compression :as compression] [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.security.jaas :as jaas] [slipway.sente] [slipway.server :as server] [slipway.session :as session] @@ -45,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} @@ -93,28 +96,31 @@ [_] {}) -(defmethod authentication :jaas-auth +(defmethod authentication :jaas-form [_] - #::security{: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-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}) - -(defmethod authentication :basic-auth + {::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 [_] - #::security{:realm "slipway" - :login-service "hash" - :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! [] @@ -124,7 +130,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! @@ -132,6 +138,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 diff --git a/test/slipway/websockets_http_test.clj b/test/integration/slipway/websockets_http_test.clj similarity index 99% rename from test/slipway/websockets_http_test.clj rename to test/integration/slipway/websockets_http_test.clj index 82baf788..ce353590 100644 --- a/test/slipway/websockets_http_test.clj +++ b/test/integration/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)) @@ -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)) @@ -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)) @@ -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/integration/slipway/websockets_https_test.clj similarity index 99% rename from test/slipway/websockets_https_test.clj rename to test/integration/slipway/websockets_https_test.clj index 4f2fd6b5..613d4e0f 100644 --- a/test/slipway/websockets_https_test.clj +++ b/test/integration/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)) @@ -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)) @@ -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)) @@ -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)) diff --git a/test/slipway/handler/compression_test.clj b/test/unit/slipway/compression_test.clj similarity index 97% rename from test/slipway/handler/compression_test.clj rename to test/unit/slipway/compression_test.clj index cf44a1d0..09e210d3 100644 --- a/test/slipway/handler/compression_test.clj +++ b/test/unit/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/connector/http_test.clj b/test/unit/slipway/connector/http_test.clj similarity index 98% rename from test/slipway/connector/http_test.clj rename to test/unit/slipway/connector/http_test.clj index c68c74ad..f4f6218b 100644 --- a/test/slipway/connector/http_test.clj +++ b/test/unit/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/unit/slipway/security/hash_test.clj b/test/unit/slipway/security/hash_test.clj new file mode 100644 index 00000000..05cd945f --- /dev/null +++ b/test/unit/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/unit/slipway/session_test.clj b/test/unit/slipway/session_test.clj new file mode 100644 index 00000000..18afc015 --- /dev/null +++ b/test/unit/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/unit/slipway/websockets_test.clj b/test/unit/slipway/websockets_test.clj new file mode 100644 index 00000000..1d9c159a --- /dev/null +++ b/test/unit/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"}))))) +