diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f6603e5..1d88be1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: java-version: '25' - name: Install clojure tools - uses: DeLaGuardo/setup-clojure@13.6.0 + uses: DeLaGuardo/setup-clojure@13.6.1 with: lein: 'latest' github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 782e057b..cdf0496a 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,19 +212,22 @@ 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)" - :connectors "the connectors supported by this server" +#:slipway.server{:connector "the connector supported by this server" + :connectors "the connectors supported by this server (when many connectors supported)" + :handler "the handler for this server, dispatches on :slipway.server/handler-type, :default is slipway.context/handler" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" :buffer-pool "the buffer-pool used by this server (nil for default behaviour)" - :error-handler "the error-handler used by this server for Jetty-level errors"} + :error-handler "the error-handler used by this server for Jetty level errors (nil for default behaviour)"} ``` #### :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 +254,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 +269,17 @@ 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{:path "the context path, default '/'" + :ring-handler "the ring-handler descendant of this context-handler" + :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 "a sequence of [:slipway.context], when used with ::server/handler of ::context/handler-collection"} ``` ### :slipway.websockets @@ -264,7 +287,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 +323,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 +359,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 +370,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 +389,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 +478,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 +492,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 +554,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..a7f088b0 100644 --- a/project.clj +++ b/project.clj @@ -15,15 +15,15 @@ [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"] - "kondo" ["with-profile" "+pedantic" "run" "-m" "clj-kondo.main" "--lint" "src:test" "--parallel"] + "kondo" ["with-profile" "+pedantic" "run" "-m" "clj-kondo.main" "--lint" "src:test/integration:test/unit" "--parallel"] "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..0d70d671 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,26 +99,27 @@ :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{:path "the context path, default '/'" + :ring-handler "the ring-handler descendant of this context-handler" + :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 "a sequence of [:slipway.context], when used with ::server/handler of ::context/handler-collection"} - #:slipway.server{:handler "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)" - :connectors "the connectors supported by this server" + #:slipway.server{:connector "the connector supported by this server" + :connectors "the connectors supported by this server (when many connectors supported)" + :handler "the handler for this server, dispatches on :slipway.server/handler-type, :default is slipway.context/handler" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" :buffer-pool "the buffer-pool used by this server (nil for default behaviour)" - :error-handler "the error-handler used by this server for Jetty level errors"} + :error-handler "the error-handler used by this server for Jetty level errors (nil for default behaviour)"} #: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..1a709fe1 --- /dev/null +++ b/src/slipway/context.clj @@ -0,0 +1,83 @@ +(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 [path null-path-info? virtual-hosts error-handler] + :or {path "/"}}] + (log/debugf "creating context-handler, path %s, null-path-info? %s" path null-path-info?) + (let [context-handler (ContextHandler.)] + (.setContextPath context-handler 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{:path "the context path, default '/'" + :ring-handler "the ring-handler descendant of this context-handler" + :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 "a sequence of [:slipway.context], when used with ::server/handler of ::context/handler-collection"}) + +(defmethod server/handler :default + [^Server server opts] + (handler server opts)) + +(defmethod server/handler ::handler-collection + [^Server server opts] + (log/debugf "creating context-handler collection with [%s] handlers" (count opts)) + (ContextHandlerCollection. (into-array ContextHandler (map (partial handler server) (::handlers 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..f13db0f2 --- /dev/null +++ b/src/slipway/security/hash.clj @@ -0,0 +1,52 @@ +(ns slipway.security.hash + (:require [clojure.tools.logging :as log] + [slipway.security :as security]) + (:import (org.eclipse.jetty.security Authenticator Constraint HashLoginService 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 hash security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) + (if-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) + (log/warn "unable to create login-service from provided configuration"))) \ 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..18ef8e51 --- /dev/null +++ b/src/slipway/security/jaas.clj @@ -0,0 +1,37 @@ +(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 jaas security handler with authenticator %s and %s constraints" (type authenticator) (count constraint-mappings)) + (if-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) + (log/warn "unable to create login-service from provided configuration"))) \ 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..239dc508 --- /dev/null +++ b/src/slipway/security/openid.clj @@ -0,0 +1,54 @@ +(ns slipway.security.openid + (:require [clojure.tools.logging :as log]) + (:import (org.eclipse.jetty.security Constraint SecurityHandler SecurityHandler$PathMapped) + (org.eclipse.jetty.security.openid OpenIdAuthenticator OpenIdConfiguration OpenIdConfiguration$Builder OpenIdLoginService))) + +(defn configuration ^OpenIdConfiguration + [{::keys [issuer client-id client-secret authorization-endpoint token-endpoint end-session-endpoint + authentication-method http-client scopes logout-when-id-token-is-expired?]}] + (-> (OpenIdConfiguration$Builder. issuer client-id client-secret) + (.authorizationEndpoint authorization-endpoint) + (.tokenEndpoint token-endpoint) + (.endSessionEndpoint end-session-endpoint) + (.authenticationMethod authentication-method) + (.httpClient http-client) + (.scopes (some->> scopes (into-array String))) + (.logoutWhenIdTokenIsExpired logout-when-id-token-is-expired?))) + +(defn authenticator ^OpenIdAuthenticator + [config {::keys [oidc-redirect-success oidc-redirect-error oidc-redirect-logout] + :or {oidc-redirect-success OpenIdAuthenticator/J_SECURITY_CHECK}}] + (OpenIdAuthenticator. config oidc-redirect-success oidc-redirect-error oidc-redirect-logout)) + +(comment + #:slipway.security.openid{:issuer "the URL of the OpenID provider" + :client-id "OAuth 2.0 Client Identifier valid at the OpenID provider" + :client-secret "the client secret known only by the Client and the OpenID" + :authorization-endpoint "the URL of the OpenID provider's authorization endpoint if configured" + :token-endpoint "the URL of the OpenID provider's token endpoint if configured" + :end-session-endpoint "the URL of the OpenID provider's end session endpoint if configured" + :authentication-method "authentication method to use with the Token Endpoint" + :http-client "the (optional) HttpClient instance to use" + :scopes "a sequence of ^String scopes to request" + :logout-when-id-token-is-expired? "whether to logout when the ID token is expired" + :oidc-redirect-success "the path where the OIDC provider redirects back to Jetty" + :oidc-redirect-error "optional page where authentication errors are redirected" + :oidc-redirect-logout "optional page where the user is redirected to this page after logout" + :identity-service "a concrete Jetty IdentityService" + :constraint-mappings "a vector of [^String pathSpec, org.eclipse.jetty.security.Constraint]"}) + +(defn handler ^SecurityHandler + [{::keys [issuer constraint-mappings identity-service] :as opts}] + (log/debugf "creating openid security handler with %s constraints" (count constraint-mappings)) + (let [config (configuration opts) + login-service (OpenIdLoginService configuration) + security-handler (doto (SecurityHandler$PathMapped.) + (.setAuthenticator (authenticator config opts)) + (.setLoginService login-service) + (.setRealmName issuer))] + (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..8ba3d688 100644 --- a/src/slipway/server.clj +++ b/src/slipway/server.clj @@ -1,27 +1,32 @@ (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-type 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)" - :connectors "the connectors supported by this server" + #:slipway.server{:connector "the connector supported by this server" + :connectors "the connectors supported by this server (when many connectors supported)" + :handler "the handler for this server, dispatches on :slipway.server/handler-type, :default is slipway.context/handler" :thread-pool "the thread-pool used by this server (nil for default behaviour)" :scheduler "the scheduler used by this server (nil for default behaviour)" :buffer-pool "the buffer-pool used by this server (nil for default behaviour)" - :error-handler "the error-handler used by this server for Jetty level errors"}) + :error-handler "the error-handler used by this server for Jetty level errors (nil for default behaviour)"}) (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)) + (let [server (Server. ^ThreadPool thread-pool ^Scheduler scheduler ^ByteBufferPool buffer-pool) + {handler-config ::handler + connector-config ::connector} opts] + (.setConnectors server (into-array Connector + (if connector-config + [(connector server connector-config)] + (map #(connector server %) connectors)))) + (.setHandler server ^Handler (handler server handler-config)) + (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/integration/slipway/context/collection_test.clj b/test/integration/slipway/context/collection_test.clj new file mode 100644 index 00000000..e6102333 --- /dev/null +++ b/test/integration/slipway/context/collection_test.clj @@ -0,0 +1,324 @@ +(ns slipway.context.collection-test + (:require [clojure.test :refer [deftest is testing]] + [slipway.connector.http :as http] + [slipway.context :as context] + [slipway.example.app :as app] + [slipway.example.html :as html] + [slipway.security :as-alias security] + [slipway.security.hash :as-alias hash] + [slipway.server :as server] + [slipway.test-client :as client] + [slipway.test-server :as test-server]) + (:import (java.net ConnectException) + (javax.net.ssl SSLException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) + +(def of-interest [:protocol-version :status :reason-phrase :body :headers :orig-content-encoding]) + +(deftest single-context + + (testing "default context" + (try + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::server/handler-type ::context/handler-collection + ::context/handlers [{::context/ring-handler (app/handler)}]} + :error-handler app/server-error-handler}) + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {})} + (-> (client/do-get "http://localhost:3000/user" {}) + (select-keys of-interest)))) + + (finally (test-server/stop!)))) + + (testing "specific context" + (try + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::server/handler-type ::context/handler-collection + ::context/handlers [{::context/ring-handler (app/handler) + ::context/path "/x-context"}]} + :error-handler app/server-error-handler}) + + ;; the full path is now at /x-content/user as per the context-path + (is (= {:status 404 + :reason-phrase "Not Found"} + (-> (client/do-get "http://localhost:3000/user" {}) + (select-keys [:status :reason-phrase])))) + + ;; previous content accessible at /x-content/user + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {})} + (-> (client/do-get "http://localhost:3000/x-context/user" {}) + (select-keys of-interest)))) + + (finally (test-server/stop!))))) + +(deftest multiple-contexts + + (testing "multiple contexts with mixed auth" + + ;; In this test, we run two contexts on a single connector: + ;; - At default context, the example application with hash-user / form authentication + ;; - At /metrics context, the example application with custom in-memory hash-user and basic authentication + ;; + ;; Typically you would run different applications, but we can test the variance with one app deployed twice + ;; demonstrate that the correct context takes precedence with different user-auth applied in either case + + (try + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::server/handler-type ::context/handler-collection + ::context/handlers [{::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + {::context/path "/metrics" + ::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/users [["prometheus" "password" ["metrics"]]] + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints}]} + :error-handler app/server-error-handler}) + + (testing "default context, form authentication" + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"} + + :body ""} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 302 + :reason-phrase "Found" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Expires" "Thu, 01 Jan 1970 00:00:00 GMT" + "Location" "http://localhost:3000/login" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "") + (select-keys of-interest)))) + + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/")))) + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/user")))) + + ;; auth redirect goes to expected login page + (is (= "http://localhost:3000/login" (get-in (client/do-get "http" "localhost" 3000 "") [:headers "Location"]))) + + ;; login / login-retry don't redirect + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page false)} + (-> (client/do-get "http" "localhost" 3000 "/login") + (select-keys of-interest)))) + + (is (= 200 (:status (client/do-get "http" "localhost" 3000 "/login-retry")))) + + ;; jetty nukes session and redirects to /login regardless + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/logout"))))) + + (testing "login" + + ;; root without '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body)))) ;; can't compare home html due to csrf token + + ;; root with '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "/" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body))))) + + (testing "wrong-credentials" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page true)} + (-> (client/do-login "http" "localhost" 3000 "/user" "admin" "wrong") + :ring + (select-keys of-interest))))) + + (testing "post-login-redirect" + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "" "admin" "admin"))) + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/" "admin" "admin"))) + + (is (= "http://localhost:3000/user" + (client/do-get-login-redirect "http" "localhost" 3000 "/user" "admin" "admin")))) + + (testing "post-login-redirect-null-request-context" + + ;; if we start our session on the login page we have no post-login request context we fallback + ;; to the default context, this tests a default context is in place in the handler chain + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/login" "admin" "admin")))) + + (testing "session-continuation" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "user" :roles #{"user"}}})} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "user" "password") + (select-keys [:cookies]))] + (-> (client/do-get "http" "localhost" 3000 "/user" session) + (select-keys of-interest)))))) + + (testing "logout" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :reason-phrase "Found" + :status 302} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + (select-keys [:cookies]))] + (client/do-get "http" "localhost" 3000 "/logout" session) + (-> (client/do-get "http" "localhost" 3000 "/" session) + (select-keys [:protocol-version :status :reason-phrase]))))))) + + (testing "metrics context, basic authentication" + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""} + :body (html/error-page 401 "Server Error" "Unauthorized")} + (-> (client/do-get "http" "localhost" 3000 "/metrics") + (select-keys of-interest)))) + + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/metrics")))) + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/metrics/user"))))) + + (testing "credentials provided" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "prometheus:password@localhost" 3000 "/metrics/") + (select-keys of-interest) + (dissoc :body)))) + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "prometheus" :roles #{"metrics"}}})} + (-> (client/do-get "http" "prometheus:password@localhost" 3000 "/metrics/user") + (select-keys of-interest))))) + + (testing "incorrect-password" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :body (html/error-page 401 "Server Error" "Unauthorized") + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""}} + (-> (client/do-get "http" "user:wrong@localhost" 3000 "/metrics/user") + (select-keys of-interest)))))) + + (finally (test-server/stop!))))) \ 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/integration/slipway/security/hash_test.clj b/test/integration/slipway/security/hash_test.clj new file mode 100644 index 00000000..35e44e3d --- /dev/null +++ b/test/integration/slipway/security/hash_test.clj @@ -0,0 +1,274 @@ +(ns slipway.security.hash-test + (:require [clojure.test :refer [deftest is testing]] + [slipway.connector.http :as http] + [slipway.context :as context] + [slipway.example.app :as app] + [slipway.example.html :as html] + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] + [slipway.test-client :as client] + [slipway.test-server :as test-server]) + (:import (java.net ConnectException) + (javax.net.ssl SSLException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) + +(def of-interest [:protocol-version :status :reason-phrase :body :headers :orig-content-encoding]) + +(deftest in-memory-hash-form + + (try + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/users [["admin" "admin" ["server-administrator" + "content-administrator" + "admin" + "user"]] + ["user" "password" ["user"]]] + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"} + + :body ""} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 302 + :reason-phrase "Found" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Expires" "Thu, 01 Jan 1970 00:00:00 GMT" + "Location" "http://localhost:3000/login" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "") + (select-keys of-interest)))) + + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/")))) + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/user")))) + + ;; auth redirect goes to expected login page + (is (= "http://localhost:3000/login" (get-in (client/do-get "http" "localhost" 3000 "") [:headers "Location"]))) + + ;; login / login-retry don't redirect + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page false)} + (-> (client/do-get "http" "localhost" 3000 "/login") + (select-keys of-interest)))) + + (is (= 200 (:status (client/do-get "http" "localhost" 3000 "/login-retry")))) + + ;; jetty nukes session and redirects to /login regardless + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/logout"))))) + + (testing "login" + + ;; root without '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body)))) ;; can't compare home html due to csrf token + + ;; root with '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "/" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body))))) + + (testing "wrong-credentials" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page true)} + (-> (client/do-login "http" "localhost" 3000 "/user" "admin" "wrong") + :ring + (select-keys of-interest))))) + + (testing "post-login-redirect" + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "" "admin" "admin"))) + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/" "admin" "admin"))) + + (is (= "http://localhost:3000/user" + (client/do-get-login-redirect "http" "localhost" 3000 "/user" "admin" "admin")))) + + (testing "post-login-redirect-null-request-context" + + ;; if we start our session on the login page we have no post-login request context we fallback + ;; to the default context, this tests a default context is in place in the handler chain + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/login" "admin" "admin")))) + + (testing "session-continuation" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "user" :roles #{"user"}}})} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "user" "password") + (select-keys [:cookies]))] + (-> (client/do-get "http" "localhost" 3000 "/user" session) + (select-keys of-interest)))))) + + (testing "logout" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :reason-phrase "Found" + :status 302} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + (select-keys [:cookies]))] + (client/do-get "http" "localhost" 3000 "/logout" session) + (-> (client/do-get "http" "localhost" 3000 "/" session) + (select-keys [:protocol-version :status :reason-phrase])))))) + + (finally (test-server/stop!)))) + +(deftest in-memory-hash-basic + + (try + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/users [["admin" "admin" ["server-administrator" + "content-administrator" + "admin" + "user"]] + ["user" "password" ["user"]]] + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""} + :body (html/error-page 401 "Server Error" "Unauthorized")} + (-> (client/do-get "http" "localhost" 3000 "") + (select-keys of-interest)))) + + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/")))) + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/user"))))) + + (testing "credentials provided" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "admin:admin@localhost" 3000 "") + (select-keys of-interest) + (dissoc :body)))) + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "user" :roles #{"user"}}})} + (-> (client/do-get "http" "user:password@localhost" 3000 "/user") + (select-keys of-interest))))) + + (testing "incorrect-password" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :body (html/error-page 401 "Server Error" "Unauthorized") + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""}} + (-> (client/do-get "http" "user:wrong@localhost" 3000 "/user") + (select-keys of-interest))))) + + (finally (test-server/stop!)))) \ No newline at end of file diff --git a/test/slipway/server_forwarded_test.clj b/test/integration/slipway/server/forwarded_test.clj similarity index 70% rename from test/slipway/server_forwarded_test.clj rename to test/integration/slipway/server/forwarded_test.clj index e6647418..be5715da 100644 --- a/test/slipway/server_forwarded_test.clj +++ b/test/integration/slipway/server/forwarded_test.clj @@ -1,18 +1,39 @@ -(ns slipway.server-forwarded-test +(ns slipway.server.forwarded-test (:require [clojure.test :refer [deftest is testing]] - [slipway.test-client :as client] + [slipway.compression :as compression] + [slipway.connector.http :as http] + [slipway.connector.https :as https] + [slipway.context :as context] + [slipway.example.app :as app] [slipway.example.html :as html] - [slipway.test-server :as server]) + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] + [slipway.test-client :as client] + [slipway.test-server :as test-server]) (:import (java.net ConnectException) (javax.net.ssl SSLException) - (org.apache.http ProtocolException))) + (org.apache.http ProtocolException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) (def of-interest [:protocol-version :status :reason-phrase :body :orig-content-encoding]) (deftest simple (try - (server/start! [:http+https+forwarded]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler)} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -30,12 +51,25 @@ (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest compression (try - (server/start! [:http+https+forwarded :compression-nil]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? nil} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -53,10 +87,23 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http+https+forwarded :compression-true]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? true} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -74,10 +121,23 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http+https+forwarded :compression-false]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? false} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -95,12 +155,29 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest form-authentication (try - (server/start! [:http+https+forwarded] :hash-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints http" @@ -134,7 +211,8 @@ [:headers "Location"]))) ;; NOTE: this is the test that ForwardedRequestCustomizer is configured and working as expected. - ;; https://www.eclipse.org/jetty/documentation/jetty-10/operations-guide/index.html#og-protocols-proxy-forwarded + ;; https://jetty.org/docs/jetty/12.1/operations-guide/protocols/index.html#proxy-forwarded + ;; Remove {:http-forwarded? true} on the http connector to see this test fail (is (= "https://localhost:3000/login" (get-in (client/do-get "http://localhost:3000/" {:headers {"Forwarded" "for=2.36.72.144:21216;proto=https"}}) @@ -186,7 +264,8 @@ [:headers "Location"]))) ;; NOTE: this is the test that ForwardedRequestCustomizer is configured and working as expected. - ;; https://www.eclipse.org/jetty/documentation/jetty-10/operations-guide/index.html#og-protocols-proxy-forwarded + ;; https://jetty.org/docs/jetty/12.1/operations-guide/protocols/index.html#proxy-forwarded + ;; Remove {:http-forwarded? true} on the https connector to see this test fail (is (= "http://localhost:3443/login" (get-in (client/do-get "https://localhost:3443/" {:headers {"Forwarded" "for=2.36.72.144:21216;proto=http"} @@ -344,12 +423,29 @@ (-> (client/do-get "https" "localhost" 3443 "/" session) (select-keys [:protocol-version :status :reason-phrase])))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication-http (try - (server/start! [:http+https+forwarded] :basic-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -406,12 +502,29 @@ (-> (client/do-get "http" "user:wrong@localhost" 3000 "/user") (select-keys of-interest))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication-https (try - (server/start! [:http+https+forwarded] :basic-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :http-forwarded? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :http-forwarded? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -468,4 +581,4 @@ (-> (client/do-get "https" "user:wrong@localhost" 3443 "/user" {:insecure? true}) (select-keys of-interest))))) - (finally (server/stop!)))) \ No newline at end of file + (finally (test-server/stop!)))) \ No newline at end of file diff --git a/test/slipway/server_http_test.clj b/test/integration/slipway/server/http_test.clj similarity index 84% rename from test/slipway/server_http_test.clj rename to test/integration/slipway/server/http_test.clj index bd6f4300..fb2bcd71 100644 --- a/test/slipway/server_http_test.clj +++ b/test/integration/slipway/server/http_test.clj @@ -1,17 +1,28 @@ -(ns slipway.server-http-test +(ns slipway.server.http-test (:require [clojure.test :refer [deftest is testing]] + [slipway.compression :as compression] + [slipway.connector.http :as http] + [slipway.context :as context] + [slipway.example.app :as app] [slipway.example.html :as html] + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] [slipway.test-client :as client] - [slipway.test-server :as server]) + [slipway.test-server :as test-server]) (:import (java.net ConnectException) - (javax.net.ssl SSLException))) + (javax.net.ssl SSLException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) (def of-interest [:protocol-version :status :reason-phrase :body :headers :orig-content-encoding]) (deftest simple-http (try - (server/start! [:http]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler)} + :error-handler app/server-error-handler}) ;; gzip/deflate accept-encodings are the default ;; jetty 12 defaults to chunked encoding for compressed payloads @@ -40,12 +51,16 @@ (-> (client/do-get "http://localhost:3000/user" {:decompress-body false}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest compression (try - (server/start! [:http :compression-nil]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? nil} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -69,10 +84,14 @@ (-> (client/do-get "http" "localhost" 3000 "/login" {:decompress-body false}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http :compression-true]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? true} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -96,10 +115,14 @@ (-> (client/do-get "http" "localhost" 3000 "/login" {:decompress-body false}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http :compression-false]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? false} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -124,12 +147,20 @@ (-> (client/do-get "http" "localhost" 3000 "/login" {:decompress-body false}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest form-authentication (try - (server/start! [:http] :hash-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -274,12 +305,20 @@ (-> (client/do-get "http" "localhost" 3000 "/" session) (select-keys [:protocol-version :status :reason-phrase])))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication (try - (server/start! [:http] :basic-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -358,4 +397,4 @@ (-> (client/do-get "http" "user:wrong@localhost" 3000 "/user") (select-keys of-interest))))) - (finally (server/stop!)))) \ No newline at end of file + (finally (test-server/stop!)))) \ No newline at end of file diff --git a/test/slipway/server_https_test.clj b/test/integration/slipway/server/https_test.clj similarity index 68% rename from test/slipway/server_https_test.clj rename to test/integration/slipway/server/https_test.clj index 1c9b1d2b..29d9a930 100644 --- a/test/slipway/server_https_test.clj +++ b/test/integration/slipway/server/https_test.clj @@ -1,17 +1,34 @@ -(ns slipway.server-https-test +(ns slipway.server.https-test (:require [clojure.test :refer [deftest is testing]] + [slipway.compression :as compression] + [slipway.connector.https :as https] + [slipway.context :as context] + [slipway.example.app :as app] [slipway.example.html :as html] + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] [slipway.test-client :as client] - [slipway.test-server :as server]) + [slipway.test-server :as test-server]) (:import (java.net ConnectException) - (org.apache.http ProtocolException))) + (org.apache.http ProtocolException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) (def of-interest [:protocol-version :status :reason-phrase :body :headers :orig-content-encoding]) (deftest simple-https (try - (server/start! [:https]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler #::context{:ring-handler (app/handler)} + :error-handler app/server-error-handler}) ;; gzip/deflate accept-encodings are the default ;; jetty 12 defaults to chunked encoding for compressed payloads @@ -43,12 +60,22 @@ (is (thrown? Exception (client/do-get "http://localhost:3443/" {}))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest compression (try - (server/start! [:https :compression-nil]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? nil} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -73,10 +100,20 @@ :insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:https :compression-true]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? true} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -101,10 +138,20 @@ :insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:https :compression-false]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::compression/enabled? false} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -128,12 +175,26 @@ :insecure? true}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest form-authentication (try - (server/start! [:https] :hash-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -279,12 +340,26 @@ (-> (client/do-get "https" "localhost" 3443 "/" session) (select-keys [:protocol-version :status :reason-phrase])))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication (try - (server/start! [:https] :basic-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -363,14 +438,23 @@ (-> (client/do-get "https" "user:wrong@localhost" 3443 "/user" {:insecure? true}) (select-keys of-interest))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest strict-transport-security (testing "no hsts configuration" (try - (server/start! [:https]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler #::context{:ring-handler (app/handler)} + :error-handler app/server-error-handler}) (let [result (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys (conj of-interest :headers))) @@ -386,12 +470,23 @@ (is (= nil sts-header))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (testing "sts-max-age and subdomains" (try - (server/start! [:hsts]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :sts-max-age-s 31536000 + :sts-include-subdomains? true} + :handler #::context{:ring-handler (app/handler)} + :error-handler app/server-error-handler}) (let [result (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys (conj of-interest :headers))) @@ -407,12 +502,22 @@ (is (= "max-age=31536000; includeSubDomains" sts-header))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (testing "sts-max-age without subdomains" (try - (server/start! [:hsts-no-subdomains]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :sts-max-age-s 31536000} + :handler #::context{:ring-handler (app/handler)} + :error-handler app/server-error-handler}) (let [result (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys (conj of-interest :headers))) @@ -428,12 +533,22 @@ (is (= "max-age=31536000" sts-header))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (testing "hsts no max age (incorrect configuration, no header included)" (try - (server/start! [:hsts-no-max-age]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :sts-include-subdomains? true} + :handler #::context{:ring-handler (app/handler)} + :error-handler app/server-error-handler}) (let [result (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys (conj of-interest :headers))) @@ -449,4 +564,4 @@ (is (= nil sts-header))) - (finally (server/stop!))))) \ No newline at end of file + (finally (test-server/stop!))))) \ No newline at end of file diff --git a/test/slipway/server_proxied_test.clj b/test/integration/slipway/server/proxied_test.clj similarity index 70% rename from test/slipway/server_proxied_test.clj rename to test/integration/slipway/server/proxied_test.clj index 35fd3343..dc3a153d 100644 --- a/test/slipway/server_proxied_test.clj +++ b/test/integration/slipway/server/proxied_test.clj @@ -1,11 +1,20 @@ -(ns slipway.server-proxied-test +(ns slipway.server.proxied-test (:require [clojure.test :refer [deftest is testing]] + [slipway.compression :as compression] + [slipway.connector.http :as http] + [slipway.connector.https :as https] + [slipway.context :as context] + [slipway.example.app :as app] [slipway.example.html :as html] + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] [slipway.test-client :as client] - [slipway.test-server :as server]) + [slipway.test-server :as test-server]) (:import (java.net ConnectException) (javax.net.ssl SSLException) - (org.apache.http ProtocolException))) + (org.apache.http ProtocolException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) ;; Note, this test simply confirms not unintended server behaviour changes when a ProxyConnectionFactory is enabled. ;; to specifically test the ProxyConnectionFactory requires HA Proxy with proxy-protocol configured, e.g. http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt @@ -16,7 +25,19 @@ (deftest simple (try - (server/start! [:http+https+proxied]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler)} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -34,12 +55,25 @@ (-> (client/do-get "https://localhost:3443/user" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest compression (try - (server/start! [:http+https+proxied :compression-nil]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? nil} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -57,10 +91,23 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http+https+proxied :compression-true]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? true} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -78,10 +125,23 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!))) + (finally (test-server/stop!))) (try - (server/start! [:http+https+proxied :compression-false]) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::compression/enabled? false} + :error-handler app/server-error-handler}) (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} :status 200 @@ -99,12 +159,29 @@ (-> (client/do-get "https" "localhost" 3443 "/login" {:insecure? true}) (select-keys of-interest)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest form-authentication (try - (server/start! [:http+https+proxied] :hash-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints http" @@ -333,12 +410,29 @@ (-> (client/do-get "https" "localhost" 3443 "/" session) (select-keys [:protocol-version :status :reason-phrase])))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication-http (try - (server/start! [:http+https+proxied] :basic-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -395,12 +489,29 @@ (-> (client/do-get "http" "user:wrong@localhost" 3000 "/user") (select-keys of-interest))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest basic-authentication-https (try - (server/start! [:http+https+proxied] :basic-auth) + (test-server/start! + #::server{:connectors [#::http{:port 3000 + :proxy-protocol? true} + #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12" + :proxy-protocol? true}] + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + :error-handler app/server-error-handler}) (testing "constraints" @@ -457,4 +568,4 @@ (-> (client/do-get "https" "user:wrong@localhost" 3443 "/user" {:insecure? true}) (select-keys of-interest))))) - (finally (server/stop!)))) \ No newline at end of file + (finally (test-server/stop!)))) \ No newline at end of file diff --git a/test/integration/slipway/server/virtual_hosts_test.clj b/test/integration/slipway/server/virtual_hosts_test.clj new file mode 100644 index 00000000..47acefa0 --- /dev/null +++ b/test/integration/slipway/server/virtual_hosts_test.clj @@ -0,0 +1,431 @@ +(ns slipway.server.virtual-hosts-test + (:require [clojure.test :refer [deftest is testing]] + [slipway.connector.http :as http] + [slipway.context :as context] + [slipway.example.app :as app] + [slipway.example.html :as html] + [slipway.security :as-alias security] + [slipway.security.hash :as-alias hash] + [slipway.server :as server] + [slipway.test-client :as client] + [slipway.test-server :as test-server]) + (:import (java.net ConnectException) + (javax.net.ssl SSLException) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) + +(def of-interest [:protocol-version :status :reason-phrase :body :headers :orig-content-encoding]) + +(deftest virtual-hosts + + (testing "multiple contexts with mixed auth, pinned to different connectors" + + ;; In this test, we run three contexts, two pinned to connector on :3000, one pinned to connector on :3001 + ;; - On :3000 at default context, the example application with hash-user and form authentication + ;; - On :3000 at /metrics context, the example application with custom in-memory hash-user and basic authentication + ;; - On :3001 at default context, the example application with (different) custom in-memory hash-user and form authentication + ;; + ;; Typically you would run different applications, but we can test the variance with one app deployed three times + ;; and demonstrate that the correct context takes precedence with different user-auth applied in each case and + ;; the handlers are correctly pinned to their connectors on different ports + + (try + (test-server/start! + #::server{:connectors [{::http/name "connector-3000" + ::http/port 3000} + {::http/name "connector-3001" + ::http/port 3001}] + :handler {::server/handler-type ::context/handler-collection + ::context/handlers [{::context/ring-handler (app/handler) + ::context/virtual-hosts ["@connector-3000"] + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints} + {::context/path "/metrics" + ::context/virtual-hosts ["@connector-3000"] + ::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/users [["prometheus" "password" ["metrics"]]] + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints} + {::context/virtual-hosts ["@connector-3001"] + ::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/users [["x-user" "x-password" ["x-role"]]] + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints}]} + :error-handler app/server-error-handler}) + + (testing "port :3000, default context, form authentication" + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"} + + :body ""} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 302 + :reason-phrase "Found" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Expires" "Thu, 01 Jan 1970 00:00:00 GMT" + "Location" "http://localhost:3000/login" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "") + (select-keys of-interest)))) + + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/")))) + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/user")))) + + ;; auth redirect goes to expected login page + (is (= "http://localhost:3000/login" (get-in (client/do-get "http" "localhost" 3000 "") [:headers "Location"]))) + + ;; login / login-retry don't redirect + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page false)} + (-> (client/do-get "http" "localhost" 3000 "/login") + (select-keys of-interest)))) + + (is (= 200 (:status (client/do-get "http" "localhost" 3000 "/login-retry")))) + + ;; jetty nukes session and redirects to /login regardless + (is (= 302 (:status (client/do-get "http" "localhost" 3000 "/logout"))))) + + (testing "login" + + ;; root without '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body)))) ;; can't compare home html due to csrf token + + ;; root with '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3000 "/" "admin" "admin") + :ring + (select-keys of-interest) + (dissoc :body))))) + + (testing "wrong-credentials" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page true)} + (-> (client/do-login "http" "localhost" 3000 "/user" "admin" "wrong") + :ring + (select-keys of-interest))))) + + (testing "post-login-redirect" + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "" "admin" "admin"))) + + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/" "admin" "admin"))) + + (is (= "http://localhost:3000/user" + (client/do-get-login-redirect "http" "localhost" 3000 "/user" "admin" "admin")))) + + (testing "post-login-redirect-null-request-context" + + ;; if we start our session on the login page we have no post-login request context we fallback + ;; to the default context, this tests a default context is in place in the handler chain + (is (= "http://localhost:3000/" + (client/do-get-login-redirect "http" "localhost" 3000 "/login" "admin" "admin")))) + + (testing "session-continuation" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "user" :roles #{"user"}}})} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "user" "password") + (select-keys [:cookies]))] + (-> (client/do-get "http" "localhost" 3000 "/user" session) + (select-keys of-interest)))))) + + (testing "logout" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :reason-phrase "Found" + :status 302} + (let [session (-> (client/do-login "http" "localhost" 3000 "" "admin" "admin") + (select-keys [:cookies]))] + (client/do-get "http" "localhost" 3000 "/logout" session) + (-> (client/do-get "http" "localhost" 3000 "/" session) + (select-keys [:protocol-version :status :reason-phrase]))))))) + + (testing "port :3000 metrics context, basic authentication" + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3000 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3000 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""} + :body (html/error-page 401 "Server Error" "Unauthorized")} + (-> (client/do-get "http" "localhost" 3000 "/metrics") + (select-keys of-interest)))) + + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/metrics")))) + (is (= 401 (:status (client/do-get "http" "localhost" 3000 "/metrics/user"))))) + + (testing "credentials provided" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "prometheus:password@localhost" 3000 "/metrics/") + (select-keys of-interest) + (dissoc :body)))) + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "prometheus" :roles #{"metrics"}}})} + (-> (client/do-get "http" "prometheus:password@localhost" 3000 "/metrics/user") + (select-keys of-interest))))) + + (testing "incorrect-password" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 401 + :reason-phrase "Unauthorized" + :body (html/error-page 401 "Server Error" "Unauthorized") + :orig-content-encoding nil + :headers {"Cache-Control" "must-revalidate,no-cache,no-store" + "Connection" "close" + "Content-Length" "1484" + "Content-Type" "text/html;charset=iso-8859-1" + "Vary" "Accept-Encoding" + "WWW-Authenticate" "Basic realm=\"slipway\""}} + (-> (client/do-get "http" "user:wrong@localhost" 3000 "/metrics/user") + (select-keys of-interest)))))) + + (testing "port :3001, default context, form authentication" + + (testing "constraints" + + ;; wrong port / scheme + (is (thrown? ConnectException (:status (client/do-get "http" "localhost" 2999 "")))) + (is (thrown? SSLException (:status (client/do-get "https" "localhost" 3001 "")))) + + ;; does not require authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding nil + :headers {"Connection" "close" + "Content-Length" "0" + "Content-Type" "text/plain" + "Vary" "Accept-Encoding"} + + :body ""} + (-> (client/do-get "http" "localhost" 3001 "/up") + (select-keys of-interest)))) + + ;; requires authentication + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 302 + :reason-phrase "Found" + :orig-content-encoding nil + :body "" + :headers {"Connection" "close" + "Content-Length" "0" + "Expires" "Thu, 01 Jan 1970 00:00:00 GMT" + "Location" "http://localhost:3001/login" + "Vary" "Accept-Encoding"}} + (-> (client/do-get "http" "localhost" 3001 "") + (select-keys of-interest)))) + + (is (= 302 (:status (client/do-get "http" "localhost" 3001 "/")))) + (is (= 302 (:status (client/do-get "http" "localhost" 3001 "/user")))) + + ;; auth redirect goes to expected login page + (is (= "http://localhost:3001/login" (get-in (client/do-get "http" "localhost" 3001 "") [:headers "Location"]))) + + ;; login / login-retry don't redirect + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page false)} + (-> (client/do-get "http" "localhost" 3001 "/login") + (select-keys of-interest)))) + + (is (= 200 (:status (client/do-get "http" "localhost" 3001 "/login-retry")))) + + ;; jetty nukes session and redirects to /login regardless + (is (= 302 (:status (client/do-get "http" "localhost" 3001 "/logout"))))) + + (testing "login" + + ;; root without '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3001 "" "x-user" "x-password") + :ring + (select-keys of-interest) + (dissoc :body)))) ;; can't compare home html due to csrf token + + ;; root with '/' (tests jetty nullPathInfo) + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"}} + (-> (client/do-login "http" "localhost" 3001 "/user" "x-user" "x-password") + :ring + (select-keys of-interest) + (dissoc :body))))) + + (testing "wrong-credentials" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/login-page true)} + (-> (client/do-login "http" "localhost" 3001 "/user" "x-user" "wrong") + :ring + (select-keys of-interest))))) + + (testing "post-login-redirect" + + (is (= "http://localhost:3001/" + (client/do-get-login-redirect "http" "localhost" 3001 "" "x-user" "x-password"))) + + (is (= "http://localhost:3001/" + (client/do-get-login-redirect "http" "localhost" 3001 "/" "x-user" "x-password"))) + + (is (= "http://localhost:3001/user" + (client/do-get-login-redirect "http" "localhost" 3001 "/user" "x-user" "x-password")))) + + (testing "post-login-redirect-null-request-context" + + ;; if we start our session on the login page we have no post-login request context we fallback + ;; to the default context, this tests a default context is in place in the handler chain + (is (= "http://localhost:3001/" + (client/do-get-login-redirect "http" "localhost" 3001 "/login" "x-user" "x-password")))) + + (testing "session-continuation" + + (is (= {:protocol-version {:name "HTTP", :major 1, :minor 1} + :status 200 + :reason-phrase "OK" + :orig-content-encoding "gzip" + :headers {"Connection" "close" + "Content-Type" "text/html" + "Vary" "Accept-Encoding"} + :body (html/user-page {:slipway.user/identity {:name "x-user" :roles #{"x-role"}}})} + (let [session (-> (client/do-login "http" "localhost" 3001 "" "x-user" "x-password") + (select-keys [:cookies]))] + (-> (client/do-get "http" "localhost" 3001 "/user" session) + (select-keys of-interest)))))) + + (testing "logout" + + (is (= {:protocol-version {:name "HTTP" :major 1 :minor 1} + :reason-phrase "Found" + :status 302} + (let [session (-> (client/do-login "http" "localhost" 3001 "" "x-user" "x-password") + (select-keys [:cookies]))] + (client/do-get "http" "localhost" 3001 "/logout" session) + (-> (client/do-get "http" "localhost" 3001 "/" session) + (select-keys [:protocol-version :status :reason-phrase]))))))) + + (finally (test-server/stop!))))) \ No newline at end of file diff --git a/test/slipway/websockets_http_test.clj b/test/integration/slipway/server/ws_http_test.clj similarity index 87% rename from test/slipway/websockets_http_test.clj rename to test/integration/slipway/server/ws_http_test.clj index 82baf788..e577be8d 100644 --- a/test/slipway/websockets_http_test.clj +++ b/test/integration/slipway/server/ws_http_test.clj @@ -1,10 +1,18 @@ -(ns slipway.websockets-http-test +(ns slipway.server.ws-http-test (:require [clojure.test :refer [deftest is testing]] [clojure.tools.logging :as log] + [slipway.connector.http :as http] + [slipway.context :as context] + [slipway.example.app :as app] + [slipway.security :as security] + [slipway.security.hash :as hash] + [slipway.server :as server] [slipway.test-client :as client] - [slipway.test-server :as server]) + [slipway.test-server :as test-server] + [slipway.websockets :as websockets]) (:import (java.security SecureRandom) - (java.util Base64))) + (java.util Base64) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) ;; use this output to run a server and validate via curl (see commented sexp below) (defn print-ws-upgrade-curl @@ -40,7 +48,11 @@ (deftest full-connection-no-auth (try - (server/start! [:http]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "localhost" 3000) client-id (str (random-uuid)) @@ -86,7 +98,7 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}}))))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (comment @@ -95,7 +107,16 @@ (deftest full-connection-form-auth (try - (server/start! [:http] :hash-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-login "http" "localhost" 3000 "/" "admin" "admin") client-id (str (random-uuid)) @@ -129,14 +150,23 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}})))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (comment (deftest full-connection-basic-auth (try - (server/start! [:http] :basic-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "admin:admin@localhost" 3000) client-id (str (random-uuid)) @@ -171,12 +201,16 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}})))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (deftest ws-connection-upgrade-with-no-auth (try - (server/start! [:http :websockets]) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "localhost" 3000) client-id (str (random-uuid)) @@ -292,12 +326,21 @@ "sec-websocket-key" sec-ws-key}}) :status)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest ws-connection-upgrade-with-form-auth (try - (server/start! [:http :websockets] :hash-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-login "http" "localhost" 3000 "/" "admin" "admin") client-id (str (random-uuid)) @@ -379,6 +422,7 @@ "Upgrade" "Websocket" "Origin" "http://attacker.site"}}) :status))) + ;; wrong scheme origin header (is (= 400 (-> (format "http://localhost:3000/chsk?client-id=%s&csrf-token=%s" client-id csrf-token) (client/do-get {:cookies cookies @@ -410,12 +454,21 @@ bytes)))}}) :status))))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest ws-connection-upgrade-with-basic-auth (try - (server/start! [:http :websockets] :basic-auth) + (test-server/start! + #::server{:connector {::http/port 3000} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "http" "admin:admin@localhost" 3000) client-id (str (random-uuid)) @@ -498,6 +551,7 @@ "Upgrade" "Websocket" "Origin" "http://attacker.site"}}) :status))) + ;; wrong scheme origin header (is (= 400 (-> (format "http://admin:admin@localhost:3000/chsk?client-id=%s&csrf-token=%s" client-id csrf-token) (client/do-get {:cookies cookies @@ -529,4 +583,4 @@ bytes)))}}) :status)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) diff --git a/test/slipway/websockets_https_test.clj b/test/integration/slipway/server/ws_https_test.clj similarity index 83% rename from test/slipway/websockets_https_test.clj rename to test/integration/slipway/server/ws_https_test.clj index 4f2fd6b5..4c6aa29f 100644 --- a/test/slipway/websockets_https_test.clj +++ b/test/integration/slipway/server/ws_https_test.clj @@ -1,11 +1,19 @@ -(ns slipway.websockets-https-test +(ns slipway.server.ws-https-test (:require [clj-http.conn-mgr :as conn] [clojure.test :refer [deftest is testing]] [clojure.tools.logging :as log] + [slipway.connector.https :as-alias https] + [slipway.context :as-alias context] + [slipway.example.app :as app] + [slipway.security :as-alias security] + [slipway.security.hash :as-alias hash] + [slipway.server :as-alias server] [slipway.test-client :as client] - [slipway.test-server :as server]) + [slipway.test-server :as test-server] + [slipway.websockets :as-alias websockets]) (:import (java.security SecureRandom) - (java.util Base64))) + (java.util Base64) + (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) ;; use this output to run a server and validate via curl (see commented sexp below) (defn print-ws-upgrade-curl @@ -41,7 +49,17 @@ (deftest full-connection-with-no-auth (try - (server/start! [:https :websockets]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) @@ -92,7 +110,7 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}}))))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (comment @@ -101,7 +119,22 @@ (deftest full-connection-with-form-auth (try - (server/start! [:https :websockets] :hash-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-login "https" "localhost" 3443 "/" "admin" "admin" {:insecure? true}) client-id (str (random-uuid)) @@ -138,7 +171,7 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}})))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (comment @@ -147,7 +180,22 @@ (deftest full-connection-with-basic-auth (try - (server/start! [:https :websockets] :basic-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "admin:admin@localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) @@ -186,12 +234,22 @@ "sec-websocket-version" "13" "sec-websocket-key" sec-ws-key}})))) - (finally (server/stop!))))) + (finally (test-server/stop!))))) (deftest ws-connection-upgrade-with-no-auth (try - (server/start! [:https :websockets]) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) @@ -283,6 +341,7 @@ "Upgrade" "Websocket" "Origin" "http://attacker.site"}}) :status))) + ;; wrong scheme origin header (is (= 400 (-> (format "https://localhost:3443/chsk?client-id=%s&csrf-token=%s" client-id csrf-token) (client/do-get {:cookies cookies @@ -301,12 +360,27 @@ "Origin" "https://localhost:2999"}}) :status)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest ws-connection-upgrade-with-form-auth (try - (server/start! [:https :websockets] :hash-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (FormAuthenticator. "/login" "/login-retry" false) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-login "https" "localhost" 3443 "/" "admin" "admin" {:insecure? true}) client-id (str (random-uuid)) @@ -398,6 +472,7 @@ "Upgrade" "Websocket" "Origin" "http://attacker.site"}}) :status))) + ;; wrong scheme origin header (is (= 400 (-> (format "https://localhost:3443/chsk?client-id=%s&csrf-token=%s" client-id csrf-token) (client/do-get {:cookies cookies @@ -432,12 +507,27 @@ bytes)))}}) :status)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) (deftest ws-connection-upgrade-with-basic-auth (try - (server/start! [:https :websockets] :basic-auth) + (test-server/start! + #::server{:connector #::https{:port 3443 + :keystore "dev-resources/my-keystore.jks" + :keystore-type "PKCS12" + :keystore-password "password" + :truststore "dev-resources/my-truststore.jks" + :truststore-password "password" + :truststore-type "PKCS12"} + :handler {::context/ring-handler (app/handler) + ::security/handler "hash" + ::hash/realm "slipway" + ::hash/user-file "dev-resources/jaas/hash-realm.properties" + ::hash/authenticator (BasicAuthenticator.) + ::hash/constraint-mappings app/constraints + ::websockets/enabled? true} + :error-handler app/server-error-handler}) (let [{:keys [csrf-token cookies]} (client/do-get-csrf "https" "admin:admin@localhost" 3443 {:insecure? true}) client-id (str (random-uuid)) @@ -556,6 +646,7 @@ "Upgrade" "Websocket" "Origin" "http://attacker.site"}}) :status))) + ;; wrong scheme origin header (is (= 400 (-> (format "https://admin:admin@localhost:3443/chsk?client-id=%s&csrf-token=%s" client-id csrf-token) (client/do-get {:cookies cookies @@ -590,4 +681,4 @@ bytes)))}}) :status)))) - (finally (server/stop!)))) + (finally (test-server/stop!)))) 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/integration/slipway/test_server.clj b/test/integration/slipway/test_server.clj new file mode 100644 index 00000000..41d0673a --- /dev/null +++ b/test/integration/slipway/test_server.clj @@ -0,0 +1,20 @@ +(ns slipway.test-server + "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.sente])) + +(def state (atom nil)) + +(defn stop! + [] + (when-let [server @state] + (slipway/stop server))) + +(defn start! + "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" + [config] + (stop!) + (reset! state (slipway/start config))) \ No newline at end of file diff --git a/test/slipway/test_server.clj b/test/slipway/test_server.clj deleted file mode 100644 index 3c63fc48..00000000 --- a/test/slipway/test_server.clj +++ /dev/null @@ -1,137 +0,0 @@ -(ns slipway.test-server - "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.connector.http :as http] - [slipway.connector.https :as https] - [slipway.example.app :as app] - [slipway.handler.compression :as compression] - [slipway.security :as security] - [slipway.sente] - [slipway.server :as server] - [slipway.session :as session] - [slipway.websockets :as websockets]) - (:import (org.eclipse.jetty.security.authentication BasicAuthenticator FormAuthenticator))) - -(def state (atom nil)) - -(def http-forwarded #::http{:http-forwarded? true}) -(def http-proxied #::http{:proxy-protocol? true}) - -(def http-connector #::http{:port 3000}) - -(def https-forwarded #::https{:http-forwarded? true}) -(def https-proxied #::https{:proxy-protocol? true}) - -(def https-connector #::https{:port 3443 - :keystore "dev-resources/my-keystore.jks" - :keystore-type "PKCS12" - :keystore-password "password" - :truststore "dev-resources/my-truststore.jks" - :truststore-password "password" - :truststore-type "PKCS12"}) - -(def https-rfc-2616-connector #::https{:http-compliance "RFC2616"}) -(def https-rfc-7230-connector #::https{:http-compliance "RFC7230"}) - -(def hsts #::https{:sts-max-age-s 31536000 - :sts-include-subdomains? true}) - -(def hsts-no-subdomains #::https{:sts-max-age-s 31536000}) - -(def hsts-no-max-age #::https{:sts-include-subdomains? true}) - -(def options - {:http #::server{:connectors [http-connector] - :error-handler app/server-error-handler} - - :websockets #::websockets{:path-spec "/chsk"} - - :https #::server{:connectors [https-connector] - :error-handler app/server-error-handler} - - :https-rfc2616 #::server{:connectors [(merge https-connector https-rfc-2616-connector)] - :error-handler app/server-error-handler} - - :https-rfc7230 #::server{:connectors [(merge https-connector https-rfc-7230-connector)] - :error-handler app/server-error-handler} - - :hsts #::server{:connectors [(merge https-connector hsts)] - :error-handler app/server-error-handler} - - :hsts-no-subdomains #::server{:connectors [(merge https-connector hsts-no-subdomains)] - :error-handler app/server-error-handler} - - ;; this is an error condition / incorrect configuration - subdomains requires max-age set - :hsts-no-max-age #::server{:connectors [(merge https-connector hsts-no-max-age)] - :error-handler app/server-error-handler} - - :http+https #::server{:connectors [http-connector https-connector] - :error-handler app/server-error-handler} - - :http+https+forwarded #::server{:connectors [(merge http-connector http-forwarded) - (merge https-connector https-forwarded)] - :error-handler app/server-error-handler} - - :http+https+proxied #::server{:connectors [(merge http-connector http-proxied) - (merge https-connector https-proxied)] - :error-handler app/server-error-handler} - - :compression-nil #::compression{:enabled? nil} - - :compression-false #::compression{:enabled? false} - - :compression-true #::compression{:enabled? true} - - :short-session #::session{:max-inactive-interval-s 10} - - :join #::slipway{:join? true}}) - -(defmulti authentication identity) - -(defmethod authentication :default - [_] - {}) - -(defmethod authentication :jaas-auth - [_] - #::security{:realm "slipway" - :login-service "jaas" - :authenticator (FormAuthenticator. "/login" "/login-retry" false) - :constraint-mappings app/constraints}) - -(defmethod authentication :hash-auth - [_] - #::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{:realm "slipway" - :login-service "hash" - :hash-user-file "dev-resources/jaas/hash-realm.properties" - :authenticator (BasicAuthenticator.) - :constraint-mappings app/constraints}) - -(defn stop! - [] - (when-let [server @state] - (slipway/stop server))) - -"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) - - Note: Authentication loginHandlers are stateful, so they must be created fresh for each server" -(defn start! - ([keys] - (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 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"}))))) +