-
Notifications
You must be signed in to change notification settings - Fork 49
/
elnode-proxy.el
300 lines (250 loc) · 10.4 KB
/
elnode-proxy.el
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
;;; elnode-proxy.el -- proxying with elnode -*- lexical-binding: t -*-
;;; Commentary:
;; This is stuff to let you make proxy servers with Elnode.
;;; Code:
(require 's)
(require 'dash)
(require 'web)
(require 'elnode)
(require 'kv)
(require 'cl) ; for destructuring-bind and defun*
(defun elnode-proxy/web-hdr-hash->alist (web-hdr)
(-filter
(lambda (hdr-pair)
(unless (member
(downcase (symbol-name (car hdr-pair)))
'("status-code" "status-string" "status-version"))
(cons (symbol-name (car hdr-pair))
(cdr hdr-pair))))
(kvhash->alist web-hdr)))
(defun elnode--proxy-x-forwarded-for (httpcon)
"Return an X-Forwaded-For header."
(let ((ipaddr (elnode-get-remote-ipaddr httpcon))
(hdr (elnode-http-header httpcon "X-Forwarded-For")))
(if hdr
(concat hdr (format ", %s" ipaddr))
ipaddr)))
(defun elnode-proxy/web-client (httpc header data httpcon web-url header-filter)
"The web client used for the proxying.
WEB-URL is the origin URL. HEADER-FILTER is a function that will
filter the alist of headers."
(unless (elnode/con-get httpcon :elnode-proxy-header-sent)
(let* ((headers (elnode-proxy/web-hdr-hash->alist header))
(headers-x
(condition-case err
(funcall header-filter web-url headers)
(error (prog1 headers
(message
"elnode-proxy/web-client: got an error %S while filtering headers %S"
err headers))))))
(apply 'elnode-http-start httpcon 200 headers-x))
(elnode/con-put httpcon :elnode-proxy-header-sent t))
(if (eq data :done)
(elnode-http-return httpcon)
(elnode-http-send-string httpcon data)))
(cl-defun elnode-proxy-do (httpcon url &key header-filter)
"Do proxying to URL on HTTPCON.
A request is made to the specified URL. The URL may include
`s-format' patterns for interpolation with any of these
variables:
path - the path from the HTTPCON
params - the params from the HTTPCON
query - the params from the HTTPCON as a query
For example, \"http://myserver:8000${path}${query}\" would cause
\"myserver\" on port 8000 to get the query from the user with the
specified path and query.
:HEADER-FILTER is an optional function which can be used to
filter the headers returned from the HTTP call to the origin. The
function is called with the origin URL and the headers as an
a-list of symbols."
(let* ((method (elnode-http-method httpcon))
(path (elnode-http-pathinfo httpcon))
(params (web-to-query-string
(elnode-http-params httpcon)))
(params-alist
(list
(cons "path" path)
(cons "query" (if (s-blank? params) ""
(concat "?" params)))
(cons "params" params)))
(web-url (s-format url 'aget params-alist))
hdr-sent)
(let ((web-con
(web-http-call
method
(lambda (httpc hdr data)
(elnode-proxy/web-client
httpc hdr data httpcon web-url
(if (functionp header-filter)
header-filter
;; Else just pass through
(lambda (url headers) headers))))
:mode 'stream
:url web-url
:extra-headers
`(("X-Forwarded-For" . ,(elnode--proxy-x-forwarded-for httpcon))
("X-Proxy-Client" . "elnode/web")))))
(elnode/con-put httpcon :elnode-child-process web-con))))
(defun elnode-proxy-bounce (httpcon handler host-port)
"Bounce this request.
If HTTPCON is not a request for port HOST-PORT then bounce to
HOST-PORT, else it is a request on HOST-PORT so pass to HANDLER."
(destructuring-bind (hostname this-port)
(split-string (elnode-server-info httpcon) ":")
(if (equal (format "%s" this-port)
(format "%s" host-port))
(funcall handler httpcon)
(elnode-proxy-do
httpcon
(format "http://%s:%s${path}${query}" hostname host-port)))))
(defun elnode-proxy-make-bouncer (handler host-port)
"Make a proxy bouncer handler for HANDLER proc on OTHER-PORT.
This is for managing proxy calls. If the resulting handler
receives a call on anything than HOST-PORT then it proxies the
request to the HOST-PORT. Otherwise it just handles the
request."
(lambda (httpcon)
(elnode-proxy-bounce httpcon handler host-port)))
;;;###autoload
(defun elnode-make-proxy (url)
"Make a proxy handler sending requests to URL.
See `elnode-proxy-do' for how URL is handled.
An HTTP user-agent with a specified HTTP proxy sends the full
request as the path, eg:
GET http://somehost:port/path?query HTTP/1.1
So `elnode-make-proxy' can make (something like) a full proxy
server with:
(elnode-make-proxy \"${path}${query}\")
There may be many things that a full proxy does that this does
not do however.
Reverse proxying is a simpler and perhaps more useful.
Proxying is a form of shortcut evaluation. This function returns
having bound it's HTTP connection paremeter to a process which
will deliver the content from the downstream HTTP connection."
(lambda (httpcon)
(elnode-proxy-do httpcon url)))
(defvar elnode--proxy-server-port-history nil
"History variable used for proxy server port reading.")
(defvar elnode--proxy-server-goto-url-history nil
"History variable used for proxy goto urls.")
;;;###autoload
(defun elnode-make-proxy-server (port &optional url)
"Make a proxy server on the specified PORT.
Optionally have requests go to URL. If URL is not specified it
is \"${path}${query}\".
Interactively use C-u to specify the URL."
(interactive
(list
(read-from-minibuffer
"proxy server port:" nil nil nil
'elnode--proxy-server-port-history)
(if current-prefix-arg
(read-from-minibuffer
"proxy server goto url:" "${path}${query}" nil nil
'elnode--proxy-server-goto-url-history
"${path}${query}")
"${path}${query}")))
(let ((proxy-handler
(elnode-make-proxy (or url "${path}${query}"))))
(elnode-start proxy-handler :port port)))
(defun elnode-send-proxy-redirect (httpcon location)
"Send back a proxy redirect to LOCATION.
A proxy redirect is setting \"X-Accel-Redirect\" to a location,
proxies can interpret the header with some kind of internal only
URL resolution mechanism and do dispatch to another backend
without sending the redirect back to the origin UA."
(elnode-http-header-set
httpcon "X-Accel-Redirect" location)
;; This is an nginx specific hack because it seems nginx kills the
;; socket once the accel header arrives
(condition-case err
(elnode-send-redirect httpcon location)
(error (unless (string-match
"\\(SIGPIPE\\|no longer connected\\)"
(format "%s" (cdr err)))
(signal (car err) (cdr err))))))
(defun elnode-send-proxy-location (httpcon location)
"Send LOCATION with proxying techniques.
If the HTTPCON comes from a proxy (detected by checking the
\"X-Forwarded-For\") then an `elnode-send-proxy-redirect' to
location is sent.
Alternately it sets up a direct proxy call to the current server
for the location. So, either way, this call causes a shortcut
evaluation. Either the upstream proxy server handles the request
or we return having bound the current HTTPCON to an internal
proxy connection."
(if (and (elnode-http-header httpcon "X-Forwarded-For")
(not (equal
"elnode/web"
(elnode-http-header httpcon "X-Proxy-Client"))))
(elnode-send-proxy-redirect httpcon location)
;; Else we're not behind a proxy, send a proxy version
(let* ((server (elnode-server-info httpcon))
(url (format "http://%s%s" server location)))
(funcall (elnode-make-proxy url) httpcon))))
(defun* elnode-proxy-post (httpcon path
&key (mode 'batch)
callback data extra-headers)
"Make an HTTP call to localhost or the first upstream proxy."
(let* ((hp-pair
(if (elnode-http-header httpcon "X-Forwarded-For")
(elnode-get-remote-ipaddr httpcon)
(elnode-server-info httpcon)))
(url (format "http://%s%s" hp-pair path)))
(web-http-post
(or callback
(lambda (httpc hdr data)
(elnode-error
"%s post response %S %s"
httpcon hdr data)))
:url url :mode mode :data data
:extra-headers extra-headers)))
(defun elnode/proxy-route (httpcon service handler path)
"Proxies a particular route from `elnode-route'."
(let* ((server (elnode/con-get httpcon :server))
(p2 path)
(maps (process-get server :elnode-service-map))
(port
(or
(kva service maps)
(string-to-number
(cadr
(split-string
(elnode-server-info httpcon) ":"))))))
;; Wrap the handler in a bouncer
(elnode-proxy-bounce httpcon handler port)))
(defun elnode-route (httpcon routes)
"Pass HTTPCON to the handler decided by ROUTES.
ROUTES is a routing table matching regexs to handlers with extra
meta information. Routes may do additional things like cause a
route to be proxyed to another server.
Using ROUTES you can describe complex multi-process, multi-port
elnode configurations.
ROUTES is an alist where each element looks like:
(REGEXP . FUNCTION)
or:
(REGEXP FUNCTION `:service' SERVICE-NAME)
FUNCTION is a normal elnode handler. SERVICE-NAME is a name that
may be attached to the route so that it can be mapped to a TCP
port, or even another Emacs process. Mapping service names is
done by `elnode-start'."
(let*
(services
(rtable
(loop for (path . resource) in table
collect
(if (atom resource)
(list path resource)
;; Else it's a more complex resource description
(let* ((handler (car resource))
(service (plist-get (cdr resource) :service))
;; Make the function from the resource description
(func
(lambda (httpcon)
(elnode/proxy-route
httpcon service handler path))))
(when service (push service services))
(list path func))))))
(elnode-hostpath-dispatcher httpcon rtable)))
(provide 'elnode-proxy)
;;; elnode-proxy.el ends here