mirror of
https://github.com/penpot/penpot.git
synced 2026-11-02 16:10:42 +00:00
Verify the OIDC state token once per callback and reuse the parsed claims, removing the duplicate verification while preserving the invalid-state error redirect. Test that the trusted-origin middleware is registered in the root router chain and ordered before auth and errors, and cover the OIDC logout post-logout redirect with and without a trusted origin. Warn at startup about malformed PENPOT_TRUSTED_ORIGINS entries, unify the request stamp keyword spelling, and clean up the test helpers. Closes #11625 AI-assisted-by: deepseek-v4.1-flash
303 lines
9.9 KiB
Clojure
303 lines
9.9 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns app.http
|
|
(:require
|
|
[app.auth.oidc :as-alias oidc]
|
|
[app.common.data :as d]
|
|
[app.common.logging :as l]
|
|
[app.common.schema :as sm]
|
|
[app.common.transit :as t]
|
|
[app.db :as-alias db]
|
|
[app.http.access-token :as actoken]
|
|
[app.http.assets :as-alias assets]
|
|
[app.http.awsns :as-alias awsns]
|
|
[app.http.debug :as-alias debug]
|
|
[app.http.errors :as errors]
|
|
[app.http.link-preview :as-alias link-preview]
|
|
[app.http.management :as mgmt]
|
|
[app.http.middleware :as mw]
|
|
[app.http.security :as sec]
|
|
[app.http.session :as session]
|
|
[app.http.websocket :as-alias ws]
|
|
[app.main :as-alias main]
|
|
[app.metrics :as mtx]
|
|
[app.rpc :as-alias rpc]
|
|
[app.setup :as-alias setup]
|
|
[integrant.core :as ig]
|
|
[promesa.exec :as px]
|
|
[reitit.core :as r]
|
|
[reitit.middleware :as rr]
|
|
[yetti.adapter :as yt]
|
|
[yetti.request :as yreq]
|
|
[yetti.response :as-alias yres])
|
|
(:import
|
|
io.undertow.server.ConnectorStatistics
|
|
io.undertow.Undertow
|
|
org.xnio.management.XnioWorkerMXBean))
|
|
|
|
(declare router-handler)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; HTTP SERVER
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def default-params
|
|
{::port 6060
|
|
::host "0.0.0.0"
|
|
::max-body-size 367001600 ; default 350 MiB
|
|
})
|
|
|
|
(def ^:private metrics-sample-interval-ms 15000)
|
|
|
|
(defn sample-worker-metrics
|
|
"Publishes the current state of the xnio worker thread pool (the
|
|
request dispatch queue and its threads) as gauges."
|
|
[metrics ^XnioWorkerMXBean mxbean]
|
|
(let [queue-size (.getWorkerQueueSize mxbean)
|
|
busy-count (.getBusyWorkerThreadCount mxbean)
|
|
pool-size (.getWorkerPoolSize mxbean)
|
|
max-size (.getMaxWorkerPoolSize mxbean)]
|
|
|
|
;; negative values are missing measurements, not zeros: the xnio
|
|
;; MXBean may transiently report -1 on the busy thread count.
|
|
(when (>= queue-size 0)
|
|
(mtx/run! metrics :id :http-worker-queue-size :val queue-size))
|
|
|
|
(when (>= busy-count 0)
|
|
(mtx/run! metrics :id :http-worker-busy-threads :val busy-count))
|
|
|
|
(when (>= pool-size 0)
|
|
(mtx/run! metrics :id :http-worker-pool-size :val pool-size))
|
|
|
|
(when (>= max-size 0)
|
|
(mtx/run! metrics :id :http-worker-max-pool-size :val max-size))))
|
|
|
|
(defn sample-connector-metrics
|
|
"Publishes the current state of the http listener connection
|
|
statistics. Undertow exposes absolute totals, so counters are
|
|
published as deltas of the last seen values (the atom state holds the
|
|
last observed totals). When a delta comes back negative (mainly
|
|
because the underlying counters were reset) the counter is skipped
|
|
and the reference updated."
|
|
[metrics state ^ConnectorStatistics cs]
|
|
(let [{:keys [last-requests last-errors]} (deref state)
|
|
total-requests (.getRequestCount cs)
|
|
total-errors (.getErrorCount cs)
|
|
delta-requests (max 0 (- total-requests last-requests))
|
|
delta-errors (max 0 (- total-errors last-errors))]
|
|
|
|
(when (pos? delta-requests)
|
|
(mtx/run! metrics :id :http-connector-requests-total :inc delta-requests))
|
|
|
|
(when (pos? delta-errors)
|
|
(mtx/run! metrics :id :http-connector-errors-total :inc delta-errors))
|
|
|
|
(mtx/run! metrics
|
|
:id :http-connector-active-connections
|
|
:val (.getActiveConnections cs))
|
|
|
|
(swap! state merge {:last-requests total-requests
|
|
:last-errors total-errors})))
|
|
|
|
(defn sample-http-metrics
|
|
"Samples the current state of the http server: worker thread pool
|
|
state and listener connection statistics. Called periodically by a
|
|
sampler that starts together with the server."
|
|
[metrics state ^Undertow server]
|
|
(try
|
|
(when-let [mxbean (some-> server (.getWorker) (.getMXBean))]
|
|
(sample-worker-metrics metrics mxbean))
|
|
|
|
(when-let [cs (some-> server (.getListenerInfo) (first) (.getConnectorStatistics))]
|
|
(sample-connector-metrics metrics state cs))
|
|
|
|
(catch Exception cause
|
|
(l/warn :msg "unexpected error on http metrics sampling"
|
|
:cause cause))))
|
|
|
|
(defn create-metrics-sampler
|
|
"Creates a daemon scheduler that periodically samples the state of
|
|
the http server and publishes it as metrics. A single thread is used,
|
|
and an unexpected error on a single sample does NOT cancel the
|
|
subsequent runs."
|
|
[^Undertow server metrics]
|
|
(let [state (atom {:last-requests 0 :last-errors 0})
|
|
scheduler (px/scheduled-executor
|
|
:parallelism 1
|
|
:factory (px/thread-factory :prefix "penpot/http-metrics/"
|
|
:daemon true))
|
|
sample (fn sample []
|
|
(try
|
|
(sample-http-metrics metrics state server)
|
|
(finally
|
|
;; reschedule even if a single sample fails, so
|
|
;; an unexpected error does not cancel the
|
|
;; following runs.
|
|
(px/schedule scheduler metrics-sample-interval-ms sample))))]
|
|
|
|
(px/schedule scheduler 0 sample)
|
|
scheduler))
|
|
|
|
(defmethod ig/expand-key ::server
|
|
[k v]
|
|
{k (merge default-params (d/without-nils v))})
|
|
|
|
(def ^:private schema:server-params
|
|
[:map
|
|
[::port ::sm/int]
|
|
[::host ::sm/text]
|
|
[::io-threads {:optional true} ::sm/int]
|
|
[::max-worker-threads {:optional true} ::sm/int]
|
|
[::max-body-size {:optional true} ::sm/int]
|
|
[::router {:optional true} [:fn r/router?]]
|
|
[::handler {:optional true} ::sm/fn]])
|
|
|
|
(defmethod ig/assert-key ::server
|
|
[_ params]
|
|
(assert (sm/check schema:server-params params)))
|
|
|
|
(defmethod ig/init-key ::server
|
|
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
|
|
(l/info :hint "starting http server" :port port :host host)
|
|
(let [on-dispatch
|
|
(fn [_ start-at-ns]
|
|
(let [timing (- (System/nanoTime) start-at-ns)
|
|
timing (int (/ timing 1000000))]
|
|
(mtx/run! metrics
|
|
:id :http-server-dispatch-timing
|
|
:val timing)))
|
|
|
|
options
|
|
{:http/port port
|
|
:http/host host
|
|
:http/max-body-size (::max-body-size cfg)
|
|
:http/max-multipart-body-size (::max-body-size cfg)
|
|
:xnio/direct-buffers false
|
|
:xnio/io-threads (::io-threads cfg)
|
|
:xnio/max-worker-threads (::max-worker-threads cfg)
|
|
:ring/compat :ring2
|
|
:server/statistics true
|
|
:events/on-dispatch on-dispatch
|
|
:socket/backlog 4069}
|
|
|
|
handler
|
|
(cond
|
|
(some? router)
|
|
(router-handler router)
|
|
|
|
(some? handler)
|
|
handler
|
|
|
|
:else
|
|
(throw (UnsupportedOperationException. "handler or router are required")))
|
|
|
|
server
|
|
(yt/start! (yt/server handler (d/without-nils options)))
|
|
|
|
sampler
|
|
(create-metrics-sampler server metrics)]
|
|
|
|
(assoc cfg ::server server ::metrics-sampler sampler)))
|
|
|
|
(defmethod ig/halt-key! ::server
|
|
[_ {:keys [::metrics-sampler ::server ::port] :as cfg}]
|
|
(l/info :msg "stopping http server" :port port)
|
|
(px/shutdown-now metrics-sampler)
|
|
(yt/stop! server))
|
|
|
|
(defn- not-found-handler
|
|
[_]
|
|
{::yres/status 404})
|
|
|
|
(defn- router-handler
|
|
[router]
|
|
(letfn [(resolve-handler [request]
|
|
(if-let [match (r/match-by-path router (yreq/path request))]
|
|
(let [params (:path-params match)
|
|
result (:result match)
|
|
handler (or (:handler result) not-found-handler)
|
|
request (assoc request :path-params params)]
|
|
(partial handler request))
|
|
(partial not-found-handler request)))
|
|
|
|
(on-error [cause request]
|
|
(let [{:keys [::yres/body] :as response} (errors/handle cause request)]
|
|
(cond-> response
|
|
(map? body)
|
|
(-> (update ::yres/headers assoc "content-type" "application/transit+json")
|
|
(assoc ::yres/body (t/encode-str body {:type :json-verbose}))))))]
|
|
|
|
(fn [request]
|
|
(let [handler (resolve-handler request)]
|
|
(try
|
|
(handler)
|
|
(catch Throwable cause
|
|
(on-error cause request)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; HTTP ROUTER
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(def ^:private schema:routes
|
|
[:vector :any])
|
|
|
|
(def ^:private schema:router-params
|
|
[:map
|
|
[::ws/routes schema:routes]
|
|
[::rpc/routes schema:routes]
|
|
[::oidc/routes schema:routes]
|
|
[::assets/routes schema:routes]
|
|
[::link-preview/routes schema:routes]
|
|
[::debug/routes schema:routes]
|
|
[::mtx/routes schema:routes]
|
|
[::awsns/routes schema:routes]
|
|
[::mgmt/routes schema:routes]
|
|
::session/manager
|
|
::setup/props
|
|
::db/pool])
|
|
|
|
(defmethod ig/assert-key ::router
|
|
[_ params]
|
|
(assert (sm/check schema:router-params params)))
|
|
|
|
(defn- root-middleware
|
|
"Root middleware chain applied to every route. Exposed as a function so
|
|
tests can assert the trusted-origin middleware is registered and ordered
|
|
so it wraps the rest of the chain."
|
|
[cfg]
|
|
[[mw/server-timing]
|
|
[mw/trusted-origin]
|
|
[sec/sec-fetch-metadata]
|
|
[mw/params]
|
|
[mw/format-response]
|
|
[mw/auth {:bearer (partial session/decode-token cfg)
|
|
:cookie (partial session/decode-token cfg)
|
|
:token (partial actoken/decode-token cfg)}]
|
|
[mw/parse-request]
|
|
[mw/errors errors/handle]
|
|
[mw/restrict-methods]])
|
|
|
|
(defmethod ig/init-key ::router
|
|
[_ cfg]
|
|
(rr/router
|
|
[["" {:middleware (root-middleware cfg)}
|
|
|
|
(::mtx/routes cfg)
|
|
(::assets/routes cfg)
|
|
(::link-preview/routes cfg)
|
|
(::debug/routes cfg)
|
|
|
|
["/webhooks"
|
|
(::awsns/routes cfg)]
|
|
|
|
["/management"
|
|
(::mgmt/routes cfg)]
|
|
|
|
(::ws/routes cfg)
|
|
(::oidc/routes cfg)
|
|
(::rpc/routes cfg)]]))
|