Skip to content

Commit

Permalink
support for hsts configuration + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
d-t-w committed Dec 9, 2024
1 parent 983049d commit f0a6db7
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 10 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,10 @@ Configuration of an HTTPS server connector.
: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? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"}
:sni-required? "true if SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "true if the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "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"}
```

### :slipway.handler.gzip
Expand Down
6 changes: 4 additions & 2 deletions common/src/slipway.clj
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
: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? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"}
:sni-required? "true if SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "true if the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "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"}

#: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 to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
Expand Down
19 changes: 14 additions & 5 deletions common/src/slipway/connector/https.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@
(org.eclipse.jetty.util.ssl SslContextFactory$Server)))

(defn default-config ^HttpConfiguration
[{::keys [port http-forwarded? sni-required? sni-host-check?] :or {sni-required? false sni-host-check? false}}]
(log/infof "sni required? %s, sni host check? %s" sni-required? sni-host-check?)
[{::keys [port http-forwarded? sni-required? sni-host-check? sts-max-age sts-include-subdomains?]
:or {sni-required? false
sni-host-check? false
sts-max-age -1
sts-include-subdomains? false}}]
(log/infof "sni required? %s, sni host check? %s, sts-max-age %s, sts-include-subdomains? %s"
sni-required? sni-host-check? sts-max-age sts-include-subdomains?)
(let [config (doto (HttpConfiguration.)
(.setSecurePort port)
(.setSendServerVersion false)
(.setSendDateHeader false)
(.addCustomizer (doto (SecureRequestCustomizer.)
(.setSniRequired sni-required?)
(.setSniHostCheck sni-host-check?))))]
(.setSniHostCheck sni-host-check?)
(.setStsMaxAge sts-max-age)
(.setStsIncludeSubDomains sts-include-subdomains?))))]
(when http-forwarded? (.addCustomizer config (ForwardedRequestCustomizer.)))
config))

Expand Down Expand Up @@ -98,8 +105,10 @@
: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? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"})
:sni-required? "true if SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "true if the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "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"})

(defmethod server/connector ::connector
[^Server server {::keys [host port idle-timeout proxy-protocol? http-config configurator]
Expand Down
17 changes: 17 additions & 0 deletions common/test/slipway/example.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
:truststore-password "password"
:truststore-type "PKCS12"})

(def hsts #::https{:sts-max-age 31536000
:sts-include-subdomains? true})

(def hsts-no-subdomains #::https{:sts-max-age 31536000})

(def hsts-no-max-age #::https{:sts-include-subdomains? true})

(def form-authenticator (FormAuthenticator. "/login" "/login-retry" false))

(def options
Expand All @@ -38,6 +45,16 @@
:https #::server{:connectors [https-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}

Expand Down
96 changes: 95 additions & 1 deletion common/test/slipway/https_server_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,98 @@
(-> (client/do-get "https" "user:wrong@localhost" 3443 "/user" {:insecure? true})
(select-keys of-interest)))))

(finally (example/stop!))))
(finally (example/stop!))))

(deftest strict-transport-security

(testing "hsts not configured"

(try
(example/start! [:https])

(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, User-Agent"}
:body (html/user-page {})}
(-> (client/do-get "https://localhost:3443/user" {:insecure? true})
(select-keys (conj of-interest :headers)))))

(finally (example/stop!))))

(testing "no hsts configuration"

(try
(example/start! [:https])

(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, User-Agent"}
:body (html/user-page {})}
(-> (client/do-get "https://localhost:3443/user" {:insecure? true})
(select-keys (conj of-interest :headers)))))

(finally (example/stop!))))

(testing "sts-max-age and subdomains"

(try
(example/start! [:hsts])

(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"
"Strict-Transport-Security" "max-age=31536000; includeSubDomains"
"Vary" "Accept-Encoding, User-Agent"}
:body (html/user-page {})}
(-> (client/do-get "https://localhost:3443/user" {:insecure? true})
(select-keys (conj of-interest :headers)))))

(finally (example/stop!))))

(testing "sts-include without subdomains"

(try
(example/start! [:hsts-no-subdomains])

(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"
"Strict-Transport-Security" "max-age=31536000"
"Vary" "Accept-Encoding, User-Agent"}
:body (html/user-page {})}
(-> (client/do-get "https://localhost:3443/user" {:insecure? true})
(select-keys (conj of-interest :headers)))))

(finally (example/stop!))))

(testing "hsts no max age (incorrect configuration, no header included)"

(try
(example/start! [:hsts-no-max-age])

(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, User-Agent"}
:body (html/user-page {})}
(-> (client/do-get "https://localhost:3443/user" {:insecure? true})
(select-keys (conj of-interest :headers)))))

(finally (example/stop!)))))

0 comments on commit f0a6db7

Please sign in to comment.