-
Notifications
You must be signed in to change notification settings - Fork 0
/
response.clj
320 lines (280 loc) · 9.97 KB
/
response.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
(ns ring.util.response
"Functions for generating and augmenting response maps."
{:author "James Reeves"
:contributors "Modified by Carlos da Cunha Fontes to work with Babashka"
:url "https://github.com/ring-clojure/ring"
:license {:name "Distributed under the MIT License, the same as Ring."}}
(: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.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)
(when (.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)]
(when (.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)]
(when (or (safe-path? root path)
(and (:allow-symlinks? opts) (not (directory-transversal? path))))
(File. root path))
(File. path)))
(defn- find-file [^String path opts]
(when-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]
(when-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- url-as-file ^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]
(when-let [file (url-as-file url)]
(when-not (.isDirectory file)
(file-data file))))
(defn url-response
"Return a response for the supplied URL."
{:added "1.2"}
[^URL url]
(when-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 "^/" ""))
load #(if-let [loader (:loader options)]
(io/resource % loader)
(io/resource %))]
(when-not (directory-transversal? root+path)
(when-let [resource (load root+path)]
(let [response (url-response resource)]
(when (or (not (instance? File (:body response)))
(safe-file-resource? response options))
response)))))))