My Life

Changes On Branch aleph
Login

Changes On Branch aleph

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch aleph Excluding Merge-Ins

This is equivalent to a diff from 00de2fb20c to 7d5a77599c

2024-02-11
21:20
Added HTTP1 Leaf check-in: 7d5a77599c user: scstarkey tags: aleph
21:17
Borrowing some improvements from the aleph branch check-in: a94f0852b9 user: scstarkey tags: trunk
19:50
More memory check-in: 30d4dfba91 user: scstarkey tags: aleph
18:15
Testing aleph support check-in: b018440299 user: scstarkey tags: aleph
16:37
Much less memory usage check-in: 00de2fb20c user: scstarkey tags: trunk
2024-02-08
17:33
Many, including delete post, move login link, activity report emails And don't let us package with any test-refresh/focus tests And a bit less logging And preparation for allowing anybody to webmention even without the protocol check-in: 6c75bd0bdf user: scstarkey tags: trunk

Changes to containerfiles/mylife.containerfile.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







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
COPY src-prod ./src-prod
COPY test ./test
24
25
26
27
28
29
30
31

24
25
26
27
28
29
30

31







-
+
ENV HOT_RELOAD_ENABLED=false

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

Changes to project.clj.

1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

32
33
34
35
36
37
38












+


















-







(def slf4j-version "2.0.7")
(def acme4j-version "2.16")
(def reitit-version "0.6.0")
(def bouncy-version "1.75")

(defproject me.calmabiding.mylife "0.1.0-SNAPSHOT"
  :description "My Life. On the Web. In your RSS reader."
  :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"]
                 [com.draines/postal "2.0.5"]
                 [com.fzakaria/slf4j-timbre "0.4.1"]
                 [com.github.seancorfield/next.jdbc "1.3.909"]
                 [com.github.multiformats/java-multibase "1.1.1"]
                 [com.taoensso/timbre "6.3.1"]
                 [com.vladsch.flexmark/flexmark-all "0.64.8"]
                 [com.xtdb/xtdb-core "1.24.3"]
                 [com.xtdb/xtdb-jdbc "1.24.3"]
                 [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"]
                 [nano-id "1.1.0"]
                 [org.apache.xmlgraphics/batik-all "1.17"]

Changes to src/mylife/config.clj.

447
448
449
450
451
452
453






454
455
456
457
458
459
460
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466







+
+
+
+
+
+








(defn server-url []
  (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)))

(defn get-site-owner []

Changes to src/mylife/core.clj.

1

2

3
4
5
6




7
8
9
10
11
12
13
14
15
16
17
18
19
20







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46


47
48
49
50
51
52
53
54
55
56
57
58
59
60

61
62
63



64
65


66
67
68
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84
85
































86
87
88
89
90




91
92
93
94

95


96
97
98
99
100

101
102
103

104
105
106
107
108

109
110
111
112
113
114
115
116
117
118
119

120
121

122
123
124
125
126
127
128
1
2

3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

49
50
51
52
53
54

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76


77
78
79
80
81
82
83
84
85
86
87
88

89
90
91







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

128
129
130
131
132
133
134
135
136

137
138
139
140
141
142

143
144
145

146
147
148
149
150

151
152
153
154
155
156
157
158
159
160
161

162
163

164
165
166
167
168
169
170
171

+
-
+



-
+
+
+
+








-




-
+
+
+
+
+
+
+



















-
+





-
+
+













-
+



+
+
+
-
-
+
+










-
+


-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+
+
+
+




+
-
+
+




-
+


-
+




-
+










-
+

-
+







(ns mylife.core
  (:require [aleph.http :as http]
  (:require [clojure.java.io :as io]
            [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
    (uncaughtException [_ thread ex]
      (log/error {:what :uncaught-exception
                  :exception ex
                  :where (str "Uncaught exception on" (.getName thread))}))))

(defn- ssl-err [e keystore keystore-password-file]
  (log/error
    e
    "Unable to load certificates. Cleaning up possibly corrupt files.
    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))
                {:keys [private-key cert]} (ssl/self-signed-cert (env :domain))]
            (doseq [^File f [keystore keystore-password-file]]
              (s/new-secure-file! f))
            (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 []
        {:key-manager-factory
         (ssl/load-key-manager-factory keystore-path (key-password-fn))

        {:keystore keystore-path
         :key-password-fn (fn [] (slurp keystore-password-file))}))))
         :keystore-path keystore-path
         :key-password-fn key-password-fn}))))

(mount/defstate ^{:on-reload :noop} jdbc-session-cleaner
  :start
  (start-cleaner cfg-db)

  :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 externally" (external-connect-url))
    (log/infof "You can connect to %s" (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
           (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))}))
      (catch Throwable t
        (log/error t "Unable to generate SSL cert")))))

Changes to src/mylife/ssl.clj.

1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38











+


















+







(ns mylife.ssl
  (:require [clojure.java.io :as io]
            [mylife.disk-safety :as s]
            [taoensso.timbre :as log])
  (:import (java.io File FileWriter FileReader FileInputStream)
           (java.security KeyPairGenerator Security SecureRandom KeyStore
                          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)
           (org.shredzone.acme4j.util KeyPairUtils CSRBuilder)
           (org.shredzone.acme4j Session
                                 AccountBuilder
                                 Order
                                 Status
                                 Authorization)
           (org.shredzone.acme4j.challenge Http01Challenge)
           (org.shredzone.acme4j.exception AcmeRetryAfterException)))

(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"))

(defn self-signed-cert [domain]
247
248
249
250
251
252
253
254


255
256
257
258
259
260

261
262
263
264
265
266
267
268
269
270
271
272
273
274










249
250
251
252
253
254
255

256
257
258
259
260
261
262

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287







-
+
+





-
+














+
+
+
+
+
+
+
+
+
+
(defn acme-cert-route []
  ["/.well-known/acme-challenge/:token"
   {: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
                  (into-array X509Certificate certs))
    (with-open [out (io/output-stream keystore-path)]
      (.store key-store out password))))

(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)))))