Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Site:** on phones the header, menus and ability buttons fit the screen. (#711)
- **Server:** blank settings count as unset, Docker secrets work for the login key, and the Windows start scripts find their ports. (#701)
- **Server:** without email configured, registration is refused unless explicitly allowed. (#712)
- **Site:** returning visitors download the app again only when it has changed, and see a new release straight away.
- **Accounts:** site emails greet you by your preferred name, or by your username when you have
not set one, instead of "Hi there".
- **Characters:** the builder's tab bar wraps on narrow screens.
Expand Down Expand Up @@ -192,6 +193,7 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Accounts:** on a server whose JVM runs a non-English locale, an email address containing a capital I folded to a different account key, so sign-in, password reset, the unsubscribe link and email change could all miss the account.

- Sign-in, password reset, share links, image fetching and PDF export are hardened against misuse, and secrets are no longer printed at startup.
- **Site:** pages no longer load fonts from Google, so visiting the site no longer sends your address to Google.

## [breaking/2026-stack-modernization]

Expand Down
88 changes: 88 additions & 0 deletions resources/public/fonts/open-sans/OFL.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)

-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7 changes: 1 addition & 6 deletions resources/public/js/cookies.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
function Pop() {
var cssRuleFile = "/css/cookiestyles.css";
let lnk = document.createElement("link");
lnk.setAttribute("rel", "stylesheet");
lnk.setAttribute("type", "text/css");
lnk.setAttribute("href", cssRuleFile);
document.getElementsByTagName("head")[0].appendChild(lnk);
// Its stylesheet is linked by the page itself (index.clj), with the file's content stamp.
let styl = "undefined";
var conDivObj;
var fadeInTime = 10;
Expand Down
6 changes: 3 additions & 3 deletions src/clj/orcpub/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
"CSP that allows same-origin scripts without strict-dynamic.
Compatible with traditional <script src> tags. Less secure than strict mode."
{:default-src "'self'"
:script-src "'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com"
:style-src "'self' 'unsafe-inline' https://fonts.googleapis.com"
:font-src "'self' https://fonts.gstatic.com"
:script-src "'self' 'unsafe-inline' 'unsafe-eval'"
:style-src "'self' 'unsafe-inline'" ; + https://fonts.googleapis.com to use Google Fonts
:font-src "'self'" ; + https://fonts.gstatic.com to use Google Fonts
:img-src "'self' data: https:"
:object-src "'none'"})

Expand Down
8 changes: 5 additions & 3 deletions src/clj/orcpub/csp.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
Options: :dev-mode? adds ws://localhost:3449 to connect-src; :extra-connect-src and
:extra-frame-src are seqs of extra origins (from integrations).
script-src is 'strict-dynamic' plus the nonce, so only nonced scripts execute. Styles and
fonts also allow Google Fonts, images data: and https:; object-src is 'none'; everything
fonts are this site's own (inline styles allowed), images data: and https:; object-src is 'none'; everything
else, including base-uri, frame-ancestors and form-action, is 'self'."
[nonce & {:keys [dev-mode? extra-connect-src extra-frame-src]}]
(str "default-src 'self'; "
"script-src 'strict-dynamic' 'nonce-" nonce "'; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com; "
;; Open Sans is self-hosted. To serve it from Google Fonts again, add
;; https://fonts.googleapis.com to style-src and https://fonts.gstatic.com to font-src.
"style-src 'self' 'unsafe-inline'; "
"font-src 'self'; "
"img-src 'self' data: https:; "
"connect-src 'self'"
(when (seq extra-connect-src) (str " " (str/join " " extra-connect-src)))
Expand Down
27 changes: 18 additions & 9 deletions src/clj/orcpub/index.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@
[orcpub.favicon :as fi]
[orcpub.fork.integrations :as integrations]
[orcpub.loading-spinner :as spinner]
[orcpub.env :as env]))
[orcpub.env :as env]
[orcpub.config :as config]
[orcpub.pedestal :as pedestal]))


(defn asset-url
"`path` (a public file such as /js/compiled/orcpub.js) with ?v= its content stamp, so a new build
has a new address and can be cached for a year (pedestal/static-cache-control). The bare `path`
in dev mode, where Figwheel rewrites the file, or when the file is missing."
[path]
(if-let [v (when-not (config/dev-mode?) (pedestal/content-stamp path))]
(str path "?v=" v)
path))

