Liking cljdoc? Tell your friends :D

Slipway: a Clojure companion to Jetty.

Jetty VersionClojars Project
Jetty 9Clojars Project
Jetty 10Clojars Project
Jetty 11Clojars Project

Slipway Test

Eclipse Jetty is the web server at the heart of our product, Kpow for Apache Kafka®.

Slipway is our Clojure companion to embedded Jetty.

Slipway is not an opinionated web-framework, it is a battle-tested web server with websocket support.

We intend (eventually) to open-source a full-stack example application using slipway in shortcut.

Quick Start

Choose a project by Jetty version, then open a REPL.

Start slipway with a ring-handler and a map of configuration options:

(require '[slipway :as slipway])
(require '[slipway.server :as server])
(require '[slipway.connector.http :as http])

(defn handler [_] {:status 200 :body "Hello world"})

(def http-connector #::http{:port 3000})

(slipway/start handler #::server{:connectors [http-connector]})

Your hello world application is now running on http://localhost:3000.

Example Configurations

Various configuration of Slipway can be found in the example.clj namespace.

These configurations are used by our integration tests. The stateful start!/stop! functions within that namespace are not considered a guide for your own use of Slipway, but they are convenient for playing with server configurations like so:

(require '[slipway.example :as example])

(example/start! [:http :hash-auth :short-session])

Your sample application with property file based authz is now available on http://localhost:3000.

You can login with jetty/jetty, admin/admin, plain/plain, other/other, or user/password as defined in hash-realm.properties.

Thanks to the :short-session configuration your session will expire after 10s of inactivity.


Slipway Login

Why Jetty?

Jetty is a mature, stable, commercially supported project with an active, deeply experienced core team of contributors.

Ubiquitous in the enterprise Java world, Jetty has many eyes raising issues and driving improvments.

More than a simple web server, Jetty is battle-tested, performant, and feature rich.

Our Requirements

Kpow is a secure web-application with a rich SPA UI served by websockets.

Deployed in-cloud and on-premises Kpow has seemingly every possible Jetty configuration option in use by at least one end-user.

User: Can I configure a custom CA certificate to secure my JAAS/LDAPS authentication?

Kpow Team: Yes (thanks to Jetty).

We have a hard requirement to support customers on Java 8 and Java 11+ and incorporate feedback from external security teams.

Primary Goals

Slipway aims to provide first-class, extensible support for:

  • HTTP 1.1
  • HTTPS / SSL
  • Synchronous handlers
  • JAAS Authentication (LDAP, HashUser, etc)
  • Form / basic authentication
  • WebSockets
  • Java 8 / 11+
  • Jetty 9 / 10 / 11
  • Session management
  • Proxy protocol / http forwarded
  • Common / sensible defaults (e.g. gzip compression)
  • Configurable error handling
  • Automated CVE scanning with NVD
  • Comprehensive integration tests
  • Ring compatibility

Secondary Goals

  • Broad support for general Jetty use-cases / configuration

Future Goals

  • Backport our SAML, OpenID and OAuth authentication implementations

Currently Out Of Scope

  • Http2/3
  • Asynchronous Handlers
  • Ajax (including auto-fallback)

Non-Goals

  • A simplified DSL for Jetty

Installation

Slipway will shortly be available from Clojars.

Add one of the version-specific dependencies to your project:

;; Jetty 10: Recommended for general use, requires Java 11+

[io.factorhouse/slipway-jetty10 "1.1.0"]
;; Jetty 9: If you require running with Java 8

[io.operatr/slipway-jetty9 "1.1.0"]
;; Jetty 11: If you want to run with Jakarta rather than Javax

[io.operatr/slipway-jetty9 "1.1.0"]

Configuration

TBD: short-term check out slipway.clj for options configuration and example.clj for example usage.

  #:slipway.handler.gzip{:enabled?            "is gzip enabled? default true"
                         :included-mime-types "mime types to include (without charset or other parameters), leave nil for default types"
                         :excluded-mime-types "mime types to exclude (replacing any previous exclusion set)"
                         :min-gzip-size       "min response size to trigger dynamic compression"}

  #: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."
                            :port                       "port this connector listens on. If set the 0 a random port is assigned which may be obtained with getLocalPort()"
                            :idle-timeout               "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 180000."
                            :http-forwarded?            "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs"
                            :proxy-protocol?            "if true, add the ProxyConnectionFactor. 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"
                            :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, 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 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 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"}

  #: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."
                           :port            "port this connector listens on. If set the 0 a random port is assigned which may be obtained with getLocalPort()"
                           :idle-timeout    "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 180000."
                           :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"}

  #:slipway.authz{:login-service       "pluggable Jetty LoginService identifier, 'jaas' and 'hash' supported by default"
                  :authenticator       "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)"
                  :constraint-mappings "a list of concrete Jetty ConstraintMapping"
                  :realm               "the JAAS realm to use with jaas or hash authentication"
                  :hash-user-file      "the path to a Jetty Hash User File"}

  #:slipway.session{:secure-request-only?  "set the secure flag on session cookies"
                    :http-only?            "set the http-only flag on session cookies"
                    :same-site             "set session cookie same-site policy to :none, :lax, or :strict"
                    :max-inactive-interval "max session idle time (in s)"
                    :tracking-modes        "a set (colloection) of #{:cookie, :ssl, or :url}"
                    :cookie-name           "the name of the session cookie"
                    :session-id-manager    "the meta manager used for cross context session management"
                    :refresh-cookie-age    "max time before a session cookie is re-set (in s)"
                    :path-parameter-name   "name of path parameter used for URL session tracking"}

  ;; Jetty 10 / Jetty 11 Websockets
  #:slipway.websockets{:idle-timeout            "max websocket idle time (in ms)"
                       :input-buffer-size       "max websocket input buffer size"
                       :output-buffer-size      "max websocket output buffer size"
                       :max-text-message-size   "max websocket text message size"
                       :max-binary-message-size "max websocket binary message size"
                       :max-frame-size          "max websoccket frame size"
                       :auto-fragment           "websocket auto fragment"}

  ;; Jetty 9 Websockets
  #:slipway.websockets{:idle-timeout            "max websocket idle time (in ms)"
                       :input-buffer-size       "max websocket input buffer size"
                       :max-text-message-size   "max websocket text message size"
                       :max-binary-message-size "max websocket binary message size"}

  #:slipway.handler{:context-path    "the root context path, default '/'"
                    :ws-path         "the path serving the websocket upgrade handler, default '/chsk'"
                    :null-path-info? "true if /path is not redirected to /path/, default true"}

  #:slipway.server{:handler       "the base Jetty handler implementation (:default defmethod impl found in slipway.handler)"
                   :connectors    "the connectors supported by this server"
                   :thread-pool   "the thread-pool used by this server (leave null for reasonable defaults)"
                   :error-handler "the error-handler used by this server for Jetty level errors"}

  #:slipway{:join? "join the Jetty threadpool, blocks the calling thread until jetty exits, default false"}

