##// END OF EJS Templates
wireproto: define permissions-based routing of HTTPv2 wire protocol...
Gregory Szorc -
r37065:fddcb51b default
parent child Browse files
Show More
@@ -2970,6 +2970,11 b' def debugwireproto(ui, repo, path=None, '
2970 url = path + httppath
2970 url = path + httppath
2971 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
2971 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
2972
2972
2973 # urllib.Request insists on using has_data() as a proxy for
2974 # determining the request method. Override that to use our
2975 # explicitly requested method.
2976 req.get_method = lambda: method
2977
2973 try:
2978 try:
2974 opener.open(req).read()
2979 opener.open(req).read()
2975 except util.urlerr.urlerror as e:
2980 except util.urlerr.urlerror as e:
@@ -144,6 +144,46 b' A command returning a ``stream`` respons'
144 ``application/mercurial-0.*`` media type and the HTTP response is typically
144 ``application/mercurial-0.*`` media type and the HTTP response is typically
145 using *chunked transfer* (``Transfer-Encoding: chunked``).
145 using *chunked transfer* (``Transfer-Encoding: chunked``).
146
146
147 HTTP Version 2 Transport
148 ------------------------
149
150 **Experimental - feature under active development**
151
152 Version 2 of the HTTP protocol is exposed under the ``/api/*`` URL space.
153 It's final API name is not yet formalized.
154
155 Commands are triggered by sending HTTP requests against URLs of the
156 form ``<permission>/<command>``, where ``<permission>`` is ``ro`` or
157 ``rw``, meaning read-only and read-write, respectively and ``<command>``
158 is a named wire protocol command.
159
160 Commands that modify repository state in meaningful ways MUST NOT be
161 exposed under the ``ro`` URL prefix. All available commands MUST be
162 available under the ``rw`` URL prefix.
163
164 Server adminstrators MAY implement blanket HTTP authentication keyed
165 off the URL prefix. For example, a server may require authentication
166 for all ``rw/*`` URLs and let unauthenticated requests to ``ro/*``
167 URL proceed. A server MAY issue an HTTP 401, 403, or 407 response
168 in accordance with RFC 7235. Clients SHOULD recognize the HTTP Basic
169 (RFC 7617) and Digest (RFC 7616) authentication schemes. Clients SHOULD
170 make an attempt to recognize unknown schemes using the
171 ``WWW-Authenticate`` response header on a 401 response, as defined by
172 RFC 7235.
173
174 Read-only commands are accessible under ``rw/*`` URLs so clients can
175 signal the intent of the operation very early in the connection
176 lifecycle. For example, a ``push`` operation - which consists of
177 various read-only commands mixed with at least one read-write command -
178 can perform all commands against ``rw/*`` URLs so that any server-side
179 authentication requirements are discovered upon attempting the first
180 command - not potentially several commands into the exchange. This
181 allows clients to fail faster or prompt for credentials as soon as the
182 exchange takes place. This provides a better end-user experience.
183
184 Requests to unknown commands or URLS result in an HTTP 404.
185 TODO formally define response type, how error is communicated, etc.
186
147 SSH Protocol
187 SSH Protocol
148 ============
188 ============
149
189
@@ -272,6 +272,64 b' def handlewsgiapirequest(rctx, req, res,'
272 req.dispatchparts[2:])
272 req.dispatchparts[2:])
273
273
274 def _handlehttpv2request(rctx, req, res, checkperm, urlparts):
274 def _handlehttpv2request(rctx, req, res, checkperm, urlparts):
275 from .hgweb import common as hgwebcommon
276
277 # URL space looks like: <permissions>/<command>, where <permission> can
278 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
279
280 # Root URL does nothing meaningful... yet.
281 if not urlparts:
282 res.status = b'200 OK'
283 res.headers[b'Content-Type'] = b'text/plain'
284 res.setbodybytes(_('HTTP version 2 API handler'))
285 return
286
287 if len(urlparts) == 1:
288 res.status = b'404 Not Found'
289 res.headers[b'Content-Type'] = b'text/plain'
290 res.setbodybytes(_('do not know how to process %s\n') %
291 req.dispatchpath)
292 return
293
294 permission, command = urlparts[0:2]
295
296 if permission not in (b'ro', b'rw'):
297 res.status = b'404 Not Found'
298 res.headers[b'Content-Type'] = b'text/plain'
299 res.setbodybytes(_('unknown permission: %s') % permission)
300 return
301
302 # At some point we'll want to use our own API instead of recycling the
303 # behavior of version 1 of the wire protocol...
304 # TODO return reasonable responses - not responses that overload the
305 # HTTP status line message for error reporting.
306 try:
307 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
308 except hgwebcommon.ErrorResponse as e:
309 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
310 for k, v in e.headers:
311 res.headers[k] = v
312 res.setbodybytes('permission denied')
313 return
314
315 if command not in wireproto.commands:
316 res.status = b'404 Not Found'
317 res.headers[b'Content-Type'] = b'text/plain'
318 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
319 return
320
321 repo = rctx.repo
322 ui = repo.ui
323
324 proto = httpv2protocolhandler(req, ui)
325
326 if not wireproto.commands.commandavailable(command, proto):
327 res.status = b'404 Not Found'
328 res.headers[b'Content-Type'] = b'text/plain'
329 res.setbodybytes(_('invalid wire protocol command: %s') % command)
330 return
331
332 # We don't do anything meaningful yet.
275 res.status = b'200 OK'
333 res.status = b'200 OK'
276 res.headers[b'Content-Type'] = b'text/plain'
334 res.headers[b'Content-Type'] = b'text/plain'
277 res.setbodybytes(b'/'.join(urlparts) + b'\n')
335 res.setbodybytes(b'/'.join(urlparts) + b'\n')
@@ -284,6 +342,34 b' API_HANDLERS = {'
284 },
342 },
285 }
343 }
286
344
345 class httpv2protocolhandler(wireprototypes.baseprotocolhandler):
346 def __init__(self, req, ui):
347 self._req = req
348 self._ui = ui
349
350 @property
351 def name(self):
352 return HTTPV2
353
354 def getargs(self, args):
355 raise NotImplementedError
356
357 def forwardpayload(self, fp):
358 raise NotImplementedError
359
360 @contextlib.contextmanager
361 def mayberedirectstdio(self):
362 raise NotImplementedError
363
364 def client(self):
365 raise NotImplementedError
366
367 def addcapabilities(self, repo, caps):
368 raise NotImplementedError
369
370 def checkperm(self, perm):
371 raise NotImplementedError
372
287 def _httpresponsetype(ui, req, prefer_uncompressed):
373 def _httpresponsetype(ui, req, prefer_uncompressed):
288 """Determine the appropriate response type and compression settings.
374 """Determine the appropriate response type and compression settings.
289
375
@@ -1,7 +1,24 b''
1 $ HTTPV2=exp-http-v2-0001
2
1 $ send() {
3 $ send() {
2 > hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
4 > hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
3 > }
5 > }
4
6
7 $ cat > dummycommands.py << EOF
8 > from mercurial import wireprototypes, wireproto
9 > @wireproto.wireprotocommand('customreadonly', permission='pull')
10 > def customreadonly(repo, proto):
11 > return wireprototypes.bytesresponse(b'customreadonly bytes response')
12 > @wireproto.wireprotocommand('customreadwrite', permission='push')
13 > def customreadwrite(repo, proto):
14 > return wireprototypes.bytesresponse(b'customreadwrite bytes response')
15 > EOF
16
17 $ cat >> $HGRCPATH << EOF
18 > [extensions]
19 > dummycommands = $TESTTMP/dummycommands.py
20 > EOF
21
5 $ hg init server
22 $ hg init server
6 $ cat > server/.hg/hgrc << EOF
23 $ cat > server/.hg/hgrc << EOF
7 > [experimental]
24 > [experimental]
@@ -13,7 +30,7 b''
13 HTTP v2 protocol not enabled by default
30 HTTP v2 protocol not enabled by default
14
31
15 $ send << EOF
32 $ send << EOF
16 > httprequest GET api/exp-http-v2-0001
33 > httprequest GET api/$HTTPV2
17 > user-agent: test
34 > user-agent: test
18 > EOF
35 > EOF
19 using raw connection to peer
36 using raw connection to peer
@@ -43,14 +60,14 b' Restart server with support for HTTP v2 '
43 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
60 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
44 $ cat hg.pid > $DAEMON_PIDS
61 $ cat hg.pid > $DAEMON_PIDS
45
62
46 Requests simply echo their path (for now)
63 Request to read-only command works out of the box
47
64
48 $ send << EOF
65 $ send << EOF
49 > httprequest GET api/exp-http-v2-0001/path1/path2
66 > httprequest GET api/$HTTPV2/ro/customreadonly
50 > user-agent: test
67 > user-agent: test
51 > EOF
68 > EOF
52 using raw connection to peer
69 using raw connection to peer
53 s> GET /api/exp-http-v2-0001/path1/path2 HTTP/1.1\r\n
70 s> GET /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
54 s> Accept-Encoding: identity\r\n
71 s> Accept-Encoding: identity\r\n
55 s> user-agent: test\r\n
72 s> user-agent: test\r\n
56 s> host: $LOCALIP:$HGPORT\r\n (glob)
73 s> host: $LOCALIP:$HGPORT\r\n (glob)
@@ -60,6 +77,178 b' Requests simply echo their path (for now'
60 s> Server: testing stub value\r\n
77 s> Server: testing stub value\r\n
61 s> Date: $HTTP_DATE$\r\n
78 s> Date: $HTTP_DATE$\r\n
62 s> Content-Type: text/plain\r\n
79 s> Content-Type: text/plain\r\n
63 s> Content-Length: 12\r\n
80 s> Content-Length: 18\r\n
81 s> \r\n
82 s> ro/customreadonly\n
83
84 Request to unknown command yields 404
85
86 $ send << EOF
87 > httprequest GET api/$HTTPV2/ro/badcommand
88 > user-agent: test
89 > EOF
90 using raw connection to peer
91 s> GET /api/exp-http-v2-0001/ro/badcommand HTTP/1.1\r\n
92 s> Accept-Encoding: identity\r\n
93 s> user-agent: test\r\n
94 s> host: $LOCALIP:$HGPORT\r\n (glob)
95 s> \r\n
96 s> makefile('rb', None)
97 s> HTTP/1.1 404 Not Found\r\n
98 s> Server: testing stub value\r\n
99 s> Date: $HTTP_DATE$\r\n
100 s> Content-Type: text/plain\r\n
101 s> Content-Length: 42\r\n
102 s> \r\n
103 s> unknown wire protocol command: badcommand\n
104
105 Request to read-write command fails because server is read-only by default
106
107 GET to read-write request not allowed
108
109 $ send << EOF
110 > httprequest GET api/$HTTPV2/rw/customreadonly
111 > user-agent: test
112 > EOF
113 using raw connection to peer
114 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
115 s> Accept-Encoding: identity\r\n
116 s> user-agent: test\r\n
117 s> host: $LOCALIP:$HGPORT\r\n (glob)
118 s> \r\n
119 s> makefile('rb', None)
120 s> HTTP/1.1 405 push requires POST request\r\n
121 s> Server: testing stub value\r\n
122 s> Date: $HTTP_DATE$\r\n
123 s> Content-Length: 17\r\n
124 s> \r\n
125 s> permission denied
126
127 Even for unknown commands
128
129 $ send << EOF
130 > httprequest GET api/$HTTPV2/rw/badcommand
131 > user-agent: test
132 > EOF
133 using raw connection to peer
134 s> GET /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
135 s> Accept-Encoding: identity\r\n
136 s> user-agent: test\r\n
137 s> host: $LOCALIP:$HGPORT\r\n (glob)
138 s> \r\n
139 s> makefile('rb', None)
140 s> HTTP/1.1 405 push requires POST request\r\n
141 s> Server: testing stub value\r\n
142 s> Date: $HTTP_DATE$\r\n
143 s> Content-Length: 17\r\n
144 s> \r\n
145 s> permission denied
146
147 SSL required by default
148
149 $ send << EOF
150 > httprequest POST api/$HTTPV2/rw/customreadonly
151 > user-agent: test
152 > EOF
153 using raw connection to peer
154 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
155 s> Accept-Encoding: identity\r\n
156 s> user-agent: test\r\n
157 s> host: $LOCALIP:$HGPORT\r\n (glob)
158 s> \r\n
159 s> makefile('rb', None)
160 s> HTTP/1.1 403 ssl required\r\n
161 s> Server: testing stub value\r\n
162 s> Date: $HTTP_DATE$\r\n
163 s> Content-Length: 17\r\n
64 s> \r\n
164 s> \r\n
65 s> path1/path2\n
165 s> permission denied
166
167 Restart server to allow non-ssl read-write operations
168
169 $ killdaemons.py
170 $ cat > server/.hg/hgrc << EOF
171 > [experimental]
172 > web.apiserver = true
173 > web.api.http-v2 = true
174 > [web]
175 > push_ssl = false
176 > EOF
177
178 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
179 $ cat hg.pid > $DAEMON_PIDS
180
181 Server insists on POST for read-write commands
182
183 $ send << EOF
184 > httprequest GET api/$HTTPV2/rw/customreadonly
185 > user-agent: test
186 > EOF
187 using raw connection to peer
188 s> GET /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
189 s> Accept-Encoding: identity\r\n
190 s> user-agent: test\r\n
191 s> host: $LOCALIP:$HGPORT\r\n (glob)
192 s> \r\n
193 s> makefile('rb', None)
194 s> HTTP/1.1 405 push requires POST request\r\n
195 s> Server: testing stub value\r\n
196 s> Date: $HTTP_DATE$\r\n
197 s> Content-Length: 17\r\n
198 s> \r\n
199 s> permission denied
200
201 $ killdaemons.py
202 $ cat > server/.hg/hgrc << EOF
203 > [experimental]
204 > web.apiserver = true
205 > web.api.http-v2 = true
206 > [web]
207 > push_ssl = false
208 > allow-push = *
209 > EOF
210
211 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid
212 $ cat hg.pid > $DAEMON_PIDS
213
214 Authorized request for valid read-write command works
215
216 $ send << EOF
217 > httprequest POST api/$HTTPV2/rw/customreadonly
218 > user-agent: test
219 > EOF
220 using raw connection to peer
221 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
222 s> Accept-Encoding: identity\r\n
223 s> user-agent: test\r\n
224 s> host: $LOCALIP:$HGPORT\r\n (glob)
225 s> \r\n
226 s> makefile('rb', None)
227 s> HTTP/1.1 200 OK\r\n
228 s> Server: testing stub value\r\n
229 s> Date: $HTTP_DATE$\r\n
230 s> Content-Type: text/plain\r\n
231 s> Content-Length: 18\r\n
232 s> \r\n
233 s> rw/customreadonly\n
234
235 Authorized request for unknown command is rejected
236
237 $ send << EOF
238 > httprequest POST api/$HTTPV2/rw/badcommand
239 > user-agent: test
240 > EOF
241 using raw connection to peer
242 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
243 s> Accept-Encoding: identity\r\n
244 s> user-agent: test\r\n
245 s> host: $LOCALIP:$HGPORT\r\n (glob)
246 s> \r\n
247 s> makefile('rb', None)
248 s> HTTP/1.1 404 Not Found\r\n
249 s> Server: testing stub value\r\n
250 s> Date: $HTTP_DATE$\r\n
251 s> Content-Type: text/plain\r\n
252 s> Content-Length: 42\r\n
253 s> \r\n
254 s> unknown wire protocol command: badcommand\n
General Comments 0
You need to be logged in to leave comments. Login now