(def homebrew-url
"URL to fetch server-hosted .orcbrew plugins from on first load.
Expand Down Expand Up @@ -174,7 +186,7 @@
:img "/favicon"
:xml "/favicon"
:ver "1"))
(include-css "/css/cookiestyles.css")
(include-css (asset-url "/css/cookiestyles.css"))
(script-tag {:nonce nonce}
(h/raw "document.documentElement.style.setProperty('--innerHeight', `${window.innerHeight}px`);
window.addEventListener('resize', () => document.documentElement.style.setProperty('--innerHeight', `${window.innerHeight}px`));"))
Expand Down Expand Up @@ -272,16 +284,13 @@ html {
;; It depends on nothing the app owns: server-rendered markup, inline styles (styles.css may
;; not have loaded), plain localStorage, and a vanilla anchor download, not FileSaver.
(boot-rescue nonce)
(include-css "/css/compiled/styles.css")
(include-css (asset-url "/css/compiled/styles.css"))
;; Every script tag carries the per-request nonce. It is nil in dev mode,
;; where no CSP header is set at all; enforcing otherwise.
(script-tag {:src "/js/compiled/orcpub.js" :nonce nonce})
(script-tag {:src "/js/cookies.js" :nonce nonce})
(script-tag {:src (asset-url "/js/compiled/orcpub.js") :nonce nonce})
(script-tag {:src (asset-url "/js/cookies.js") :nonce nonce})
(include-css "/assets/font-awesome/5.13.1/css/all.min.css")
;; 400, 600 and 700: the stylesheet asks for bold on tabs, titles and labels, and with only
;; 400 loaded Chrome rendered all of it at regular weight. One variable file serves all
;; three, so the extra weights cost nothing over 400 alone. See docs/design/style-guide.md.
(include-css "https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap")
;; Open Sans is served from this site (styles.core/open-sans-faces), not Google Fonts.
(script-tag {:nonce nonce} (h/raw " window.start.init({Palette:\"palette7\",Mode:\"banner bottom\",})"))
(when homebrew-url
(script-tag {:nonce nonce}
Expand Down
45 changes: 45 additions & 0 deletions src/clj/orcpub/pedestal.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns orcpub.pedestal
(:require [com.stuartsierra.component :as component]
[clojure.java.io :as io]
[io.pedestal.http :as http]
[io.pedestal.interceptor :as interceptor]
[io.pedestal.log :as log]
Expand Down Expand Up @@ -79,6 +80,50 @@
;; Create the nonce interceptor with current dev-mode? setting
(def nonce-interceptor (make-nonce-interceptor (config/dev-mode?)))

(defn- read-content-stamp
"The first 12 hex digits of the SHA-1 of the public resource at `path`, or nil when it is missing."
[path]
(when-let [r (io/resource (str "public" path))]
(with-open [in (io/input-stream r)] (subs (sha1 in) 0 12))))

(def content-stamp
"The content stamp of public file `path` (read-content-stamp), remembered: files do not change
while the server runs. index/asset-url puts it in addresses as ?v=."
(memoize read-content-stamp))

(defn static-cache-control
"The Cache-Control for a static file response at `uri`, or nil to leave the response alone.
`stamp` is :current when the request's ?v= is this file's content stamp (kept a year: that
address names exactly this file), :stale when it names another build (rechecked), else nil.
Unstamped compiled files are rechecked every time; images, fonts and vendored assets kept a week."
[uri stamp]
(cond
(and (= :current stamp) (re-find #"^/(js|css)/" uri)) "public, max-age=31536000, immutable"
(and (= :stale stamp) (re-find #"^/(js|css)/" uri)) "no-cache"
(re-find #"^/(js|css)/compiled/" uri) "no-cache"
(re-find #"^/(image|fonts|assets|favicon)/" uri) "public, max-age=604800"
(re-find #"^/(js|css)/[^/]+$" uri) "public, max-age=86400"))

(defn make-static-cache-interceptor
"Interceptor that sets Cache-Control (static-cache-control) on a successful GET of a static file
whose response sets none. Does nothing when `dev-mode?`, where Figwheel rewrites the files.
GOTCHA: Pedestal's static files answer ahead of every route, so this must be FIRST in the chain
(system/with-static-caching) for its :leave to see them."
[dev-mode?]
(interceptor/interceptor
{:name ::static-cache
:leave (fn [ctx]
(let [{:keys [request-method uri query-string]} (:request ctx)
{:keys [status headers]} (:response ctx)
cc (when (and (not dev-mode?) (= :get request-method) (= 200 status)
(not (get headers "Cache-Control")))
(static-cache-control (str uri)
(when-let [v (some->> query-string (re-find #"(?:^|&)v=([^&]*)") second)]
(if (= v (content-stamp uri)) :current :stale))))]
(cond-> ctx cc (assoc-in [:response :headers "Cache-Control"] cc))))}))

(def static-cache-interceptor (make-static-cache-interceptor (config/dev-mode?)))

(def etag-interceptor
(interceptor/interceptor
{:name :etag-interceptor
Expand Down
28 changes: 27 additions & 1 deletion src/clj/orcpub/styles/core.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(ns orcpub.styles.core
(:require [garden.stylesheet :refer [at-media at-keyframes]]
(:require [garden.stylesheet :refer [at-media at-keyframes at-font-face]]
[garden.units :refer [px]]
[orcpub.constants :as const]
[garden.selectors :as s]
Expand Down Expand Up @@ -1774,8 +1774,34 @@
[:.details-column
{:max-width "500px"}])])

(def open-sans-subsets
"Open Sans character sets as [subset unicode-range], one variable-weight woff2 file each under
resources/public/fonts/open-sans/ (OFL, see OFL.txt there). A browser downloads only the files
whose range a page's text uses."
[["cyrillic-ext" "U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F"]
["cyrillic" "U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116"]
["greek-ext" "U+1F00-1FFF"]
["greek" "U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF"]
["hebrew" "U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F"]
["math" "U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-209F, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF"]
["symbols" "U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AE, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F8D0-1F8D8, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FAC6, U+1FAC8, U+1FACC-1FADD, U+1FADF-1FAEB, U+1FAEF-1FAFA, U+1FB00-1FBFF"]
["vietnamese" "U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB"]
["latin-ext" "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF"]
["latin" "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"]])

(def open-sans-faces
"@font-face rules serving Open Sans from this site, weights 400 to 700, one per character set."
(for [[subset unicode-range] open-sans-subsets]
(at-font-face {:font-family "Open Sans"
:font-style "normal"
:font-weight "400 700"
:font-display "swap"
:src (str "url(/fonts/open-sans/open-sans-" subset ".woff2) format(\"woff2\")")
:unicode-range unicode-range})))

(def app
(concat
open-sans-faces
[[:.character-builder-header
{:margin-bottom "19px"}]

Expand Down
7 changes: 7 additions & 0 deletions src/clj/orcpub/system.clj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
;; See orcpub.config/get-secure-headers-config
})

(defn with-static-caching
"The service map with pedestal/static-cache-interceptor at the front of its chain, ahead of the
static file handler."
[service-map]
(update service-map ::http/interceptors #(into [pedestal/static-cache-interceptor] %)))

(defn with-client-address
"The service map with the player's-address interceptor first in its chain, so every limit and
log line sees the player rather than the proxy (orcpub.client-address)."
Expand Down Expand Up @@ -142,6 +148,7 @@
prod-service-map
(when (= :dev env) dev-service-map-overrides))
true http/default-interceptors
true with-static-caching
true with-portrait-guard
true with-client-address
(= :dev env) http/dev-interceptors)
Expand Down
4 changes: 3 additions & 1 deletion src/cljs/orcpub/dnd/e5/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,9 @@
(dispatch [:route routes/dnd-e5-my-encounters-route]))

(def logo [:a {:href "/" } [:img.h-60.pointer
{:src branding/logo-path}]])
;; CSS mask-image (the portrait placeholder) fetches the same logo in CORS mode; a CORS-mode
;; img lets the browser answer both from one request.
{:src branding/logo-path :cross-origin "anonymous"}]])

(defn app-header []
(let [device-type @(subscribe [:device-type])
Expand Down
Loading
Loading