Index: containerfiles/mylife.containerfile ================================================================== --- containerfiles/mylife.containerfile +++ containerfiles/mylife.containerfile @@ -1,9 +1,9 @@ FROM clojure:lein-2.10.0-alpine AS build WORKDIR /root COPY project.clj ./project.clj -RUN apk add --no-cache bash git +RUN apk add --no-cache bash git libstdc++ RUN lein deps RUN lein with-profile dev,test,uberjar deps COPY src ./src COPY src-dev ./src-dev @@ -26,6 +26,6 @@ RUN mkdir -p /usr/local/mylife/.server RUN apt-get update && apt-get install -y fontconfig libfreetype6 && apt-get clean COPY --from=build /root/target/uberjar/mylife.jar /usr/local/mylife -CMD java -XX:+UseZGC -XX:+ZGenerational -Xmx192m -jar /usr/local/mylife/mylife.jar +CMD java -XX:+UseZGC -XX:+ZGenerational -Xmx340m -jar /usr/local/mylife/mylife.jar Index: project.clj ================================================================== --- project.clj +++ project.clj @@ -8,10 +8,11 @@ :url "https://calmabiding.me/mylife" :license {:name "Affero GNU GPL 3.0" :url "https://www.gnu.org/licenses/agpl-3.0.en.html"} :repositories {"jitpack" {:url "https://jitpack.io"}} :dependencies [[org.clojure/clojure "1.11.1"] + [aleph "0.7.1"] [clj-cron-parse "0.1.5"] [clj-http "3.12.3"] [clj-rss "0.4.0"] [clojure.java-time "1.4.2"] [clojure-humanize "0.2.2"] @@ -26,11 +27,10 @@ [commons-io "2.15.1"] [conman "0.9.6"] [coreagile/defenv "3.0.0"] [expound "0.9.0"] [org.clj-commons/hickory "0.7.4"] - [info.sunng/ring-jetty9-adapter "0.31.1"] [io.sunshower.arcus/arcus-identicon "1.41.50.Final"] [jdbc-ring-session "1.5.3"] [markdown-clj "1.11.8"] [metosin/ring-http-response "0.9.3"] [mount "0.1.17"] Index: src/mylife/config.clj ================================================================== --- src/mylife/config.clj +++ src/mylife/config.clj @@ -449,10 +449,16 @@ (let [{:keys [domain ssl-port]} (env)] (if (= 443 ssl-port) (format "https://%s" domain) (format "https://%s:%s" domain ssl-port)))) +(defn internal-server-url [] + (let [{:keys [domain port]} (env)] + (if (= 80 port) + (format "http://%s" domain) + (format "http://%s:%s" domain port)))) + (defn external-connect-url [] (or (env :ext-url-override) (env :external-url) (server-url))) Index: src/mylife/core.clj ================================================================== --- src/mylife/core.clj +++ src/mylife/core.clj @@ -1,25 +1,34 @@ (ns mylife.core - (:require [clojure.java.io :as io] + (:require [aleph.http :as http] + [clojure.java.io :as io] [jdbc-ring-session.cleaner :refer [start-cleaner stop-cleaner]] [mount.core :as mount] [mylife.activity :refer [generate-available-activity-email]] - [mylife.config :refer [cfg-db env external-connect-url] :as cfg] + [mylife.config :refer [cfg-db + env + external-connect-url + internal-server-url] :as cfg] [mylife.data :as d] [mylife.disk-safety :as s] [mylife.email :as email] [mylife.scheduler :as sched] [mylife.ssl :as ssl] [mylife.time :as time] [mylife.web :as w] [mylife.webmention :as wm] - [ring.adapter.jetty9 :as jetty] [taoensso.timbre :as log]) (:gen-class) (:import (java.io File) (java.util UUID) - (org.eclipse.jetty.server Server))) + (io.netty.handler.ssl + ApplicationProtocolConfig + ApplicationProtocolConfig$Protocol + ApplicationProtocolConfig$SelectedListenerFailureBehavior + ApplicationProtocolConfig$SelectorFailureBehavior + ApplicationProtocolNames + SslContextBuilder))) (set! *warn-on-reflection* true) (Thread/setDefaultUncaughtExceptionHandler (reify Thread$UncaughtExceptionHandler @@ -35,17 +44,18 @@ Will try again when you start the server again!") (doseq [^File f [keystore keystore-password-file]] (.delete f)) (System/exit 1)) -(mount/defstate ^{:on-reload :noop} ssl-options +(mount/defstate ^{:on-reload :noop} keystore-fn :start (when (env :server-enabled?) (let [cert-dir (doto (-> :cert-dir env io/file) s/guarantee-secure-dir!) keystore (io/file cert-dir ".keystore") keystore-password-file (io/file cert-dir ".keystore-password") - keystore-path (.getAbsolutePath keystore)] + keystore-path (.getAbsolutePath keystore) + key-password-fn (fn [] (slurp keystore-password-file))] (if-not (and (.exists keystore) (.exists keystore-password-file)) (try (log/info "Generating self-signed SSL certificates") (let [new-password (str (UUID/randomUUID)) @@ -55,16 +65,19 @@ (spit keystore-password-file new-password) (ssl/save-single-cert! keystore private-key cert new-password)) (catch Exception e (ssl-err e keystore keystore-password-file))) (try - (ssl/validate-keystore keystore (slurp keystore-password-file)) + (ssl/validate-keystore keystore (key-password-fn)) (catch Exception e (ssl-err e keystore keystore-password-file)))) (fn [] - {:keystore keystore-path - :key-password-fn (fn [] (slurp keystore-password-file))})))) + {:key-manager-factory + (ssl/load-key-manager-factory keystore-path (key-password-fn)) + + :keystore-path keystore-path + :key-password-fn key-password-fn})))) (mount/defstate ^{:on-reload :noop} jdbc-session-cleaner :start (start-cleaner cfg-db) @@ -71,43 +84,73 @@ :stop (stop-cleaner jdbc-session-cleaner)) (declare stop-server) -(mount/defstate ^{:on-reload :noop} ^Server http-server +(mount/defstate ^{:on-reload :noop} http-server :start (when (env :server-enabled?) - (let [handler (w/app cfg-db)] - (jetty/run-jetty - handler - (let [{:keys [keystore key-password-fn]} (ssl-options)] - (merge {:join? false, :ssl? true, :h2? true, :async? false - :keystore keystore, :key-password (key-password-fn)} - (env)))))) + (let [handler (w/app cfg-db) + {:keys [port ssl-port]} (env) + + ^ApplicationProtocolConfig$SelectorFailureBehavior + selector-failure-behavior + ApplicationProtocolConfig$SelectorFailureBehavior/NO_ADVERTISE + + ^ApplicationProtocolConfig$SelectedListenerFailureBehavior + listener-failure-behavior + ApplicationProtocolConfig$SelectedListenerFailureBehavior/ACCEPT + + {:keys [key-manager-factory]} (keystore-fn) + + protocol-config + (ApplicationProtocolConfig. + ApplicationProtocolConfig$Protocol/ALPN + selector-failure-behavior + listener-failure-behavior + [ApplicationProtocolNames/HTTP_2 + ApplicationProtocolNames/HTTP_1_1]) + ssl-context + (-> (SslContextBuilder/forServer key-manager-factory) + (.applicationProtocolConfig protocol-config) + (.build))] + {:http (http/start-server handler {:port port}) + + :https + (http/start-server + handler + {:port ssl-port + :http-versions [:http2 :http1] + :ssl-context ssl-context})})) :stop (stop-server)) (defn stop-server [] - (when http-server (.stop http-server))) + (let [{:keys [http https]} http-server] + (when (and http https) + (.close http) + (.close https)))) (mount/defstate ^{:on-reload :noop} http-server-context :start (when (env :server-enabled?) - (log/infof "You can connect to %s" (external-connect-url)))) + (log/infof "You can connect to %s externally" (external-connect-url)) + (log/infof "You can connect to %s for internal configuration" + (internal-server-url)))) (defn- restart-web [] (log/info "Restarting web listener") (mount/stop - #'mylife.core/ssl-options + #'mylife.core/keystore-fn #'mylife.core/http-server) (mount/start - #'mylife.core/ssl-options + #'mylife.core/keystore-fn #'mylife.core/http-server) (log/info "Web listener restart complete")) (defn- check-acme-ssl [] - (let [{:keys [keystore key-password-fn]} (ssl-options)] + (let [{:keys [keystore-path key-password-fn]} (keystore-fn)] (try (ssl/guarantee-acme-cert! (merge (select-keys (env) [:domain :cert-dir :acme-server-uri]) {:new-cert-fn @@ -114,13 +157,13 @@ (fn [private-key cert-chain] (log/infof (str "Saving new LetsEncrypt certificate chain to '%s'. It may " "take up to a minute for the new cert to be available") - keystore) + keystore-path) (ssl/save-cert-chain! - keystore + keystore-path private-key cert-chain (key-password-fn)) (restart-web))})) Index: src/mylife/ssl.clj ================================================================== --- src/mylife/ssl.clj +++ src/mylife/ssl.clj @@ -7,10 +7,11 @@ PrivateKey KeyPair) (java.security.cert X509Certificate CertificateFactory) (java.time Instant) (java.time.temporal ChronoUnit) (java.util Date Collection) + (javax.net.ssl KeyManagerFactory) (org.bouncycastle.asn1.x500 X500Name) (org.bouncycastle.operator.jcajce JcaContentSignerBuilder) (org.bouncycastle.jce.provider BouncyCastleProvider) (org.bouncycastle.cert.jcajce JcaX509v3CertificateBuilder JcaX509CertificateConverter) @@ -25,10 +26,11 @@ (Security/addProvider (BouncyCastleProvider.)) (def ^:private acme-token (atom nil)) (def ^:private acme-content (atom nil)) +(def ^:private keystore-type "JKS") ;; "Borrowed" from (comment (str "https://github.com/neo4j/neo4j/blob/3.5/community/ssl/src/main/" "java/org/neo4j/ssl/PkiUtils.java#L94")) @@ -249,17 +251,18 @@ {:get {:handler get-acme-content}}]) (defn validate-keystore [^File keystore-file ^String password] (with-open [is (FileInputStream. keystore-file)] - (doto (KeyStore/getInstance "JKS") (.load is (.toCharArray password))))) + (doto (KeyStore/getInstance keystore-type) + (.load is (.toCharArray password))))) (defn save-cert-chain! [^String keystore-path ^PrivateKey private-key ^Collection certs ^String password] - (let [key-store (doto (KeyStore/getInstance "JKS") (.load nil nil)) + (let [key-store (doto (KeyStore/getInstance keystore-type) (.load nil nil)) password (.toCharArray password)] (.setKeyEntry key-store "certificate" private-key password @@ -270,5 +273,15 @@ (defn save-single-cert! [^String keystore-path ^PrivateKey private-key ^X509Certificate cert ^String password] (save-cert-chain! keystore-path private-key [cert] password)) + +(defn load-key-manager-factory [keystore-path keystore-password] + (with-open [keystore-is (io/input-stream keystore-path)] + (let [keystore-password (.toCharArray keystore-password) + + keystore + (doto (KeyStore/getInstance keystore-type) + (.load keystore-is keystore-password))] + (doto (KeyManagerFactory/getInstance "SunX509") + (.init keystore keystore-password)))))