/
response.clj
222 lines (194 loc) · 6.5 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
(ns ring.util.response
"Functions for generating and augmenting response maps."
(:import java.io.File java.util.Date java.net.URL
java.net.URLDecoder java.net.URLEncoder)
(:use [ring.util.time :only (format-date)]
[ring.util.io :only (last-modified-date)])
(:require [clojure.java.io :as io]
[clojure.string :as str]))
(defn redirect
"Returns a Ring response for an HTTP 302 redirect."
[url]
{:status 302
:headers {"Location" url}
:body ""})
(defn redirect-after-post
"Returns a Ring response for an HTTP 303 redirect."
[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 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."
[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- safe-path?
"Is a filepath safe for a particular root?"
[^String root ^String path]
(.startsWith (.getCanonicalPath (File. root path))
(.getCanonicalPath (File. root))))
(defn- directory-transversal?
"Check if a path contains '..'."
[^String path]
(-> (str/split path #"/|\\")
(set)
(contains? "..")))
(defn- find-index-file
"Search the directory for an index file."
[^File dir]
(first
(filter
#(.startsWith (.toLowerCase (.getName ^File %)) "index.")
(.listFiles dir))))
(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-content-length [resp]
(let [file ^File (:body resp)]
(header resp "Content-Length" (.length file))))
(defn- file-last-modified [resp]
(let [file ^File (:body resp)]
(header resp "Last-Modified" (format-date (last-modified-date file)))))
(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? - serve files through symbolic links, defaults to false"
[filepath & [opts]]
(if-let [file (find-file filepath opts)]
(-> (response file)
(file-content-length)
(file-last-modified))))
;; In Clojure versions 1.2.0, 1.2.1 and 1.3.0, the as-file function
;; in clojure.java.io does not correctly decode special characters in
;; URLs (e.g. '%20' should be turned into ' ').
;;
;; See: http://dev.clojure.org/jira/browse/CLJ-885
;;
;; 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 charset
"Returns an updated Ring response with the supplied charset added to the
Content-Type header."
{:added "1.1"}
[resp charset]
(update-in resp [:headers "Content-Type"]
(fn [content-type]
(-> (or content-type "text/plain")
(str/replace #";\s*charset=[^;]*" "")
(str "; charset=" 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))))
(defn- connection-content-length [resp ^java.net.URLConnection conn]
(let [content-length (.getContentLength conn)]
(if (neg? content-length)
resp
(header resp "Content-Length" content-length))))
(defn- connection-last-modified [resp ^java.net.URLConnection conn]
(let [last-modified (.getLastModified conn)]
(if (zero? last-modified)
resp
(header resp "Last-Modified" (format-date (Date. last-modified))))))
(defn- file-url [^java.net.URL url]
(if (= "file" (.getProtocol url))
(url-as-file url)))
(defn- directory-url? [^java.net.URL url]
(-> (str url) (.endsWith "/")))
(defn url-response
"Return a response for the supplied URL."
{:added "1.2"}
[^URL url]
(if-let [^File file (file-url url)]
(if-not (.isDirectory file)
(-> (response file)
(file-content-length)
(file-last-modified)))
(when-not (directory-url? url)
(let [conn (.openConnection url)]
(if-let [stream (.getInputStream conn)]
(-> (response stream)
(connection-content-length conn)
(connection-last-modified conn)))))))
(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"
[path & [opts]]
(let [path (-> (str (:root opts "") "/" path)
(.replace "//" "/")
(.replaceAll "^/" ""))]
(if-let [resource (io/resource path)]
(url-response resource))))
(defn get-header
"Look up a header in a Ring response (or request) case insensitively,
returning the value of the header."
{:added "1.2"}
[resp ^String header-name]
(some (fn [[k v]] (if (.equalsIgnoreCase header-name k) v))
(:headers resp)))