Compare commits

...

1 commit

Author SHA1 Message Date
kloimhardt
7e5d10c084
Add features Ring, Reitit, Selmer (#672)
* modified deps.edn

* start adding ring middleware feature

* project.clj

* ring added

* correct features

* add ring response

* add reitit

* reitit namespace

* replace ring's default-store with GraalVM compatible version

* patch reitit's tri compiler

* patch ring default and response

* set ring and reitit flags in uberjar.bat

* uberjar.bat again

* set ring and reitit features true in uberjat.bat

* amend project.clj

* add ring.middleware.content-type

* add webjars

* add muuntaja

* deps.edn

* add http-response

* extend response

* add response content-type

* remove wrap-reload

* remove ring devel

* add SELMER

* add selmer/render

* add ring.util.request namespace

* Hint to to guestbook2 branch in README

* Hint to guestbook2 branch

* set exe to bb-web

* set artifact name to babashka-web

* set back version to 0.2.2

* set version to 0.2.3

* release version 0.2.2

* clean Readme

* clean readme

* replace babashka standard readme

* list features in readme

* extend readme

* fix uberjar from merge

* fix uberjar.bat from merge

* readme

* null change

* null change 2

* null 3

* zero 4

* zero 5

* zero 6

* Zero 7

* zero 8

* prepare for merge to upstream/master

* further prepare

* prep

* prep for pull

* pull prep 3

* cosmetics

* clean uberjar script

* fix typo in deps.edn

Co-authored-by: kloimhardt <kloimhardt@kloimhardts-MacBook-Air.local>
2020-12-13 22:47:42 +01:00
20 changed files with 1134 additions and 0 deletions

View file

@ -2,6 +2,7 @@
"feature-yaml" "feature-csv" "feature-transit" "feature-yaml" "feature-csv" "feature-transit"
"feature-java-time" "feature-java-nio" "feature-java-time" "feature-java-nio"
"feature-httpkit-client" "feature-httpkit-server" "feature-httpkit-client" "feature-httpkit-server"
"feature-ring" "feature-reitit" "feature-selmer"
"feature-lanterna" "feature-lanterna"
"sci/src" "babashka.curl/src" "pods/src" "sci/src" "babashka.curl/src" "pods/src"
"babashka.nrepl/src" "babashka.nrepl/src"
@ -20,6 +21,13 @@
clj-commons/clj-yaml {:mvn/version "0.7.2"} clj-commons/clj-yaml {:mvn/version "0.7.2"}
com.cognitect/transit-clj {:mvn/version "1.0.324"} com.cognitect/transit-clj {:mvn/version "1.0.324"}
nrepl/bencode {:mvn/version "1.1.0"} nrepl/bencode {:mvn/version "1.1.0"}
ring/ring-core {:mvn/version "1.8.1"}
ring/ring-defaults {:mvn/version "0.3.2"}
ring-webjars {:mvn/version"0.2.0"}
metosin/reitit {:mvn/version "0.5.5"}
metosin/muuntaja {:mvn/version "0.6.7"}
metosin/ring-http-response {:mvn/version "0.9.1"}
selmer {:mvn/version "1.12.29"}
seancorfield/next.jdbc {:mvn/version "1.1.610"} seancorfield/next.jdbc {:mvn/version "1.1.610"}
org.postgresql/postgresql {:mvn/version "42.2.18"} org.postgresql/postgresql {:mvn/version "42.2.18"}
org.hsqldb/hsqldb {:mvn/version "2.5.1"} org.hsqldb/hsqldb {:mvn/version "2.5.1"}

View file

@ -0,0 +1,10 @@
(ns babashka.impl.muuntaja-core
(:require [muuntaja.core :as m]
[sci.core :as sci :refer [copy-var]]))
(def mns (sci/create-ns 'muuntaja.core nil))
(def muuntaja-core-namespace
{:obj mns
'create (copy-var m/create mns)
'default-options (copy-var m/default-options mns)})

View file

@ -0,0 +1,10 @@
(ns babashka.impl.muuntaja-middleware
(:require [muuntaja.middleware :as m]
[sci.core :as sci :refer [copy-var]]))
(def mns (sci/create-ns 'muuntaja.middleware nil))
(def muuntaja-middleware-namespace
{:obj mns
'wrap-format (copy-var m/wrap-format mns)
'wrap-params (copy-var m/wrap-params mns)})

View file

@ -0,0 +1,13 @@
(ns babashka.impl.reitit-ring
(:require [reitit.ring :as ring]
[sci.core :as sci :refer [copy-var]]))
(def rns (sci/create-ns 'reitit.ring nil))
(def reitit-ring-namespace
{:obj rns
'ring-handler (copy-var ring/ring-handler rns)
'router (copy-var ring/router rns)
'routes (copy-var ring/routes rns)
'create-resource-handler (copy-var ring/create-resource-handler rns)
'create-default-handler (copy-var ring/create-default-handler rns)})

View file

@ -0,0 +1,452 @@
(ns reitit.trie
(:refer-clojure :exclude [compile])
(:require [clojure.string :as str]
[reitit.exception :as ex])
#?(:clj (:import [reitit Trie Trie$Match Trie$Matcher]
(java.net URLDecoder))))
(defn ^:no-doc into-set [x]
(cond
(or (set? x) (sequential? x)) (set x)
(nil? x) #{}
:else (conj #{} x)))
(defrecord Wild [value])
(defrecord CatchAll [value])
(defrecord Match [params data])
(defrecord Node [children wilds catch-all params data])
(defn wild? [x] (instance? Wild x))
(defn catch-all? [x] (instance? CatchAll x))
(defprotocol Matcher
(match [this i max path])
(view [this])
(depth ^long [this])
(length [this]))
(defprotocol TrieCompiler
(data-matcher [this params data])
(static-matcher [this path matcher])
(wild-matcher [this key end matcher])
(catch-all-matcher [this key params data])
(linear-matcher [this matchers ordered?])
(-pretty [this matcher])
(-path-matcher [this matcher]))
(defn- assoc-param [match k v]
(let [params (:params match)]
(assoc match :params (assoc params k v))))
;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix
(defn- common-prefix [s1 s2]
(let [max (min (count s1) (count s2))]
(loop [i 0]
(cond
;; full match
(> i max)
(subs s1 0 max)
;; partial match
(not= (get s1 i) (get s2 i))
(if-not (zero? i) (subs s1 0 i))
;; recur
:else (recur (inc i))))))
(defn- -keyword [s]
(if-let [^long i (str/index-of s "/")]
(keyword (subs s 0 i) (subs s (inc i)))
(keyword s)))
(defn split-path [s {:keys [syntax] :or {syntax #{:bracket :colon}}}]
(let [bracket? (-> syntax (into-set) :bracket)
colon? (-> syntax (into-set) :colon)
-static (fn [from to] (if-not (= from to) [(subs s from to)]))
-wild (fn [^long from to] [(->Wild (-keyword (subs s (inc from) to)))])
-catch-all (fn [^long from to] [(->CatchAll (keyword (subs s (inc from) to)))])]
(loop [ss nil, from 0, to 0]
(if (= to (count s))
(concat ss (-static from to))
(let [c (get s to)]
(cond
(and bracket? (= \{ c))
(let [^long to' (or (str/index-of s "}" to) (ex/fail! ::unclosed-brackets {:path s}))]
(if (= \* (get s (inc to)))
(recur (concat ss (-static from to) (-catch-all (inc to) to')) (long (inc to')) (long (inc to')))
(recur (concat ss (-static from to) (-wild to to')) (long (inc to')) (long (inc to')))))
(and colon? (= \: c))
(let [^long to' (or (str/index-of s "/" to) (count s))]
(if (= 1 (- to' to))
(recur ss from (inc to))
(recur (concat ss (-static from to) (-wild to to')) (long to') (long to'))))
(and colon? (= \* c))
(let [to' (count s)]
(recur (concat ss (-static from to) (-catch-all to to')) (long to') (long to')))
:else
(recur ss from (inc to))))))))
(defn join-path [xs]
(reduce
(fn [s x]
(str s (cond
(string? x) x
(instance? Wild x) (str "{" (-> x :value str (subs 1)) "}")
(instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}"))))
"" xs))
(defn normalize [s opts]
(-> s (split-path opts) (join-path)))
;;
;; Conflict Resolution
;;
(defn- -slice-start [[p1 :as p1s] [p2 :as p2s]]
(let [-split (fn [p]
(if-let [i (and p (str/index-of p "/"))]
[(subs p 0 i) (subs p i)]
[p]))
-slash (fn [cp p]
(cond
(not (string? cp)) [cp]
(and (string? cp) (not= (count cp) (count p))) [(subs p (count cp))]
(and (string? p) (not cp)) (-split p)))
-postcut (fn [[p :as pps]]
(let [^long i (and p (str/index-of p "/"))]
(if (and i (pos? i))
(concat [(subs p 0 i) (subs p i)] (rest pps))
pps)))
-tailcut (fn [cp [p :as ps]] (concat (-slash cp p) (rest ps)))]
(if (or (nil? p1) (nil? p2))
[(-postcut p1s) (-postcut p2s)]
(if-let [cp (and (string? p1) (string? p2) (common-prefix p1 p2))]
[(-tailcut cp p1s) (-tailcut cp p2s)]
[p1s p2s]))))
(defn- -slice-end [x xs]
(let [i (if (string? x) (str/index-of x "/"))]
(if (and (number? i) (pos? ^long i))
(concat [(subs x i)] xs)
xs)))
(defn conflicting-parts? [parts1 parts2]
(let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)]
(cond
(= s1 s2 nil) true
(or (nil? s1) (nil? s2)) false
(or (catch-all? s1) (catch-all? s2)) true
(or (wild? s1) (wild? s2)) (recur (-slice-end s1 ss1) (-slice-end s2 ss2))
(not= s1 s2) false
:else (recur ss1 ss2))))
(defn conflicting-paths? [path1 path2 opts]
(conflicting-parts? (split-path path1 opts) (split-path path2 opts)))
;;
;; Creating Tries
;;
(defn- -node [m]
(map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m)))
(defn- -insert [node [path & ps] fp params data]
(let [node' (cond
(nil? path)
(assoc node :data data :params params)
(instance? Wild path)
(let [next (first ps)]
(if (or (instance? Wild next) (instance? CatchAll next))
(ex/fail! ::following-parameters {:path fp, :parameters (map :value [path next])})
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps fp params data)))))
(instance? CatchAll path)
(assoc-in node [:catch-all path] (-node {:params params, :data data}))
(str/blank? path)
(-insert node ps fp params data)
:else
(or
(reduce
(fn [_ [p n]]
(if-let [cp (common-prefix p path)]
(if (= cp p)
;; insert into child node
(let [n' (-insert n (conj ps (subs path (count p))) fp params data)]
(reduced (assoc-in node [:children p] n')))
;; split child node
(let [rp (subs p (count cp))
rp' (subs path (count cp))
n' (-insert (-node {}) ps fp params data)
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil nil)]
(reduced (update node :children (fn [children]
(-> children
(dissoc p)
(assoc cp n'')))))))))
nil (:children node))
;; new child node
(assoc-in node [:children path] (-insert (-node {}) ps fp params data))))]
(if-let [child (get-in node' [:children ""])]
;; optimize by removing empty paths
(-> (merge-with merge (dissoc node' :data) child)
(update :children dissoc ""))
node')))
(defn- decode [path start end percent?]
(let [param (subs path start end)]
(if percent?
#?(:cljs (js/decodeURIComponent param)
:clj (URLDecoder/decode
(if (.contains ^String param "+")
(.replace ^String param "+" "%2B")
param)
"UTF-8"))
param)))
;;
;; Compilers
;;
(defn clojure-trie-compiler []
(reify
TrieCompiler
(data-matcher [_ params data]
(let [match (->Match params data)]
(reify Matcher
(match [_ i max _]
(if (= i max)
match))
(view [_] data)
(depth [_] 1)
(length [_]))))
(static-matcher [_ path matcher]
(let [size (count path)]
(reify Matcher
(match [_ i max p]
(if-not (< ^long max (+ ^long i size))
(loop [j 0]
(if (= j size)
(match matcher (+ ^long i size) max p)
(if (= (get p (+ ^long i j)) (get path j))
(recur (inc j)))))))
(view [_] [path (view matcher)])
(depth [_] (inc (depth matcher)))
(length [_] (count path)))))
(wild-matcher [_ key end matcher]
(reify Matcher
(match [_ i max path]
(if (and (< ^long i ^long max) (not= (get path i) end))
(loop [percent? false, j ^long i]
(if (= max j)
(if-let [match (match matcher max max path)]
(assoc-param match key (decode path i max percent?)))
(let [c ^char (get path j)]
(condp = c
end (if-let [match (match matcher j max path)]
(assoc-param match key (decode path i j percent?)))
\% (recur true (inc j))
(recur percent? (inc j))))))))
(view [_] [key (view matcher)])
(depth [_] (inc (depth matcher)))
(length [_])))
(catch-all-matcher [_ key params data]
(let [match (->Match params data)]
(reify Matcher
(match [_ i max path]
(if (<= ^long i ^long max) (assoc-param match key (decode path i max true))))
(view [_] [key [data]])
(depth [_] 1)
(length [_]))))
(linear-matcher [_ matchers ordered?]
(let [matchers (vec (if ordered? matchers (reverse (sort-by (juxt depth length) matchers))))
size (count matchers)]
(reify Matcher
(match [_ i max path]
(loop [j 0]
(if (< j size)
(or (match (get matchers j) i max path)
(recur (inc j))))))
(view [_] (mapv view matchers))
(depth [_] (inc ^long (apply max 0 (map depth matchers))))
(length [_]))))
(-pretty [_ matcher]
(view matcher))
(-path-matcher [_ matcher]
(fn [path]
(if-let [match (match matcher 0 (count path) path)]
(->Match (:params match) (:data match)))))))
#?(:clj
(defn java-trie-compiler []
(reify
TrieCompiler
(data-matcher [_ params data]
(Trie/dataMatcher params data))
(static-matcher [_ path matcher]
(Trie/staticMatcher ^String path ^Trie$Matcher matcher))
(wild-matcher [_ key end matcher]
(Trie/wildMatcher key (if end (Character. end)) matcher))
(catch-all-matcher [_ key params data]
(Trie/catchAllMatcher key params data))
(linear-matcher [_ matchers ordered?]
(Trie/linearMatcher matchers ordered?))
(-pretty [_ matcher]
(-> matcher str read-string eval))
(-path-matcher [_ matcher]
(fn [path]
(if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)]
(->Match (.params match) (.data match))))))))
;;
;; Managing Tries
;;
(defn- map-parameters [keys]
(zipmap keys (repeat nil)))
#?(:clj
(def record-parameters
"Memoized function to transform parameters into runtime generated Record."
(memoize
(fn [keys]
(if (some qualified-keyword? keys)
(map-parameters keys)
(let [sym (gensym "PathParams")
ctor (symbol (str "map->" sym))]
(binding [*ns* (find-ns 'user)]
(eval `(do (defrecord ~sym ~(mapv (comp symbol name) keys)) (~ctor {}))))))))))
(defn insert
"Returns a trie with routes added to it."
([routes]
(insert nil routes))
([node routes]
(reduce
(fn [acc [p d]]
(insert acc p d))
node routes))
([node path data]
(insert node path data nil))
([node path data {::keys [parameters] :or {parameters map-parameters} :as opts}]
(let [parts (split-path path opts)
params (parameters (->> parts (remove string?) (map :value)))]
(-insert (or node (-node {})) (split-path path opts) path params data))))
(defn babashka-message [& _]
(println "Reitit compiler not supported by Babashka"))
(def compiler babashka-message)
(def compile babashka-message)
#_(defn compiler
"Returns a default [[TrieCompiler]]."
[]
#?(:cljs (clojure-trie-compiler)
:clj (java-trie-compiler)))
#_(defn compile
"Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]."
([options]
(compile options (compiler)))
([options compiler]
(compile options compiler []))
([{:keys [data params children wilds catch-all] :or {params {}}} compiler cp]
(let [ends (fn [{:keys [children]}] (or (keys children) ["/"]))
matchers (-> []
(cond-> data (conj (data-matcher compiler params data)))
(into (for [[p c] children] (static-matcher compiler p (compile c compiler (conj cp p)))))
(into
(for [[p c] wilds]
(let [pv (:value p)
ends (ends c)]
(if (next ends)
(ex/fail! ::multiple-terminators {:terminators ends, :path (join-path (conj cp p))})
(wild-matcher compiler pv (ffirst ends) (compile c compiler (conj cp pv)))))))
(into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))]
(cond
(> (count matchers) 1) (linear-matcher compiler matchers false)
(= (count matchers) 1) (first matchers)
:else (data-matcher compiler {} nil)))))
(defn pretty
"Returns a simplified EDN structure of a compiled trie for printing purposes."
([compiled-trie]
(pretty compiled-trie (compiler)))
([compiled-trie compiler]
(-pretty compiler compiled-trie)))
(defn path-matcher
"Returns a function of `path -> Match` from a compiled trie."
([compiled-trie]
(path-matcher compiled-trie (compiler)))
([compiled-trie compiler]
(-path-matcher compiler compiled-trie)))
;;
;; spike
;;
(comment
(->
[["/v2/whoami" 1]
["/v2/users/:user-id/datasets" 2]
["/v2/public/projects/:project-id/datasets" 3]
["/v1/public/topics/:topic" 4]
["/v1/users/:user-id/orgs/:org-id" 5]
["/v1/search/topics/:term" 6]
["/v1/users/:user-id/invitations" 7]
["/v1/users/:user-id/topics" 9]
["/v1/users/:user-id/bookmarks/followers" 10]
["/v2/datasets/:dataset-id" 11]
["/v1/orgs/:org-id/usage-stats" 12]
["/v1/orgs/:org-id/devices/:client-id" 13]
["/v1/messages/user/:user-id" 14]
["/v1/users/:user-id/devices" 15]
["/v1/public/users/:user-id" 16]
["/v1/orgs/:org-id/errors" 17]
["/v1/public/orgs/:org-id" 18]
["/v1/orgs/:org-id/invitations" 19]
["/v1/users/:user-id/device-errors" 22]
["/v2/login" 23]
["/v1/users/:user-id/usage-stats" 24]
["/v2/users/:user-id/devices" 25]
["/v1/users/:user-id/claim-device/:client-id" 26]
["/v2/public/projects/:project-id" 27]
["/v2/public/datasets/:dataset-id" 28]
["/v2/users/:user-id/topics/bulk" 29]
["/v1/messages/device/:client-id" 30]
["/v1/users/:user-id/owned-orgs" 31]
["/v1/topics/:topic" 32]
["/v1/users/:user-id/bookmark/:topic" 33]
["/v1/orgs/:org-id/members/:user-id" 34]
["/v1/users/:user-id/devices/:client-id" 35]
["/v1/users/:user-id" 36]
["/v1/orgs/:org-id/devices" 37]
["/v1/orgs/:org-id/members" 38]
["/v2/orgs/:org-id/topics" 40]
["/v1/whoami" 41]
["/v1/orgs/:org-id" 42]
["/v1/users/:user-id/api-key" 43]
["/v2/schemas" 44]
["/v2/users/:user-id/topics" 45]
["/v1/orgs/:org-id/confirm-membership/:token" 46]
["/v2/topics/:topic" 47]
["/v1/messages/topic/:topic" 48]
["/v1/users/:user-id/devices/:client-id/reset-password" 49]
["/v2/topics" 50]
["/v1/login" 51]
["/v1/users/:user-id/orgs" 52]
["/v2/public/messages/dataset/:dataset-id" 53]
["/v1/topics" 54]
["/v1/orgs" 55]
["/v1/users/:user-id/bookmarks" 56]
["/v1/orgs/:org-id/topics" 57]]
(insert)
(compile)
(pretty)))

View file

@ -0,0 +1,13 @@
(ns babashka.impl.ring-middleware-anti-forgery
(:require [ring.middleware.anti-forgery :as anti-forgery]
[sci.core :as sci :refer [copy-var]]))
(def ans (sci/create-ns 'ring.middleware.anti-forgery nil))
(defn get-anti-forgery-token []
anti-forgery/*anti-forgery-token*)
(def ring-middleware-anti-forgery-namespace
{:obj ans
'wrap-anti-forgery (copy-var anti-forgery/wrap-anti-forgery ans)
'get-anti-forgery-token (copy-var get-anti-forgery-token ans)})

View file

@ -0,0 +1,9 @@
(ns babashka.impl.ring-middleware-content-type
(:require [ring.middleware.content-type :as content-type]
[sci.core :as sci :refer [copy-var]]))
(def cns (sci/create-ns 'ring.middleware.content-type nil))
(def ring-middleware-content-type-namespace
{:obj cns
'wrap-content-type (copy-var content-type/wrap-content-type cns)})

View file

@ -0,0 +1,15 @@
(ns babashka.impl.ring-middleware-defaults
(:require [ring.middleware.defaults :as defaults]
[ring.middleware.multipart-params]
[ring.middleware.multipart-params.temp-file]
[sci.core :as sci :refer [copy-var]]))
(alter-var-root #'ring.middleware.multipart-params/default-store (constantly (delay ring.middleware.multipart-params.temp-file/temp-file-store)))
(def dns (sci/create-ns 'ring.middleware.defaults nil))
(def ring-middleware-defaults-namespace
{:obj dns
'wrap-defaults (copy-var defaults/wrap-defaults dns)
'api-defaults (copy-var defaults/api-defaults dns)
'site-defaults (copy-var defaults/site-defaults dns)})

View file

@ -0,0 +1,9 @@
(ns babashka.impl.ring-middleware-webjars
(:require [ring.middleware.webjars :as webjars]
[sci.core :as sci :refer [copy-var]]))
(def wns (sci/create-ns 'ring.middleware.webjars nil))
(def ring-middleware-webjars-namespace
{:obj wns
'wrap-webjars (copy-var webjars/wrap-webjars wns)})

View file

@ -0,0 +1,12 @@
(ns babashka.impl.ring-util-http-response
(:require [ring.util.http-response :as http-response]
[sci.core :as sci :refer [copy-var]]))
(def hns (sci/create-ns 'ring.util.http-response nil))
(def ring-util-http-response-namespace
{:obj hns
'ok (copy-var http-response/ok hns)
'content-type (copy-var http-response/content-type hns)
'bad-request (copy-var http-response/bad-request hns)
'internal-server-error (copy-var http-response/internal-server-error hns)})

View file

@ -0,0 +1,9 @@
(ns babashka.impl.ring-util-request
(:require [ring.util.request :as request]
[sci.core :as sci :refer [copy-var]]))
(def rns (sci/create-ns 'ring.util.request nil))
(def ring-util-request-namespace
{:obj rns
'body-string (copy-var request/body-string rns)})

View file

@ -0,0 +1,11 @@
(ns babashka.impl.ring-util-response
(:require [ring.util.response :as response]
[sci.core :as sci :refer [copy-var]]))
(def rns (sci/create-ns 'ring.util.response nil))
(def ring-util-response-namespace
{:obj rns
'response (copy-var response/response rns)
'resource-data (copy-var response/resource-data rns)
'header (copy-var response/header rns)})

View file

@ -0,0 +1,117 @@
(ns ring.middleware.defaults
"Middleware for providing a handler with sensible defaults."
(:require [ring.middleware.x-headers :as x]
[ring.middleware.flash :refer [wrap-flash]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.nested-params :refer [wrap-nested-params]]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.resource :refer [wrap-resource]]
[ring.middleware.file :refer [wrap-file]]
[ring.middleware.not-modified :refer [wrap-not-modified]]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.default-charset :refer [wrap-default-charset]]
[ring.middleware.absolute-redirects :refer [wrap-absolute-redirects]]
[ring.middleware.ssl :refer [wrap-ssl-redirect wrap-hsts wrap-forwarded-scheme]]
[ring.middleware.proxy-headers :refer [wrap-forwarded-remote-addr]]))
(def api-defaults
"A default configuration for a HTTP API."
{:params {:urlencoded true
:keywordize true}
:responses {:not-modified-responses true
:absolute-redirects true
:content-types true
:default-charset "utf-8"}})
(def secure-api-defaults
"A default configuration for a HTTP API that's accessed securely over HTTPS."
(-> api-defaults
(assoc-in [:security :ssl-redirect] true)
(assoc-in [:security :hsts] true)))
(def site-defaults
"A default configuration for a browser-accessible website, based on current
best practice."
{:params {:urlencoded true
:multipart true
:nested true
:keywordize true}
:cookies true
:session {:flash true
:cookie-attrs {:http-only true, :same-site :strict}}
:security {:anti-forgery true
:xss-protection {:enable? true, :mode :block}
:frame-options :sameorigin
:content-type-options :nosniff}
:static {:resources "public"}
:responses {:not-modified-responses true
:absolute-redirects true
:content-types true
:default-charset "utf-8"}})
(def secure-site-defaults
"A default configuration for a browser-accessible website that's accessed
securely over HTTPS."
(-> site-defaults
(assoc-in [:session :cookie-attrs :secure] true)
(assoc-in [:session :cookie-name] "secure-ring-session")
(assoc-in [:security :ssl-redirect] true)
(assoc-in [:security :hsts] true)))
(defn- wrap [handler middleware options]
(if (true? options)
(middleware handler)
(if options
(middleware handler options)
handler)))
(defn- wrap-multi [handler middleware args]
(wrap handler
(fn [handler args]
(if (coll? args)
(reduce middleware handler args)
(middleware handler args)))
args))
(defn- wrap-xss-protection [handler options]
(x/wrap-xss-protection handler (:enable? options true) (dissoc options :enable?)))
(defn- wrap-x-headers [handler options]
(-> handler
(wrap wrap-xss-protection (:xss-protection options false))
(wrap x/wrap-frame-options (:frame-options options false))
(wrap x/wrap-content-type-options (:content-type-options options false))))
(defn wrap-defaults
"Wraps a handler in default Ring middleware, as specified by the supplied
configuration map.
See: api-defaults
site-defaults
secure-api-defaults
secure-site-defaults"
[handler config]
(-> handler
(wrap wrap-anti-forgery (get-in config [:security :anti-forgery] false))
(wrap wrap-flash (get-in config [:session :flash] false))
(wrap wrap-session (:session config false))
(wrap wrap-keyword-params (get-in config [:params :keywordize] false))
(wrap wrap-nested-params (get-in config [:params :nested] false))
(wrap wrap-multipart-params (get-in config [:params :multipart] false))
(wrap wrap-params (get-in config [:params :urlencoded] false))
(wrap wrap-cookies (get-in config [:cookies] false))
(wrap wrap-absolute-redirects (get-in config [:responses :absolute-redirects] false))
(wrap-multi #(wrap-resource %1 %2 config) (get-in config [:static :resources] false))
(wrap-multi wrap-file (get-in config [:static :files] false))
(wrap wrap-content-type (get-in config [:responses :content-types] false))
(wrap wrap-default-charset (get-in config [:responses :default-charset] false))
(wrap wrap-not-modified (get-in config [:responses :not-modified-responses] false))
(wrap wrap-x-headers (:security config))
(wrap wrap-hsts (get-in config [:security :hsts] false))
(wrap wrap-ssl-redirect (get-in config [:security :ssl-redirect] false))
(wrap wrap-forwarded-scheme (boolean (:proxy config)))
(wrap wrap-forwarded-remote-addr (boolean (:proxy config)))))

View file

@ -0,0 +1,348 @@
(ns ring.util.response
"Functions for generating and augmenting response maps."
(:require [clojure.java.io :as io]
[clojure.string :as str]
[ring.util.io :refer [last-modified-date]]
[ring.util.parsing :as parsing]
[ring.util.time :refer [format-date]])
(:import [java.io File]
[java.util Date]
[java.net URL URLDecoder URLEncoder]))
(def ^{:added "1.4"} redirect-status-codes
"Map a keyword to a redirect status code."
{:moved-permanently 301
:found 302
:see-other 303
:temporary-redirect 307
:permanent-redirect 308})
(defn redirect
"Returns a Ring response for an HTTP 302 redirect. Status may be
a key in redirect-status-codes or a numeric code. Defaults to 302"
([url] (redirect url :found))
([url status]
{:status (redirect-status-codes status status)
:headers {"Location" url}
:body ""}))
(defn redirect-after-post
"Returns a Ring response for an HTTP 303 redirect. Deprecated in favor
of using redirect with a :see-other status."
{:deprecated "1.4"}
[url]
{:status 303
:headers {"Location" url}
:body ""})
(defn created
"Returns a Ring response for a HTTP 201 created response."
{:added "1.2"}
([url] (created url nil))
([url body]
{:status 201
:headers {"Location" url}
:body body}))
(defn bad-request
"Returns a 400 'bad request' response."
{:added "1.7"}
[body]
{:status 400
:headers {}
:body body})
(defn not-found
"Returns a 404 'not found' response."
{:added "1.1"}
[body]
{:status 404
:headers {}
:body body})
(defn response
"Returns a skeletal Ring response with the given body, status of 200, and no
headers."
[body]
{:status 200
:headers {}
:body body})
(defn status
"Returns an updated Ring response with the given status."
([status]
{:status status
:headers {}
:body nil})
([resp status]
(assoc resp :status status)))
(defn header
"Returns an updated Ring response with the specified header added."
[resp name value]
(assoc-in resp [:headers name] (str value)))
(defn- canonical-path ^String [^File file]
(str (.getCanonicalPath file)
(if (.isDirectory file) File/separatorChar)))
(defn- safe-path? [^String root ^String path]
(.startsWith (canonical-path (File. root path))
(canonical-path (File. root))))
(defn- directory-transversal?
"Check if a path contains '..'."
[^String path]
(-> (str/split path #"/|\\")
(set)
(contains? "..")))
(defn- find-file-named [^File dir ^String filename]
(let [path (File. dir filename)]
(if (.isFile path)
path)))
(defn- find-file-starting-with [^File dir ^String prefix]
(first
(filter
#(.startsWith (.toLowerCase (.getName ^File %)) prefix)
(.listFiles dir))))
(defn- find-index-file
"Search the directory for an index file."
[^File dir]
(or (find-file-named dir "index.html")
(find-file-named dir "index.htm")
(find-file-starting-with dir "index.")))
(defn- safely-find-file [^String path opts]
(if-let [^String root (:root opts)]
(if (or (safe-path? root path)
(and (:allow-symlinks? opts) (not (directory-transversal? path))))
(File. root path))
(File. path)))
(defn- find-file [^String path opts]
(if-let [^File file (safely-find-file path opts)]
(cond
(.isDirectory file)
(and (:index-files? opts true) (find-index-file file))
(.exists file)
file)))
(defn- file-data [^File file]
{:content file
:content-length (.length file)
:last-modified (last-modified-date file)})
(defn- content-length [resp len]
(if len
(header resp "Content-Length" len)
resp))
(defn- last-modified [resp last-mod]
(if last-mod
(header resp "Last-Modified" (format-date last-mod))
resp))
(defn file-response
"Returns a Ring response to serve a static file, or nil if an appropriate
file does not exist.
Options:
:root - take the filepath relative to this root path
:index-files? - look for index.* files in directories (defaults to true)
:allow-symlinks? - allow symlinks that lead to paths outside the root path
(defaults to false)"
([filepath]
(file-response filepath {}))
([filepath options]
(if-let [file (find-file filepath options)]
(let [data (file-data file)]
(-> (response (:content data))
(content-length (:content-length data))
(last-modified (:last-modified data)))))))
;; In Clojure 1.5.1, the as-file function does not correctly decode
;; UTF-8 byte sequences.
;;
;; See: http://dev.clojure.org/jira/browse/CLJ-1177
;;
;; As a work-around, we'll backport the fix from CLJ-1177 into
;; url-as-file.
(defn- ^File url-as-file [^java.net.URL u]
(-> (.getFile u)
(str/replace \/ File/separatorChar)
(str/replace "+" (URLEncoder/encode "+" "UTF-8"))
(URLDecoder/decode "UTF-8")
io/as-file))
(defn content-type
"Returns an updated Ring response with the a Content-Type header corresponding
to the given content-type."
[resp content-type]
(header resp "Content-Type" content-type))
(defn find-header
"Looks up a header in a Ring response (or request) case insensitively,
returning the header map entry, or nil if not present."
{:added "1.4"}
[resp ^String header-name]
(->> (:headers resp)
(filter #(.equalsIgnoreCase header-name (key %)))
(first)))
(defn get-header
"Looks up a header in a Ring response (or request) case insensitively,
returning the value of the header, or nil if not present."
{:added "1.2"}
[resp header-name]
(some-> resp (find-header header-name) val))
(defn update-header
"Looks up a header in a Ring response (or request) case insensitively,
then updates the header with the supplied function and arguments in the
manner of update-in."
{:added "1.4"}
[resp header-name f & args]
(let [header-key (or (some-> resp (find-header header-name) key) header-name)]
(update-in resp [:headers header-key] #(apply f % args))))
(defn charset
"Returns an updated Ring response with the supplied charset added to the
Content-Type header."
{:added "1.1"}
[resp charset]
(update-header resp "Content-Type"
(fn [content-type]
(-> (or content-type "text/plain")
(str/replace #";\s*charset=[^;]*" "")
(str "; charset=" charset)))))
(defn get-charset
"Gets the character encoding of a Ring response."
{:added "1.6"}
[resp]
(some-> (get-header resp "Content-Type")
parsing/find-content-type-charset))
(defn set-cookie
"Sets a cookie on the response. Requires the handler to be wrapped in the
wrap-cookies middleware."
{:added "1.1"}
[resp name value & [opts]]
(assoc-in resp [:cookies name] (merge {:value value} opts)))
(defn response?
"True if the supplied value is a valid response map."
{:added "1.1"}
[resp]
(and (map? resp)
(integer? (:status resp))
(map? (:headers resp))))
(defmulti resource-data
"Returns data about the resource specified by url, or nil if an
appropriate resource does not exist.
The return value is a map with optional values for:
:content - the content of the URL, suitable for use as the :body
of a ring response
:content-length - the length of the :content, nil if not available
:last-modified - the Date the :content was last modified, nil if not
available
This dispatches on the protocol of the URL as a keyword, and
implementations are provided for :file and :jar. If you are on a
platform where (Class/getResource) returns URLs with a different
protocol, you will need to provide an implementation for that
protocol.
This function is used internally by url-response."
{:arglists '([url]), :added "1.4"}
(fn [^java.net.URL url]
(keyword (.getProtocol url))))
(defmethod resource-data :file
[url]
(if-let [file (url-as-file url)]
(if-not (.isDirectory file)
(file-data file))))
(defn- add-ending-slash [^String path]
(if (.endsWith path "/")
path
(str path "/")))
(defn- jar-directory? [^java.net.JarURLConnection conn]
(let [jar-file (.getJarFile conn)
entry-name (.getEntryName conn)
dir-entry (.getEntry jar-file (add-ending-slash entry-name))]
(and dir-entry (.isDirectory dir-entry))))
(defn- connection-content-length [^java.net.URLConnection conn]
(let [len (.getContentLength conn)]
(if (<= 0 len) len)))
(defn- connection-last-modified [^java.net.URLConnection conn]
(let [last-mod (.getLastModified conn)]
(if-not (zero? last-mod)
(Date. last-mod))))
(defmethod resource-data :jar
[^java.net.URL url]
(let [conn (.openConnection url)]
(if-not (jar-directory? conn)
{:content (.getInputStream conn)
:content-length (connection-content-length conn)
:last-modified (connection-last-modified conn)})))
(defn url-response
"Return a response for the supplied URL."
{:added "1.2"}
[^URL url]
(if-let [data (resource-data url)]
(-> (response (:content data))
(content-length (:content-length data))
(last-modified (:last-modified data)))))
(defn- get-resources [path ^ClassLoader loader]
(-> (or loader (.getContextClassLoader (Thread/currentThread)))
(.getResources path)
(enumeration-seq)))
(defn- safe-file-resource? [{:keys [body]} {:keys [root loader allow-symlinks?]}]
(or allow-symlinks?
(nil? root)
(let [root (.replaceAll (str root) "^/" "")]
(or (str/blank? root)
(let [path (canonical-path body)]
(some #(and (= "file" (.getProtocol ^URL %))
(.startsWith path (canonical-path (url-as-file %))))
(get-resources root loader)))))))
(defn resource-response
"Returns a Ring response to serve a packaged resource, or nil if the
resource does not exist.
Options:
:root - take the resource relative to this root
:loader - resolve the resource in this class loader
:allow-symlinks? - allow symlinks that lead to paths outside the root
classpath directories (defaults to false)"
([path]
(resource-response path {}))
([path options]
(let [path (-> (str "/" path) (.replace "//" "/"))
root+path (-> (str (:root options) path) (.replaceAll "^/" ""))
;; babashka patch
io-resource-fn (or (get-in options [:static :io-resource-fn]) io/resource)
load #(if-let [loader (:loader options)]
(io-resource-fn % loader)
(io-resource-fn %))]
(if-not (directory-transversal? root+path)
(if-let [resource (load root+path)]
(let [response (url-response resource)]
(if (or (not (instance? File (:body response)))
;; babashka patch
true #_(safe-file-resource? response options))
response)))))))

View file

@ -0,0 +1,11 @@
(ns babashka.impl.selmer-parser
(:require [selmer.parser :as parser]
[sci.core :as sci :refer [copy-var]]))
(def pns (sci/create-ns 'selmer.parser nil))
(def selmer-parser-namespace
{:obj pns
'render-file (copy-var parser/render-file pns)
'render (copy-var parser/render pns)
'set-resource-path! (copy-var parser/set-resource-path! pns)})

View file

@ -44,6 +44,16 @@
:dependencies [[http-kit "2.5.0"]]} :dependencies [[http-kit "2.5.0"]]}
:feature/httpkit-server {:source-paths ["feature-httpkit-server"] :feature/httpkit-server {:source-paths ["feature-httpkit-server"]
:dependencies [[http-kit "2.5.0"]]} :dependencies [[http-kit "2.5.0"]]}
:feature/ring {:source-paths ["feature-ring"]
:dependencies [[ring/ring-core "1.8.1"]
[ring/ring-defaults "0.3.2"]
[ring-webjars "0.2.0"]]}
:feature/reitit {:source-paths ["feature-reitit"]
:dependencies [[metosin/reitit "0.5.5"]
[metosin/muuntaja "0.6.7"]
[metosin/ring-http-response "0.9.1"]]}
:feature/selmer {:source-paths ["feature-selmer"]
:dependencies [[selmer "1.12.29"]]}
:feature/lanterna {:source-paths ["feature-lanterna"] :feature/lanterna {:source-paths ["feature-lanterna"]
:dependencies [[babashka/clojure-lanterna "0.9.8-SNAPSHOT"]]} :dependencies [[babashka/clojure-lanterna "0.9.8-SNAPSHOT"]]}
@ -58,6 +68,9 @@
:feature/datascript :feature/datascript
:feature/httpkit-client :feature/httpkit-client
:feature/httpkit-server :feature/httpkit-server
:feature/ring
:feature/reitit
:feature/selmer
{:dependencies [[clj-commons/conch "0.9.2"] {:dependencies [[clj-commons/conch "0.9.2"]
[com.clojure-goes-fast/clj-async-profiler "0.4.1"] [com.clojure-goes-fast/clj-async-profiler "0.4.1"]
[com.opentable.components/otj-pg-embedded "0.13.3"]]}] [com.opentable.components/otj-pg-embedded "0.13.3"]]}]

View file

@ -110,6 +110,27 @@ else
BABASHKA_LEIN_PROFILES+=",-feature/httpkit-server" BABASHKA_LEIN_PROFILES+=",-feature/httpkit-server"
fi fi
if [ "$BABASHKA_FEATURE_RING" = "true" ]
then
BABASHKA_LEIN_PROFILES+=",+feature/ring"
else
BABASHKA_LEIN_PROFILES+=",-feature/ring"
fi
if [ "$BABASHKA_FEATURE_REITIT" = "true" ]
then
BABASHKA_LEIN_PROFILES+=",+feature/reitit"
else
BABASHKA_LEIN_PROFILES+=",-feature/reitit"
fi
if [ "$BABASHKA_FEATURE_SELMER" = "true" ]
then
BABASHKA_LEIN_PROFILES+=",+feature/selmer"
else
BABASHKA_LEIN_PROFILES+=",-feature/selmer"
fi
if [ "$BABASHKA_FEATURE_LANTERNA" = "true" ] if [ "$BABASHKA_FEATURE_LANTERNA" = "true" ]
then then
BABASHKA_LEIN_PROFILES+=",+feature/lanterna" BABASHKA_LEIN_PROFILES+=",+feature/lanterna"

View file

@ -82,6 +82,24 @@ set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/httpkit-server
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/httpkit-server set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/httpkit-server
) )
if "%BABASHKA_FEATURE_RING%"=="true" (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/ring
) else (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/ring
)
if "%BABASHKA_FEATURE_REITIT%"=="true" (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/reitit
) else (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/reitit
)
if "%BABASHKA_FEATURE_SELMER%"=="true" (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/selmer
) else (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/selmer
)
if "%BABASHKA_FEATURE_LANTERNA%"=="true" ( if "%BABASHKA_FEATURE_LANTERNA%"=="true" (
set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/lanterna set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/lanterna
) else ( ) else (

View file

@ -18,4 +18,7 @@
(def oracledb? (= "true" (System/getenv "BABASHKA_FEATURE_ORACLEDB"))) (def oracledb? (= "true" (System/getenv "BABASHKA_FEATURE_ORACLEDB")))
(def hsqldb? (= "true" (System/getenv "BABASHKA_FEATURE_HSQLDB"))) (def hsqldb? (= "true" (System/getenv "BABASHKA_FEATURE_HSQLDB")))
(def datascript? (= "true" (System/getenv "BABASHKA_FEATURE_DATASCRIPT"))) (def datascript? (= "true" (System/getenv "BABASHKA_FEATURE_DATASCRIPT")))
(def ring? (= "true" (System/getenv "BABASHKA_FEATURE_RING")))
(def reitit? (= "true" (System/getenv "BABASHKA_FEATURE_REITIT")))
(def selmer? (= "true" (System/getenv "BABASHKA_FEATURE_SELMER")))
(def lanterna? (= "true" (System/getenv "BABASHKA_FEATURE_LANTERNA"))) (def lanterna? (= "true" (System/getenv "BABASHKA_FEATURE_LANTERNA")))

View file

@ -90,6 +90,23 @@
(when features/httpkit-server? (when features/httpkit-server?
(require '[babashka.impl.httpkit-server])) (require '[babashka.impl.httpkit-server]))
(when features/ring?
(require '[babashka.impl.ring-middleware-defaults]
'[babashka.impl.ring-util-response]
'[babashka.impl.ring-util-request]
'[babashka.impl.ring-middleware-content-type]
'[babashka.impl.ring-middleware-webjars]
'[babashka.impl.ring-middleware-anti-forgery]))
(when features/reitit?
(require '[babashka.impl.reitit-ring]
'[babashka.impl.ring-util-http-response]
'[babashka.impl.muuntaja-core]
'[babashka.impl.muuntaja-middleware]))
(when features/selmer?
(require '[babashka.impl.selmer-parser]))
(when features/lanterna? (when features/lanterna?
(require '[babashka.impl.lanterna])) (require '[babashka.impl.lanterna]))
@ -405,6 +422,21 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that
features/httpkit-client? (assoc 'org.httpkit.client @(resolve 'babashka.impl.httpkit-client/httpkit-client-namespace) features/httpkit-client? (assoc 'org.httpkit.client @(resolve 'babashka.impl.httpkit-client/httpkit-client-namespace)
'org.httpkit.sni-client @(resolve 'babashka.impl.httpkit-client/sni-client-namespace)) 'org.httpkit.sni-client @(resolve 'babashka.impl.httpkit-client/sni-client-namespace))
features/httpkit-server? (assoc 'org.httpkit.server @(resolve 'babashka.impl.httpkit-server/httpkit-server-namespace)) features/httpkit-server? (assoc 'org.httpkit.server @(resolve 'babashka.impl.httpkit-server/httpkit-server-namespace))
features/ring? (-> (assoc 'ring.middleware.defaults @(resolve 'babashka.impl.ring-middleware-defaults/ring-middleware-defaults-namespace))
(assoc 'ring.util.response @(resolve 'babashka.impl.ring-util-response/ring-util-response-namespace))
(assoc 'ring.util.request @(resolve 'babashka.impl.ring-util-request/ring-util-request-namespace))
(assoc 'ring.middleware.content-type @(resolve 'babashka.impl.ring-middleware-content-type/ring-middleware-content-type-namespace))
(assoc 'ring.middleware.webjars @(resolve 'babashka.impl.ring-middleware-webjars/ring-middleware-webjars-namespace))
(assoc 'ring.middleware.anti-forgery @(resolve 'babashka.impl.ring-middleware-anti-forgery/ring-middleware-anti-forgery-namespace)))
features/reitit? (-> (assoc 'reitit.ring @(resolve 'babashka.impl.reitit-ring/reitit-ring-namespace))
(assoc 'ring.util.http-response @(resolve 'babashka.impl.ring-util-http-response/ring-util-http-response-namespace))
(assoc 'muuntaja.core @(resolve 'babashka.impl.muuntaja-core/muuntaja-core-namespace))
(assoc 'muuntaja.middleware @(resolve 'babashka.impl.muuntaja-middleware/muuntaja-middleware-namespace)))
features/selmer? (assoc 'selmer.parser @(resolve 'babashka.impl.selmer-parser/selmer-parser-namespace))
features/lanterna? (assoc 'lanterna.screen @(resolve 'babashka.impl.lanterna/lanterna-screen-namespace) features/lanterna? (assoc 'lanterna.screen @(resolve 'babashka.impl.lanterna/lanterna-screen-namespace)
'lanterna.terminal @(resolve 'babashka.impl.lanterna/lanterna-terminal-namespace) 'lanterna.terminal @(resolve 'babashka.impl.lanterna/lanterna-terminal-namespace)
'lanterna.constants @(resolve 'babashka.impl.lanterna/lanterna-constants-namespace)))) 'lanterna.constants @(resolve 'babashka.impl.lanterna/lanterna-constants-namespace))))