/undertow

Clojure API to Undertow web server.

Primary LanguageClojureThe UnlicenseUnlicense

undertow

Clojure API to Undertow web server.

Clojars Project

cljdoc badge tests

Motivation

  • Embrace Undertow API, don't hide Undertow features behind layers of simplifying abstractions.
  • Decouple server configuration and concepts like ring handlers or pedestal interceptors.
  • Extend functionality using Clojure idioms.
  • Reuse Undertow's library of HTTP handlers.
  • Provide declarative description of server configuration.
  • Minimize the impact of implementation on performance.
  • Implement web security recommendations.

Companion projects

Web security

This library provides implementations:

  • Session cookie is HttpOnly with SameSite=Lax by default.

  • The handler/security implements:

    • Content-SecurityPolicy response header with report-uri handler.
    • Strict-Transport-Security response header.
    • Referrer-Policy response header.
    • X-Content-Type-Options response header (enabled by default).

Usage

Undertow server can be started using start and stopped with stop. The running server instance is java.io.Closeable so it can be used with with-open macro.

Server configuration

The start function accepts clojure map with options translated to corresponding calls of Undertow builder methods. The configuration map structure reflects Undertow’s Java API.

The minimal configuration has only :port and :handler keys (Undertow will start even with empty configuration, but it is pretty useless). This starts HTTP listener on 8080 port with default settings.

(server/start {:port 8080 :handler my-handler})

Listener configuration

Listener configuration is defined in a map of ports and listener options under the :port key.

Simple HTTP listener:

;; All three are the same:
{:port 8080}
{:port {8080 {}}}
{:port {8080 {:host "localhost"}}}

Every port can use its own handler instead of server’s one.

{:port {8081 {:handler my-handler-1}
        8082 {:handler my-handler-2}}}

HTTPS listener:

{:port {4242 {:https {:key-managers [] :trust-managers []}}}}
{:port {4242 {:https {:ssl-context my-ssl-context}}}}

NOTE: The AJP listener type is not available in declarative form.

Handler configuration

Let’s suppose there is a scenario:

  • Webapi handler on the "webapi.company.com" host.
  • Application specific handlers on the hosts "app1.company.com" and "app2.company.com".
  • Static resource handler for app hosts but not for webapi.
  • Websocket handler for app hosts but not for webapi.
  • HTTP sessions for app hosts but not for webapi, websockets and static resources.
  • Some fixed response headers.

The Undertow handler for this case can be configured in different ways:

(ns usage.handler-configuration
  (:require [strojure.undertow.handler :as handler]
            [strojure.undertow.server :as server])
  (:import (io.undertow.server HttpServerExchange)
           (io.undertow.util Headers)))

(defn- my-handler
  [label]
  (handler/with-exchange
    (fn [^HttpServerExchange e]
      (-> (.getResponseSender e)
          (.send (str "Dummy handler: " label))))))

(def ^:private websocket-callback
  {:on-connect (fn [{:keys [callback exchange channel]}] (comment callback exchange channel))
   :on-message (fn [{:keys [callback channel text]}] (comment callback channel text))
   :on-close (fn [{:keys [callback channel code reason]}] (comment callback channel code reason))
   :on-error (fn [{:keys [callback channel error]}] (comment callback channel error))})

(defn- set-content-type-options
  [^HttpServerExchange exchange]
  (let [headers (.getResponseHeaders exchange)]
    (when (.contains headers Headers/CONTENT_TYPE)
      (.put headers Headers/X_CONTENT_TYPE_OPTIONS "nosniff"))))

(defn imperative-handler-config
  "The handler configuration created by invocation of series of handler
  constructors."
  []
  ;; The chain of HTTP handler in reverse order.
  (-> (my-handler :default-handler)
      ;; The handlers for app hostnames.
      (handler/virtual-host
        {:host {"app1.company.com" (my-handler :app1-handler)
                "app2.company.com" (my-handler :app2-handler)}})
      ;; Enable sessions for next handlers (above).
      (handler/session {})
      ;; Path specific handlers.
      (handler/path {:prefix {"static" (handler/resource {:resource-manager :classpath-files
                                                          :prefix "public/static"})}
                     :exact {"websocket" (handler/websocket websocket-callback)}})
      ;; Modify response before commit.
      (handler/on-response-commit set-content-type-options)
      ;; Add fixed response headers.
      (handler/set-response-header {"X-Frame-Options" "DENY"})
      ;; The handler for webapi hostname.
      (handler/virtual-host {:host {"webapi.company.com" (my-handler :webapi-handler)}})
      ;; Supplemental useful handlers.
      (handler/simple-error-page)
      (handler/proxy-peer-address)))

