mirror of
https://github.com/penpot/penpot.git
synced 2026-05-22 08:23:42 +00:00
308 lines
11 KiB
Clojure
308 lines
11 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) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
|
|
|
(ns uxbox.http.middleware
|
|
(:require
|
|
[clojure.spec.alpha :as s]
|
|
[clojure.java.io :as io]
|
|
[cuerdas.core :as str]
|
|
[promesa.core :as p]
|
|
[reitit.ring :as rr]
|
|
[reitit.ring.middleware.exception :as exception]
|
|
[reitit.ring.middleware.multipart :as multipart]
|
|
[reitit.ring.middleware.parameters :as parameters]
|
|
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
|
[ring.middleware.session :refer [wrap-session]]
|
|
[ring.middleware.session.cookie :refer [cookie-store]]
|
|
[struct.core :as st]
|
|
[uxbox.config :as cfg]
|
|
[uxbox.http.cors :refer [wrap-cors]]
|
|
[uxbox.http.errors :as errors]
|
|
[uxbox.http.etag :refer [wrap-etag]]
|
|
[uxbox.http.response :as rsp]
|
|
[uxbox.util.data :refer [normalize-attrs]]
|
|
[uxbox.util.exceptions :as ex]
|
|
[uxbox.util.spec :as us]
|
|
[uxbox.util.transit :as t]))
|
|
|
|
(extend-protocol ring.core.protocols/StreamableResponseBody
|
|
(Class/forName "[B")
|
|
(write-body-to-stream [body _ ^java.io.OutputStream output-stream]
|
|
(with-open [out output-stream]
|
|
(.write out ^bytes body))))
|
|
|
|
(defn- transform-handler
|
|
[handler]
|
|
(fn [request respond raise]
|
|
(try
|
|
(let [response (handler request)]
|
|
(if (p/promise? response)
|
|
(-> response
|
|
(p/then respond)
|
|
(p/catch raise))
|
|
(respond response)))
|
|
(catch Exception e
|
|
(raise e)))))
|
|
|
|
;; The middleware that transform string keys to keywords and perform
|
|
;; usability transformations.
|
|
|
|
(def ^:private normalize-params-middleware
|
|
{:name ::normalize-params-middleware
|
|
:wrap (fn [handler]
|
|
(letfn [(transform-request [request key]
|
|
(if-let [data (get request key)]
|
|
(assoc request key (normalize-attrs data))
|
|
request))
|
|
(transform [request]
|
|
(-> request
|
|
(transform-request :query-params)
|
|
(transform-request :multipart-params)))]
|
|
(fn
|
|
([request] (handler (transform request)))
|
|
([request respond raise]
|
|
(try
|
|
(try
|
|
(handler (transform request) respond raise)
|
|
(catch Exception e
|
|
(raise e))))))))})
|
|
|
|
(def ^:private multipart-params-middleware
|
|
{:name ::multipart-params-middleware
|
|
:wrap wrap-multipart-params})
|
|
|
|
(def ^:private parameters-validation-middleware
|
|
(letfn [(prepare [parameters]
|
|
(reduce-kv
|
|
(fn [acc key spec]
|
|
(let [newkey (case key
|
|
:path :path-params
|
|
:query :query-params
|
|
:body :body-params
|
|
:multipart :multipart-params
|
|
(throw (ex-info "Not supported key on :parameters" {})))]
|
|
(assoc acc newkey {:key key
|
|
:fn #(st/validate spec %)})))
|
|
{} parameters))
|
|
|
|
(validate [request parameters debug]
|
|
(reduce-kv
|
|
(fn [req key spec]
|
|
(let [[errors, result] ((:fn spec) (get req key))]
|
|
(if errors
|
|
(ex/raise :type :validation
|
|
:code (:key spec)
|
|
:context errors
|
|
:prop key
|
|
:value (get req key)
|
|
:message "Invalid data")
|
|
(assoc-in req [:parameters (:key spec)] result))))
|
|
request parameters))
|
|
|
|
(compile-struct [route opts parameters]
|
|
(let [parameters (prepare parameters)]
|
|
(fn [handler]
|
|
(fn
|
|
([request]
|
|
(handler (validate request parameters)))
|
|
([request respond raise]
|
|
(try
|
|
(handler (validate request parameters false) respond raise)
|
|
(catch Exception e
|
|
(raise e))))))))
|
|
|
|
(prepare-spec [parameters]
|
|
(reduce-kv (fn [acc key s]
|
|
(let [rk (case key
|
|
:path :path-params
|
|
:query :query-params
|
|
:body :body-params
|
|
:multipart :multipart-params
|
|
(throw (ex-info "Not supported key on :parameters" {})))]
|
|
(assoc acc rk {:key key
|
|
:fn #(us/conform s %)})))
|
|
{}
|
|
parameters))
|
|
|
|
(validate-spec [request parameters]
|
|
(reduce-kv
|
|
(fn [req key spec]
|
|
(let [[result errors] ((:fn spec) (get req key))]
|
|
(if errors
|
|
(ex/raise :type :validation
|
|
:code :parameters
|
|
:context {:problems (vec (::s/problems errors))
|
|
:spec (::s/spec errors)
|
|
:value (::s/value errors)})
|
|
(assoc-in req [:parameters (:key spec)] result))))
|
|
request parameters))
|
|
|
|
(compile-spec [route opts parameters]
|
|
(let [parameters (prepare-spec parameters)]
|
|
(fn [handler]
|
|
(fn
|
|
([request]
|
|
(handler (validate-spec request parameters)))
|
|
([request respond raise]
|
|
(try
|
|
(handler (validate-spec request parameters) respond raise)
|
|
(catch Exception e
|
|
(raise e))))))))
|
|
|
|
(compile [route opts]
|
|
(when-let [parameters (:parameters route)]
|
|
(if (= :spec (:validation route))
|
|
(compile-spec route opts parameters)
|
|
(compile-struct route opts parameters))))]
|
|
{:name ::parameters-validation-middleware
|
|
:compile compile}))
|
|
|
|
(def ^:private session-middleware
|
|
(let [options {:store (cookie-store {:key "a 16-byte secret"})
|
|
:cookie-name "session"
|
|
:cookie-attrs {:same-site :lax
|
|
:http-only false}}]
|
|
{:name ::session-middleware
|
|
:wrap #(wrap-session % options)}))
|
|
|
|
(def cors-conf
|
|
{:origin #{"http://localhost:3449"}
|
|
:max-age 3600
|
|
:allow-credentials true
|
|
:allow-methods #{:post :put :get :delete}
|
|
:allow-headers #{:x-requested-with :content-type :cookie}})
|
|
|
|
(def ^:private cors-middleware
|
|
{:name ::cors-middleware
|
|
:wrap (fn [handler]
|
|
(let [cors (:http-server-cors cfg/config)]
|
|
(if (string? cors)
|
|
(->> (assoc cors-conf :origin #{cors})
|
|
(wrap-cors handler))
|
|
handler)))})
|
|
|
|
(def ^:private etag-middleware
|
|
{:name ::etag-middleware
|
|
:wrap wrap-etag})
|
|
|
|
(def ^:private exception-middleware
|
|
(exception/create-exception-middleware
|
|
(assoc exception/default-handlers
|
|
:muuntaja/decode errors/errors-handler
|
|
::exception/default errors/errors-handler)))
|
|
|
|
(def authorization-middleware
|
|
{:name ::authorization-middleware
|
|
:wrap (fn [handler]
|
|
(fn
|
|
([request]
|
|
(if-let [identity (get-in request [:session :user-id])]
|
|
(handler (assoc request :identity identity :user identity))
|
|
(rsp/forbidden nil)))
|
|
([request respond raise]
|
|
(if-let [identity (get-in request [:session :user-id])]
|
|
(handler (assoc request :identity identity :user identity) respond raise)
|
|
(respond (rsp/forbidden nil))))))})
|
|
|
|
(def format-response-middleware
|
|
(letfn [(process-response [{:keys [body] :as rsp}]
|
|
(if (coll? body)
|
|
(let [body (t/encode body {:type :json-verbose})]
|
|
(-> rsp
|
|
(assoc :body body)
|
|
(update :headers assoc "content-type" "application/transit+json")))
|
|
rsp))]
|
|
{:name ::format-response-middleware
|
|
:wrap (fn [handler]
|
|
(fn
|
|
([request]
|
|
(process-response (handler request)))
|
|
([request respond raise]
|
|
(handler request (fn [res] (respond (process-response res))) raise))))}))
|
|
|
|
(def parse-request-middleware
|
|
(letfn [(get-content-type [request]
|
|
(or (:content-type request)
|
|
(get (:headers request) "content-type")))
|
|
|
|
(slurp-bytes [body]
|
|
(with-open [input (io/input-stream body)
|
|
output (java.io.ByteArrayOutputStream. (.available input))]
|
|
(io/copy input output)
|
|
(.toByteArray output)))
|
|
|
|
(parse-body [body]
|
|
(let [^bytes body (slurp-bytes body)]
|
|
(when (pos? (alength body))
|
|
(t/decode body))))
|
|
|
|
(process-request [request]
|
|
(let [ctype (get-content-type request)]
|
|
(if (= "application/transit+json" ctype)
|
|
(try
|
|
(let [body (parse-body (:body request))]
|
|
(assoc request :body-params body))
|
|
(catch Exception e
|
|
(ex/raise :type :parse
|
|
:message "Unable to parse transit from request body."
|
|
:cause e)))
|
|
request)))]
|
|
|
|
{:name ::parse-request-middleware
|
|
:wrap (fn [handler]
|
|
(fn
|
|
([request]
|
|
(handler (process-request request)))
|
|
([request respond raise]
|
|
(let [^HttpInput body (:body request)]
|
|
(try
|
|
(handler (process-request request) respond raise)
|
|
(catch Exception e
|
|
(raise e)))))))}))
|
|
|
|
(def middleware
|
|
[cors-middleware
|
|
session-middleware
|
|
|
|
;; etag
|
|
etag-middleware
|
|
|
|
parameters/parameters-middleware
|
|
|
|
;; Format the body into transit
|
|
format-response-middleware
|
|
|
|
;; main exception handling
|
|
exception-middleware
|
|
|
|
;; parse transit format from request body
|
|
parse-request-middleware
|
|
|
|
;; multipart parsing
|
|
multipart-params-middleware
|
|
|
|
;; parameters normalization
|
|
normalize-params-middleware
|
|
|
|
;; parameters validation
|
|
parameters-validation-middleware])
|
|
|
|
(defn handler
|
|
[invar]
|
|
(let [metadata (meta invar)
|
|
hlrdata (-> metadata
|
|
(dissoc :arglist :line :column :file :ns)
|
|
(assoc :handler (transform-handler (var-get invar))
|
|
:fullname (symbol (str (:ns metadata)) (str (:name metadata)))))]
|
|
(cond-> hlrdata
|
|
(:doc metadata) (assoc :description (:doc metadata)))))
|
|
|
|
(defn options-handler
|
|
[request respond raise]
|
|
(let [methods (->> request rr/get-match :result (keep (fn [[k v]] (if v k))))
|
|
allow (->> methods (map (comp str/upper name)) (str/join ","))]
|
|
(respond {:status 200, :body "", :headers {"Allow" allow}})))
|