/
vhosts.clj
237 lines (210 loc) · 8.53 KB
/
vhosts.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
;; Copyright © 2014, JUXT LTD.
(ns bidi.vhosts
(:require
[bidi.bidi :as bidi :refer :all :exclude [path-for]]
[bidi.ring :as br]
[bidi.schema :as bsc]
[schema.core :as s]
[schema.coerce :as sc]
[schema.utils :refer [error?]])
(:import [java.net URL URI]))
(s/defschema VHost (s/conditional
map? {:scheme (s/enum :http :https)
:host s/Str}
keyword? s/Keyword))
(s/defschema VHostWithRoutes
(s/constrained [(s/one [VHost] "Virtual host")
bsc/RoutePair] (comp not-empty first) "Must have at least one vhost"))
(defn uri->host [^URI uri]
(cond-> (.getHost uri)
(pos? (.getPort uri))
(str ":" (.getPort uri))))
(def coerce-to-vhost
(sc/coercer
VHost
{VHost (fn [x]
(cond (instance? URI x)
{:scheme (keyword (.getScheme ^URI x))
:host (uri->host x)}
(instance? URL x) (recur (.toURI ^URL x))
(string? x) (recur (URI. x))
:otherwise x))}))
(def coerce-to-vhosts
(sc/coercer
[VHostWithRoutes]
{VHost coerce-to-vhost
[VHost] (fn [x]
(if
(or (string? x)
(= x :*)
(instance? URI x)
(instance? URL x))
[(coerce-to-vhost x)]
(if-not (s/check VHost x) (vector x) x)))}))
(defrecord VHostsModel [vhosts])
(defn vhosts-model [& vhosts-with-routes]
(let [vhosts (coerce-to-vhosts (vec vhosts-with-routes))]
(when (error? vhosts)
(throw (ex-info (format "Error in server model: %s"
(pr-str (:error vhosts)))
{:error (:error vhosts)})))
(map->VHostsModel {:vhosts vhosts})))
(defn- query-string [query-params]
(let [enc (fn [a b] (str (if (keyword? a) (name a) a) "=" (java.net.URLEncoder/encode (str b))))
join (fn [v] (apply str (interpose "&" v)))]
(join
(map (fn [[k v]]
(if (sequential? v)
(join (map enc (repeat k) v))
(enc k v)))
query-params))))
(defn- segments [^String s]
(let [l (re-seq #"[^/]*/?" s)]
(if (.endsWith s "/") l (butlast l))))
(defn relativize [from to]
(if (and from to)
(loop [from (segments from)
to (segments to)]
(if-not (= (first from) (first to))
(str (apply str (repeat (+ (dec (count from))) "../"))
(apply str to))
(if (next from)
(recur (next from) (next to))
(first to))))
to))
(defn scheme
"Get the real scheme (as a keyword) from a request, taking into
account reverse-proxy headers"
[req]
(or (some-> (get-in req [:headers "x-forwarded-proto"]) keyword)
(:scheme req)))
(defn host
"Get the real host from a request, taking into account reverse-proxy headers"
[req]
(or (get-in req [:headers "x-forwarded-host"])
(get-in req [:headers "host"])))
(defn prioritize-vhosts [vhosts-model vhost]
(cond->> (:vhosts vhosts-model)
vhost (sort-by (fn [[vhosts & _]] (if (first (filter (fn [x] (= x vhost)) vhosts)) -1 1)))))
(defn uri-info
"Return URI info as a map."
[prioritized-vhosts handler & [{:keys [request vhost route-params query-params prefer fragment path-info] :or {prefer :local} :as options}]]
(some
(fn [[vhosts & routes]]
(when-let [path (cond->
(apply bidi/path-for ["" (vec routes)] handler (mapcat identity route-params))
path-info (str path-info))]
(let [qs (when query-params
(query-string query-params))]
(let [to-vhost (case prefer
:local (or (first (filter (partial = vhost) vhosts))
(first vhosts))
:first (first vhosts)
:same-scheme (first (filter #(= (:scheme vhost) (:scheme %)) vhosts))
:http (first (filter #(= :http (:scheme %)) vhosts))
:https (first (filter #(= :https (:scheme %)) vhosts))
:local-then-same-scheme (or (first (filter (partial = vhost) vhosts))
(first (filter #(= (:scheme vhost) (:scheme %)) vhosts))
(first vhosts)))
uri (format "%s://%s%s%s%s"
(cond (= to-vhost :*) (name (scheme request))
:otherwise (name (:scheme to-vhost)))
(cond (= to-vhost :*) (host request)
:otherwise (:host to-vhost))
path
(if qs (str "?" qs) "")
(if fragment (str "#" fragment) ""))]
(merge {:uri uri
:path path
:host (:host to-vhost)
:scheme (:scheme to-vhost)
:href (if (and (= vhost to-vhost) request)
(format "%s%s%s"
(relativize (:uri request) path)
(if qs (str "?" qs) "")
(if fragment (str "#" fragment) ""))
uri)}
(when qs {:query-string qs})
(when fragment {:fragment fragment}))))))
prioritized-vhosts))
(def path-for (comp :path uri-info))
(def host-for (comp :host uri-info))
(def scheme-for (comp :scheme uri-info))
(def href-for (comp :href uri-info))
;; At some point in the future, uri-for will be removed, opening up
;; the possibility that it can be re-implemented.
(def ^{:deprecated true} uri-for uri-info)
(defn find-handler [vhosts-model req]
(let [vhost {:scheme (scheme req)
:host (host req)}]
(some
(fn [[vhosts & routes]]
(let [routes (vec routes)]
(when (some (fn [vh] (or (= vh :*) (= (:host vh) (:host vhost)))) vhosts)
(->
(resolve-handler
routes
(assoc req
:remainder (:uri req)
:route ["" routes]
:uri-info (fn [handler & [options]]
(uri-info (prioritize-vhosts vhosts-model vhost) handler (merge {:vhost vhost :request req} options)))))
(dissoc :route)))))
(:vhosts vhosts-model))))
(defn vhosts->roots [vhosts request]
(->> vhosts
(map first)
(apply concat)
(map (fn [x]
(cond
(= x :*) (format "%s://%s" (name (scheme request)) (host request))
:otherwise (let [{:keys [scheme host]} x]
(str (name scheme) "://" host)))))))
(defn make-default-not-found-handler [vhosts-model]
(fn [req]
{:status 404
:body (apply str "Not found\n\n"
;; It is useful to provide the hosts that are served from this server
(->> (vhosts->roots (:vhosts vhosts-model) req)
(interpose "\n")))}))
(defn make-handler
([vhosts-model] (make-handler vhosts-model identity))
([vhosts-model handler-fn]
(make-handler vhosts-model handler-fn (make-default-not-found-handler vhosts-model)))
([vhosts-model handler-fn not-found]
(fn [req]
(let [{:keys [handler route-params] :as match-context}
(find-handler vhosts-model req)]
(if-let [handler (handler-fn handler)]
(br/request
handler
(-> req
(update-in [:params] merge route-params)
(update-in [:route-params] merge route-params))
(apply dissoc match-context :handler (keys req)))
(not-found req))))))
(defrecord Redirect [status target query-params]
bidi/Matched
(resolve-handler [this m]
(when (= "" (:remainder m))
(cond-> m
true (assoc :handler this)
(not (string? target))
(assoc :location
(:uri ((:uri-info m) target
(merge
{:route-params (:route-params m)}
(when query-params {:query-params query-params})))))
true (dissoc :remainder))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))
br/Ring
(request [f req m]
(if-let [location (if-not (string? target) (:location m) target)]
{:status status
:headers {"location" location}
:body (str "Redirect to " location)}
{:status 500
:body "Failed to determine redirect location"})))
(defn redirect [target & [opts]]
(map->Redirect (merge {:status 302 :target target} opts)))