(defn symbol-handler-config
  "Declarative handler configuration as sequence of chaining handlers which are
  referred as symbols."
  []
  [;; Supplemental useful handlers.
   {:type `handler/proxy-peer-address}
   {:type `handler/simple-error-page}
   ;; The handler for webapi hostname.
   {:type `handler/virtual-host
    :host {"webapi.company.com" (my-handler :webapi-handler)}}
   ;; Add fixed response headers.
   {:type `handler/set-response-header :header {"X-Frame-Options" "DENY"}}
   ;; Modify response before commit.
   {:type `handler/on-response-commit :listener set-content-type-options}
   ;; Path specific handlers.
   {:type `handler/path
    :prefix {"static" {:type `handler/resource :resource-manager :classpath-files
                       :prefix "public/static"}}
    :exact {"websocket" {:type `handler/websocket :callback websocket-callback}}}
   ;; Enable sessions for next handlers.
   {:type `handler/session}
   ;; The handlers for app hostnames.
   {:type `handler/virtual-host
    :host {"app1.company.com" (my-handler :app1-handler)
           "app2.company.com" (my-handler :app2-handler)}}
   ;; Last resort handler
   (my-handler :default-handler)])

(defn instance-handler-config
  "Declarative handler configuration as sequence of chaining handlers which are
  referred as handler function instances."
  []
  [;; Supplemental useful handlers.
   {:type handler/proxy-peer-address}
   {:type handler/simple-error-page}
   ;; The handler for webapi hostname.
   {:type handler/virtual-host
    :host {"webapi.company.com" (my-handler :webapi-handler)}}
   ;; Add fixed response headers.
   {:type handler/set-response-header :header {"X-Frame-Options" "DENY"}}
   ;; Modify response before commit.
   {:type handler/on-response-commit :listener set-content-type-options}
   ;; Path specific handlers.
   {:type handler/path
    :prefix {"static" {:type handler/resource :resource-manager :classpath-files
                       :prefix "public/static"}}
    :exact {"websocket" {:type handler/websocket :callback websocket-callback}}}
   ;; Enable sessions for next handlers.
   {:type handler/session}
   ;; The handlers for app hostnames.
   {:type handler/virtual-host
    :host {"app1.company.com" (my-handler :app1-handler)
           "app2.company.com" (my-handler :app2-handler)}}
   ;; Last resort handler
   (my-handler :default-handler)])

(defn keyword-handler-config
  "Declarative handler configuration as sequence of chaining handlers which are
  referred as keywords so can be easily stored in EDN file."
  []
  [;; Supplemental useful handlers.
   {:type ::handler/proxy-peer-address}
   {:type ::handler/simple-error-page}
   ;; The handler for webapi hostname.
   {:type ::handler/virtual-host
    :host {"webapi.company.com" (my-handler :webapi-handler)}}
   ;; Add fixed response headers.
   {:type ::handler/set-response-header :header {"X-Frame-Options" "DENY"}}
   ;; Modify response before commit.
   {:type ::handler/on-response-commit :listener set-content-type-options}
   ;; Path specific handlers.
   {:type ::handler/path
    :prefix {"static" {:type ::handler/resource :resource-manager :classpath-files
                       :prefix "public/static"}}
    :exact {"websocket" {:type ::handler/websocket
                         :callback websocket-callback}}}
   ;; Enable sessions for next handlers.
   {:type ::handler/session}
   ;; The handlers for app hostnames.
   {:type ::handler/virtual-host
    :host {"app1.company.com" (my-handler :app1-handler)
           "app2.company.com" (my-handler :app2-handler)}}
   ;; Last resort handler
   (my-handler :default-handler)])

(comment
  (with-open [_ (server/start {:handler (imperative-handler-config)})])
  (with-open [_ (server/start {:handler (symbol-handler-config)})])
  (with-open [_ (server/start {:handler (instance-handler-config)})])
  (with-open [_ (server/start {:handler (keyword-handler-config)})])
  )

Authored by Sergey Trofimov.

license