TBD Update Below This Line


WebSockets

Slipway provides the same API as the ring-jetty9-adapter for upgrading HTTP requests to WebSockets.

(require '[slipway.websockets :as ws])
(require '[slipway.server :as slipway])

(def ws-handler {:on-connect (fn [ws] (ws/send! ws "Hello world"))
                 :on-error (fn [ws e])
                 :on-close (fn [ws status-code reason])
                 :on-text (fn [ws text-message])
                 :on-bytes (fn [ws bytes offset len])
                 :on-ping (fn [ws bytebuffer])
                 :on-pong (fn [ws bytebuffer])})

(defn handler [req]
  (if (ws/upgrade-request? req)
    (ws/upgrade-response ws-handler)
    {:status 406}))
    
(slipway/run-jetty handler {:port 3000 :join? false})

The ws object passed to each handler function implements the slipway.websockets.WebSockets protocol:

(defprotocol WebSockets
  (send! [this msg] [this msg callback])
  (ping! [this] [this msg])
  (close! [this] [this status-code reason])
  (remote-addr [this])
  (idle-timeout! [this ms])
  (connected? [this])
  (req-of [this]))

Sente integration

Slipway supports Sente out-of-the box.

Simply include Sente in your project's dependencies and follow Sente's getting started guide, and use the slipway web-server adapter:

(require '[slipway.sente :refer [get-sch-adapter]])

JAAS integration

JAAS implements a Java version of the standard Pluggable Authentication Module (PAM) framework.

JAAS can be used for two purposes:

  • for authentication of users, to reliably and securely determine who is currently executing Java code, regardless of whether the code is running as an application, an applet, a bean, or a servlet; and
  • for authorization of users to ensure they have the access control rights (permissions) required to do the actions performed.

JAAS implements a Java version of the standard Pluggable Authentication Module (PAM) framework. See Making Login Services Independent from Authentication Technologies for further information.

For more information visit the Jetty documentation.

Slipway is the only ring adapter that supports Jetty JAAS out of the box. Thus, one of the few ways to authenticate using LDAP in the Clojure world. Oftentimes a requirement for the enterprise.

Usage

Pass an :auth key to your run-jetty options map:

(require '[slipway.auth.constraints :as constraints])

{:auth-method         "basic"                               ;; either "basic" (basic authentication) or "form" (form based authencation, with a HTML login form served at :login-uri)
 :auth-type           "jaas"                                ;; either "jaas" or "hash"
 :login-uri           "/login"                              ;; the URI where the login form is hosted
 :login-retry-uri     "/login-retry"
 :realm               "my-app"
 :logout-uri          "/logout"
 :session             {:http-only?            true
                       :same-site             :strict       ;; can be :lax, :strict or :none
                       :tracking-modes        #{:cookie}    ;; can be :url, :cookie :ssl
                       :max-inactive-interval -1}           ;; set the max period of inactivity, after which the session is invalidated, in seconds.
 :constraint-mappings (constraints/constraint-mappings
                       ;; /css/* is not protected. Everyone (including unauthenticated users) can access
                       ["/css/*" (constraints/no-auth)]
                       ;; /api/* is protected. Any authenticated user can access
                       ["/api/*" (constraints/basic-auth-any-constraint)])}

Successfully authenticated users will have their details assoced into the Ring request map under the key :slipway.auth/user - it contains:

{:provider :jetty
 :name     "Jane"
 :roles    ["admin"]}

Constraints

Constraints describe an auth and/or data constraint.

The slipway.auth.constraints namespace has a few useful helper functions for working with constraints.

jaas.config

Start your application (JAR or REPL session) with the additional JVM opt -Djava.security.auth.login.config=/some/path/to/jaas.config

For example configurations refer to this tutorial

Hash realm authentication

The simplest JAAS authentication module. A static list of hashed users in a file.

Example jaas.config: ('my-app' must be the same as the configured :realm)

my-app {
           org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required
           debug="true"
           file="dev-resources/jaas/hash-realm.properties";
       };

Example hash-realm.properties:

# This file defines users passwords and roles for a HashUserRealm
#
# The format is
#  <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed.  The class
# org.eclipse.jetty.util.security.Password should be used to generate obfuscated
# passwords or password checksums
#
# If DIGEST Authentication is used, the password must be in a recoverable
# format, either plain text or OBF:.
#
jetty: MD5:164c88b302622e17050af52c89945d44,kafka-users,content-administrators
admin: CRYPT:adpexzg3FUZAk,server-administrators,content-administrators,kafka-admins
other: OBF:1xmk1w261u9r1w1c1xmq,kafka-admins,kafka-users
plain: plain,content-administrators
user: password,kafka-users
# This entry is for digest auth.  The credential is a MD5 hash of username:realmname:password
digest: MD5:6e120743ad67abfbc385bc2bb754e297,kafka-users

LDAP authentication

Example jaas.config:

ldaploginmodule {
   org.eclipse.jetty.plus.jaas.spi.LdapLoginModule required
   debug="true"
   contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
   hostname="ldap.example.com"
   port="389"
   bindDn="cn=Directory Manager"
   bindPassword="directory"
   authenticationMethod="simple"
   forceBindingLogin="false"
   userBaseDn="ou=people,dc=alcatel"
   userRdnAttribute="uid"
   userIdAttribute="uid"
   userPasswordAttribute="userPassword"
   userObjectClass="inetOrgPerson"
   roleBaseDn="ou=groups,dc=example,dc=com"
   roleNameAttribute="cn"
   roleMemberAttribute="uniqueMember"
   roleObjectClass="groupOfUniqueNames";
   };

Examples

Check back soon!

Slipway is the first step towards us releasing shortcut: an opinionated template for enterprise Clojure development.

License

Distributed under the MIT License.

Copyright (c) 2022 Factor House

Can you improve this documentation? These fine people already did:
Thomas Crowley, Derek Troy-West & d-t-w
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close