* 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>
348 lines
11 KiB
Clojure
348 lines
11 KiB
Clojure
(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)))))))
|