##// END OF EJS Templates
hgweb: use a multidict for holding query string parameters...
Gregory Szorc -
r36878:ec0af9c5 default
parent child Browse files
Show More
@@ -1,463 +1,539 b''
1 # hgweb/request.py - An http request from either CGI or the standalone server.
1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import socket
12 import socket
13 import wsgiref.headers as wsgiheaders
13 import wsgiref.headers as wsgiheaders
14 #import wsgiref.validate
14 #import wsgiref.validate
15
15
16 from .common import (
16 from .common import (
17 ErrorResponse,
17 ErrorResponse,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 statusmessage,
19 statusmessage,
20 )
20 )
21
21
22 from ..thirdparty import (
22 from ..thirdparty import (
23 attr,
23 attr,
24 )
24 )
25 from .. import (
25 from .. import (
26 error,
26 error,
27 pycompat,
27 pycompat,
28 util,
28 util,
29 )
29 )
30
30
31 class multidict(object):
32 """A dict like object that can store multiple values for a key.
33
34 Used to store parsed request parameters.
35
36 This is inspired by WebOb's class of the same name.
37 """
38 def __init__(self):
39 # Stores (key, value) 2-tuples. This isn't the most efficient. But we
40 # don't rely on parameters that much, so it shouldn't be a perf issue.
41 # we can always add dict for fast lookups.
42 self._items = []
43
44 def __getitem__(self, key):
45 """Returns the last set value for a key."""
46 for k, v in reversed(self._items):
47 if k == key:
48 return v
49
50 raise KeyError(key)
51
52 def __setitem__(self, key, value):
53 """Replace a values for a key with a new value."""
54 try:
55 del self[key]
56 except KeyError:
57 pass
58
59 self._items.append((key, value))
60
61 def __delitem__(self, key):
62 """Delete all values for a key."""
63 oldlen = len(self._items)
64
65 self._items[:] = [(k, v) for k, v in self._items if k != key]
66
67 if oldlen == len(self._items):
68 raise KeyError(key)
69
70 def __contains__(self, key):
71 return any(k == key for k, v in self._items)
72
73 def __len__(self):
74 return len(self._items)
75
76 def get(self, key, default=None):
77 try:
78 return self.__getitem__(key)
79 except KeyError:
80 return default
81
82 def add(self, key, value):
83 """Add a new value for a key. Does not replace existing values."""
84 self._items.append((key, value))
85
86 def getall(self, key):
87 """Obtains all values for a key."""
88 return [v for k, v in self._items if k == key]
89
90 def getone(self, key):
91 """Obtain a single value for a key.
92
93 Raises KeyError if key not defined or it has multiple values set.
94 """
95 vals = self.getall(key)
96
97 if not vals:
98 raise KeyError(key)
99
100 if len(vals) > 1:
101 raise KeyError('multiple values for %r' % key)
102
103 return vals[0]
104
105 def asdictoflists(self):
106 d = {}
107 for k, v in self._items:
108 if k in d:
109 d[k].append(v)
110 else:
111 d[k] = [v]
112
113 return d
114
31 @attr.s(frozen=True)
115 @attr.s(frozen=True)
32 class parsedrequest(object):
116 class parsedrequest(object):
33 """Represents a parsed WSGI request.
117 """Represents a parsed WSGI request.
34
118
35 Contains both parsed parameters as well as a handle on the input stream.
119 Contains both parsed parameters as well as a handle on the input stream.
36 """
120 """
37
121
38 # Request method.
122 # Request method.
39 method = attr.ib()
123 method = attr.ib()
40 # Full URL for this request.
124 # Full URL for this request.
41 url = attr.ib()
125 url = attr.ib()
42 # URL without any path components. Just <proto>://<host><port>.
126 # URL without any path components. Just <proto>://<host><port>.
43 baseurl = attr.ib()
127 baseurl = attr.ib()
44 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
128 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
45 # of HTTP: Host header for hostname. This is likely what clients used.
129 # of HTTP: Host header for hostname. This is likely what clients used.
46 advertisedurl = attr.ib()
130 advertisedurl = attr.ib()
47 advertisedbaseurl = attr.ib()
131 advertisedbaseurl = attr.ib()
48 # WSGI application path.
132 # WSGI application path.
49 apppath = attr.ib()
133 apppath = attr.ib()
50 # List of path parts to be used for dispatch.
134 # List of path parts to be used for dispatch.
51 dispatchparts = attr.ib()
135 dispatchparts = attr.ib()
52 # URL path component (no query string) used for dispatch.
136 # URL path component (no query string) used for dispatch.
53 dispatchpath = attr.ib()
137 dispatchpath = attr.ib()
54 # Whether there is a path component to this request. This can be true
138 # Whether there is a path component to this request. This can be true
55 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
139 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
56 havepathinfo = attr.ib()
140 havepathinfo = attr.ib()
57 # Raw query string (part after "?" in URL).
141 # Raw query string (part after "?" in URL).
58 querystring = attr.ib()
142 querystring = attr.ib()
59 # List of 2-tuples of query string arguments.
143 # multidict of query string parameters.
60 querystringlist = attr.ib()
144 qsparams = attr.ib()
61 # Dict of query string arguments. Values are lists with at least 1 item.
62 querystringdict = attr.ib()
63 # wsgiref.headers.Headers instance. Operates like a dict with case
145 # wsgiref.headers.Headers instance. Operates like a dict with case
64 # insensitive keys.
146 # insensitive keys.
65 headers = attr.ib()
147 headers = attr.ib()
66 # Request body input stream.
148 # Request body input stream.
67 bodyfh = attr.ib()
149 bodyfh = attr.ib()
68
150
69 def parserequestfromenv(env, bodyfh):
151 def parserequestfromenv(env, bodyfh):
70 """Parse URL components from environment variables.
152 """Parse URL components from environment variables.
71
153
72 WSGI defines request attributes via environment variables. This function
154 WSGI defines request attributes via environment variables. This function
73 parses the environment variables into a data structure.
155 parses the environment variables into a data structure.
74 """
156 """
75 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
157 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
76
158
77 # We first validate that the incoming object conforms with the WSGI spec.
159 # We first validate that the incoming object conforms with the WSGI spec.
78 # We only want to be dealing with spec-conforming WSGI implementations.
160 # We only want to be dealing with spec-conforming WSGI implementations.
79 # TODO enable this once we fix internal violations.
161 # TODO enable this once we fix internal violations.
80 #wsgiref.validate.check_environ(env)
162 #wsgiref.validate.check_environ(env)
81
163
82 # PEP-0333 states that environment keys and values are native strings
164 # PEP-0333 states that environment keys and values are native strings
83 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
165 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
84 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
166 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
85 # in Mercurial, so mass convert string keys and values to bytes.
167 # in Mercurial, so mass convert string keys and values to bytes.
86 if pycompat.ispy3:
168 if pycompat.ispy3:
87 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
169 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
88 env = {k: v.encode('latin-1') if isinstance(v, str) else v
170 env = {k: v.encode('latin-1') if isinstance(v, str) else v
89 for k, v in env.iteritems()}
171 for k, v in env.iteritems()}
90
172
91 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
173 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
92 # the environment variables.
174 # the environment variables.
93 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
175 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
94 # how URLs are reconstructed.
176 # how URLs are reconstructed.
95 fullurl = env['wsgi.url_scheme'] + '://'
177 fullurl = env['wsgi.url_scheme'] + '://'
96 advertisedfullurl = fullurl
178 advertisedfullurl = fullurl
97
179
98 def addport(s):
180 def addport(s):
99 if env['wsgi.url_scheme'] == 'https':
181 if env['wsgi.url_scheme'] == 'https':
100 if env['SERVER_PORT'] != '443':
182 if env['SERVER_PORT'] != '443':
101 s += ':' + env['SERVER_PORT']
183 s += ':' + env['SERVER_PORT']
102 else:
184 else:
103 if env['SERVER_PORT'] != '80':
185 if env['SERVER_PORT'] != '80':
104 s += ':' + env['SERVER_PORT']
186 s += ':' + env['SERVER_PORT']
105
187
106 return s
188 return s
107
189
108 if env.get('HTTP_HOST'):
190 if env.get('HTTP_HOST'):
109 fullurl += env['HTTP_HOST']
191 fullurl += env['HTTP_HOST']
110 else:
192 else:
111 fullurl += env['SERVER_NAME']
193 fullurl += env['SERVER_NAME']
112 fullurl = addport(fullurl)
194 fullurl = addport(fullurl)
113
195
114 advertisedfullurl += env['SERVER_NAME']
196 advertisedfullurl += env['SERVER_NAME']
115 advertisedfullurl = addport(advertisedfullurl)
197 advertisedfullurl = addport(advertisedfullurl)
116
198
117 baseurl = fullurl
199 baseurl = fullurl
118 advertisedbaseurl = advertisedfullurl
200 advertisedbaseurl = advertisedfullurl
119
201
120 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
202 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
121 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
203 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
122 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
204 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
123 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
205 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
124
206
125 if env.get('QUERY_STRING'):
207 if env.get('QUERY_STRING'):
126 fullurl += '?' + env['QUERY_STRING']
208 fullurl += '?' + env['QUERY_STRING']
127 advertisedfullurl += '?' + env['QUERY_STRING']
209 advertisedfullurl += '?' + env['QUERY_STRING']
128
210
129 # When dispatching requests, we look at the URL components (PATH_INFO
211 # When dispatching requests, we look at the URL components (PATH_INFO
130 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
212 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
131 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
213 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
132 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
214 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
133 # root. We also exclude its path components from PATH_INFO when resolving
215 # root. We also exclude its path components from PATH_INFO when resolving
134 # the dispatch path.
216 # the dispatch path.
135
217
136 apppath = env['SCRIPT_NAME']
218 apppath = env['SCRIPT_NAME']
137
219
138 if env.get('REPO_NAME'):
220 if env.get('REPO_NAME'):
139 if not apppath.endswith('/'):
221 if not apppath.endswith('/'):
140 apppath += '/'
222 apppath += '/'
141
223
142 apppath += env.get('REPO_NAME')
224 apppath += env.get('REPO_NAME')
143
225
144 if 'PATH_INFO' in env:
226 if 'PATH_INFO' in env:
145 dispatchparts = env['PATH_INFO'].strip('/').split('/')
227 dispatchparts = env['PATH_INFO'].strip('/').split('/')
146
228
147 # Strip out repo parts.
229 # Strip out repo parts.
148 repoparts = env.get('REPO_NAME', '').split('/')
230 repoparts = env.get('REPO_NAME', '').split('/')
149 if dispatchparts[:len(repoparts)] == repoparts:
231 if dispatchparts[:len(repoparts)] == repoparts:
150 dispatchparts = dispatchparts[len(repoparts):]
232 dispatchparts = dispatchparts[len(repoparts):]
151 else:
233 else:
152 dispatchparts = []
234 dispatchparts = []
153
235
154 dispatchpath = '/'.join(dispatchparts)
236 dispatchpath = '/'.join(dispatchparts)
155
237
156 querystring = env.get('QUERY_STRING', '')
238 querystring = env.get('QUERY_STRING', '')
157
239
158 # We store as a list so we have ordering information. We also store as
240 # We store as a list so we have ordering information. We also store as
159 # a dict to facilitate fast lookup.
241 # a dict to facilitate fast lookup.
160 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
242 qsparams = multidict()
161
243 for k, v in util.urlreq.parseqsl(querystring, keep_blank_values=True):
162 querystringdict = {}
244 qsparams.add(k, v)
163 for k, v in querystringlist:
164 if k in querystringdict:
165 querystringdict[k].append(v)
166 else:
167 querystringdict[k] = [v]
168
245
169 # HTTP_* keys contain HTTP request headers. The Headers structure should
246 # HTTP_* keys contain HTTP request headers. The Headers structure should
170 # perform case normalization for us. We just rewrite underscore to dash
247 # perform case normalization for us. We just rewrite underscore to dash
171 # so keys match what likely went over the wire.
248 # so keys match what likely went over the wire.
172 headers = []
249 headers = []
173 for k, v in env.iteritems():
250 for k, v in env.iteritems():
174 if k.startswith('HTTP_'):
251 if k.startswith('HTTP_'):
175 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
252 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
176
253
177 headers = wsgiheaders.Headers(headers)
254 headers = wsgiheaders.Headers(headers)
178
255
179 # This is kind of a lie because the HTTP header wasn't explicitly
256 # This is kind of a lie because the HTTP header wasn't explicitly
180 # sent. But for all intents and purposes it should be OK to lie about
257 # sent. But for all intents and purposes it should be OK to lie about
181 # this, since a consumer will either either value to determine how many
258 # this, since a consumer will either either value to determine how many
182 # bytes are available to read.
259 # bytes are available to read.
183 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
260 if 'CONTENT_LENGTH' in env and 'HTTP_CONTENT_LENGTH' not in env:
184 headers['Content-Length'] = env['CONTENT_LENGTH']
261 headers['Content-Length'] = env['CONTENT_LENGTH']
185
262
186 # TODO do this once we remove wsgirequest.inp, otherwise we could have
263 # TODO do this once we remove wsgirequest.inp, otherwise we could have
187 # multiple readers from the underlying input stream.
264 # multiple readers from the underlying input stream.
188 #bodyfh = env['wsgi.input']
265 #bodyfh = env['wsgi.input']
189 #if 'Content-Length' in headers:
266 #if 'Content-Length' in headers:
190 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
267 # bodyfh = util.cappedreader(bodyfh, int(headers['Content-Length']))
191
268
192 return parsedrequest(method=env['REQUEST_METHOD'],
269 return parsedrequest(method=env['REQUEST_METHOD'],
193 url=fullurl, baseurl=baseurl,
270 url=fullurl, baseurl=baseurl,
194 advertisedurl=advertisedfullurl,
271 advertisedurl=advertisedfullurl,
195 advertisedbaseurl=advertisedbaseurl,
272 advertisedbaseurl=advertisedbaseurl,
196 apppath=apppath,
273 apppath=apppath,
197 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
274 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
198 havepathinfo='PATH_INFO' in env,
275 havepathinfo='PATH_INFO' in env,
199 querystring=querystring,
276 querystring=querystring,
200 querystringlist=querystringlist,
277 qsparams=qsparams,
201 querystringdict=querystringdict,
202 headers=headers,
278 headers=headers,
203 bodyfh=bodyfh)
279 bodyfh=bodyfh)
204
280
205 class wsgiresponse(object):
281 class wsgiresponse(object):
206 """Represents a response to a WSGI request.
282 """Represents a response to a WSGI request.
207
283
208 A response consists of a status line, headers, and a body.
284 A response consists of a status line, headers, and a body.
209
285
210 Consumers must populate the ``status`` and ``headers`` fields and
286 Consumers must populate the ``status`` and ``headers`` fields and
211 make a call to a ``setbody*()`` method before the response can be
287 make a call to a ``setbody*()`` method before the response can be
212 issued.
288 issued.
213
289
214 When it is time to start sending the response over the wire,
290 When it is time to start sending the response over the wire,
215 ``sendresponse()`` is called. It handles emitting the header portion
291 ``sendresponse()`` is called. It handles emitting the header portion
216 of the response message. It then yields chunks of body data to be
292 of the response message. It then yields chunks of body data to be
217 written to the peer. Typically, the WSGI application itself calls
293 written to the peer. Typically, the WSGI application itself calls
218 and returns the value from ``sendresponse()``.
294 and returns the value from ``sendresponse()``.
219 """
295 """
220
296
221 def __init__(self, req, startresponse):
297 def __init__(self, req, startresponse):
222 """Create an empty response tied to a specific request.
298 """Create an empty response tied to a specific request.
223
299
224 ``req`` is a ``parsedrequest``. ``startresponse`` is the
300 ``req`` is a ``parsedrequest``. ``startresponse`` is the
225 ``start_response`` function passed to the WSGI application.
301 ``start_response`` function passed to the WSGI application.
226 """
302 """
227 self._req = req
303 self._req = req
228 self._startresponse = startresponse
304 self._startresponse = startresponse
229
305
230 self.status = None
306 self.status = None
231 self.headers = wsgiheaders.Headers([])
307 self.headers = wsgiheaders.Headers([])
232
308
233 self._bodybytes = None
309 self._bodybytes = None
234 self._bodygen = None
310 self._bodygen = None
235 self._started = False
311 self._started = False
236
312
237 def setbodybytes(self, b):
313 def setbodybytes(self, b):
238 """Define the response body as static bytes."""
314 """Define the response body as static bytes."""
239 if self._bodybytes is not None or self._bodygen is not None:
315 if self._bodybytes is not None or self._bodygen is not None:
240 raise error.ProgrammingError('cannot define body multiple times')
316 raise error.ProgrammingError('cannot define body multiple times')
241
317
242 self._bodybytes = b
318 self._bodybytes = b
243 self.headers['Content-Length'] = '%d' % len(b)
319 self.headers['Content-Length'] = '%d' % len(b)
244
320
245 def setbodygen(self, gen):
321 def setbodygen(self, gen):
246 """Define the response body as a generator of bytes."""
322 """Define the response body as a generator of bytes."""
247 if self._bodybytes is not None or self._bodygen is not None:
323 if self._bodybytes is not None or self._bodygen is not None:
248 raise error.ProgrammingError('cannot define body multiple times')
324 raise error.ProgrammingError('cannot define body multiple times')
249
325
250 self._bodygen = gen
326 self._bodygen = gen
251
327
252 def sendresponse(self):
328 def sendresponse(self):
253 """Send the generated response to the client.
329 """Send the generated response to the client.
254
330
255 Before this is called, ``status`` must be set and one of
331 Before this is called, ``status`` must be set and one of
256 ``setbodybytes()`` or ``setbodygen()`` must be called.
332 ``setbodybytes()`` or ``setbodygen()`` must be called.
257
333
258 Calling this method multiple times is not allowed.
334 Calling this method multiple times is not allowed.
259 """
335 """
260 if self._started:
336 if self._started:
261 raise error.ProgrammingError('sendresponse() called multiple times')
337 raise error.ProgrammingError('sendresponse() called multiple times')
262
338
263 self._started = True
339 self._started = True
264
340
265 if not self.status:
341 if not self.status:
266 raise error.ProgrammingError('status line not defined')
342 raise error.ProgrammingError('status line not defined')
267
343
268 if self._bodybytes is None and self._bodygen is None:
344 if self._bodybytes is None and self._bodygen is None:
269 raise error.ProgrammingError('response body not defined')
345 raise error.ProgrammingError('response body not defined')
270
346
271 # Various HTTP clients (notably httplib) won't read the HTTP response
347 # Various HTTP clients (notably httplib) won't read the HTTP response
272 # until the HTTP request has been sent in full. If servers (us) send a
348 # until the HTTP request has been sent in full. If servers (us) send a
273 # response before the HTTP request has been fully sent, the connection
349 # response before the HTTP request has been fully sent, the connection
274 # may deadlock because neither end is reading.
350 # may deadlock because neither end is reading.
275 #
351 #
276 # We work around this by "draining" the request data before
352 # We work around this by "draining" the request data before
277 # sending any response in some conditions.
353 # sending any response in some conditions.
278 drain = False
354 drain = False
279 close = False
355 close = False
280
356
281 # If the client sent Expect: 100-continue, we assume it is smart enough
357 # If the client sent Expect: 100-continue, we assume it is smart enough
282 # to deal with the server sending a response before reading the request.
358 # to deal with the server sending a response before reading the request.
283 # (httplib doesn't do this.)
359 # (httplib doesn't do this.)
284 if self._req.headers.get('Expect', '').lower() == '100-continue':
360 if self._req.headers.get('Expect', '').lower() == '100-continue':
285 pass
361 pass
286 # Only tend to request methods that have bodies. Strictly speaking,
362 # Only tend to request methods that have bodies. Strictly speaking,
287 # we should sniff for a body. But this is fine for our existing
363 # we should sniff for a body. But this is fine for our existing
288 # WSGI applications.
364 # WSGI applications.
289 elif self._req.method not in ('POST', 'PUT'):
365 elif self._req.method not in ('POST', 'PUT'):
290 pass
366 pass
291 else:
367 else:
292 # If we don't know how much data to read, there's no guarantee
368 # If we don't know how much data to read, there's no guarantee
293 # that we can drain the request responsibly. The WSGI
369 # that we can drain the request responsibly. The WSGI
294 # specification only says that servers *should* ensure the
370 # specification only says that servers *should* ensure the
295 # input stream doesn't overrun the actual request. So there's
371 # input stream doesn't overrun the actual request. So there's
296 # no guarantee that reading until EOF won't corrupt the stream
372 # no guarantee that reading until EOF won't corrupt the stream
297 # state.
373 # state.
298 if not isinstance(self._req.bodyfh, util.cappedreader):
374 if not isinstance(self._req.bodyfh, util.cappedreader):
299 close = True
375 close = True
300 else:
376 else:
301 # We /could/ only drain certain HTTP response codes. But 200 and
377 # We /could/ only drain certain HTTP response codes. But 200 and
302 # non-200 wire protocol responses both require draining. Since
378 # non-200 wire protocol responses both require draining. Since
303 # we have a capped reader in place for all situations where we
379 # we have a capped reader in place for all situations where we
304 # drain, it is safe to read from that stream. We'll either do
380 # drain, it is safe to read from that stream. We'll either do
305 # a drain or no-op if we're already at EOF.
381 # a drain or no-op if we're already at EOF.
306 drain = True
382 drain = True
307
383
308 if close:
384 if close:
309 self.headers['Connection'] = 'Close'
385 self.headers['Connection'] = 'Close'
310
386
311 if drain:
387 if drain:
312 assert isinstance(self._req.bodyfh, util.cappedreader)
388 assert isinstance(self._req.bodyfh, util.cappedreader)
313 while True:
389 while True:
314 chunk = self._req.bodyfh.read(32768)
390 chunk = self._req.bodyfh.read(32768)
315 if not chunk:
391 if not chunk:
316 break
392 break
317
393
318 self._startresponse(pycompat.sysstr(self.status), self.headers.items())
394 self._startresponse(pycompat.sysstr(self.status), self.headers.items())
319 if self._bodybytes:
395 if self._bodybytes:
320 yield self._bodybytes
396 yield self._bodybytes
321 elif self._bodygen:
397 elif self._bodygen:
322 for chunk in self._bodygen:
398 for chunk in self._bodygen:
323 yield chunk
399 yield chunk
324 else:
400 else:
325 error.ProgrammingError('do not know how to send body')
401 error.ProgrammingError('do not know how to send body')
326
402
327 class wsgirequest(object):
403 class wsgirequest(object):
328 """Higher-level API for a WSGI request.
404 """Higher-level API for a WSGI request.
329
405
330 WSGI applications are invoked with 2 arguments. They are used to
406 WSGI applications are invoked with 2 arguments. They are used to
331 instantiate instances of this class, which provides higher-level APIs
407 instantiate instances of this class, which provides higher-level APIs
332 for obtaining request parameters, writing HTTP output, etc.
408 for obtaining request parameters, writing HTTP output, etc.
333 """
409 """
334 def __init__(self, wsgienv, start_response):
410 def __init__(self, wsgienv, start_response):
335 version = wsgienv[r'wsgi.version']
411 version = wsgienv[r'wsgi.version']
336 if (version < (1, 0)) or (version >= (2, 0)):
412 if (version < (1, 0)) or (version >= (2, 0)):
337 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
413 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
338 % version)
414 % version)
339
415
340 inp = wsgienv[r'wsgi.input']
416 inp = wsgienv[r'wsgi.input']
341
417
342 if r'HTTP_CONTENT_LENGTH' in wsgienv:
418 if r'HTTP_CONTENT_LENGTH' in wsgienv:
343 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
419 inp = util.cappedreader(inp, int(wsgienv[r'HTTP_CONTENT_LENGTH']))
344 elif r'CONTENT_LENGTH' in wsgienv:
420 elif r'CONTENT_LENGTH' in wsgienv:
345 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
421 inp = util.cappedreader(inp, int(wsgienv[r'CONTENT_LENGTH']))
346
422
347 self.err = wsgienv[r'wsgi.errors']
423 self.err = wsgienv[r'wsgi.errors']
348 self.threaded = wsgienv[r'wsgi.multithread']
424 self.threaded = wsgienv[r'wsgi.multithread']
349 self.multiprocess = wsgienv[r'wsgi.multiprocess']
425 self.multiprocess = wsgienv[r'wsgi.multiprocess']
350 self.run_once = wsgienv[r'wsgi.run_once']
426 self.run_once = wsgienv[r'wsgi.run_once']
351 self.env = wsgienv
427 self.env = wsgienv
352 self.req = parserequestfromenv(wsgienv, inp)
428 self.req = parserequestfromenv(wsgienv, inp)
353 self.form = self.req.querystringdict
429 self.form = self.req.qsparams.asdictoflists()
354 self.res = wsgiresponse(self.req, start_response)
430 self.res = wsgiresponse(self.req, start_response)
355 self._start_response = start_response
431 self._start_response = start_response
356 self.server_write = None
432 self.server_write = None
357 self.headers = []
433 self.headers = []
358
434
359 def respond(self, status, type, filename=None, body=None):
435 def respond(self, status, type, filename=None, body=None):
360 if not isinstance(type, str):
436 if not isinstance(type, str):
361 type = pycompat.sysstr(type)
437 type = pycompat.sysstr(type)
362 if self._start_response is not None:
438 if self._start_response is not None:
363 self.headers.append((r'Content-Type', type))
439 self.headers.append((r'Content-Type', type))
364 if filename:
440 if filename:
365 filename = (filename.rpartition('/')[-1]
441 filename = (filename.rpartition('/')[-1]
366 .replace('\\', '\\\\').replace('"', '\\"'))
442 .replace('\\', '\\\\').replace('"', '\\"'))
367 self.headers.append(('Content-Disposition',
443 self.headers.append(('Content-Disposition',
368 'inline; filename="%s"' % filename))
444 'inline; filename="%s"' % filename))
369 if body is not None:
445 if body is not None:
370 self.headers.append((r'Content-Length', str(len(body))))
446 self.headers.append((r'Content-Length', str(len(body))))
371
447
372 for k, v in self.headers:
448 for k, v in self.headers:
373 if not isinstance(v, str):
449 if not isinstance(v, str):
374 raise TypeError('header value must be string: %r' % (v,))
450 raise TypeError('header value must be string: %r' % (v,))
375
451
376 if isinstance(status, ErrorResponse):
452 if isinstance(status, ErrorResponse):
377 self.headers.extend(status.headers)
453 self.headers.extend(status.headers)
378 if status.code == HTTP_NOT_MODIFIED:
454 if status.code == HTTP_NOT_MODIFIED:
379 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
455 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
380 # it MUST NOT include any headers other than these and no
456 # it MUST NOT include any headers other than these and no
381 # body
457 # body
382 self.headers = [(k, v) for (k, v) in self.headers if
458 self.headers = [(k, v) for (k, v) in self.headers if
383 k in ('Date', 'ETag', 'Expires',
459 k in ('Date', 'ETag', 'Expires',
384 'Cache-Control', 'Vary')]
460 'Cache-Control', 'Vary')]
385 status = statusmessage(status.code, pycompat.bytestr(status))
461 status = statusmessage(status.code, pycompat.bytestr(status))
386 elif status == 200:
462 elif status == 200:
387 status = '200 Script output follows'
463 status = '200 Script output follows'
388 elif isinstance(status, int):
464 elif isinstance(status, int):
389 status = statusmessage(status)
465 status = statusmessage(status)
390
466
391 # Various HTTP clients (notably httplib) won't read the HTTP
467 # Various HTTP clients (notably httplib) won't read the HTTP
392 # response until the HTTP request has been sent in full. If servers
468 # response until the HTTP request has been sent in full. If servers
393 # (us) send a response before the HTTP request has been fully sent,
469 # (us) send a response before the HTTP request has been fully sent,
394 # the connection may deadlock because neither end is reading.
470 # the connection may deadlock because neither end is reading.
395 #
471 #
396 # We work around this by "draining" the request data before
472 # We work around this by "draining" the request data before
397 # sending any response in some conditions.
473 # sending any response in some conditions.
398 drain = False
474 drain = False
399 close = False
475 close = False
400
476
401 # If the client sent Expect: 100-continue, we assume it is smart
477 # If the client sent Expect: 100-continue, we assume it is smart
402 # enough to deal with the server sending a response before reading
478 # enough to deal with the server sending a response before reading
403 # the request. (httplib doesn't do this.)
479 # the request. (httplib doesn't do this.)
404 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
480 if self.env.get(r'HTTP_EXPECT', r'').lower() == r'100-continue':
405 pass
481 pass
406 # Only tend to request methods that have bodies. Strictly speaking,
482 # Only tend to request methods that have bodies. Strictly speaking,
407 # we should sniff for a body. But this is fine for our existing
483 # we should sniff for a body. But this is fine for our existing
408 # WSGI applications.
484 # WSGI applications.
409 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
485 elif self.env[r'REQUEST_METHOD'] not in (r'POST', r'PUT'):
410 pass
486 pass
411 else:
487 else:
412 # If we don't know how much data to read, there's no guarantee
488 # If we don't know how much data to read, there's no guarantee
413 # that we can drain the request responsibly. The WSGI
489 # that we can drain the request responsibly. The WSGI
414 # specification only says that servers *should* ensure the
490 # specification only says that servers *should* ensure the
415 # input stream doesn't overrun the actual request. So there's
491 # input stream doesn't overrun the actual request. So there's
416 # no guarantee that reading until EOF won't corrupt the stream
492 # no guarantee that reading until EOF won't corrupt the stream
417 # state.
493 # state.
418 if not isinstance(self.req.bodyfh, util.cappedreader):
494 if not isinstance(self.req.bodyfh, util.cappedreader):
419 close = True
495 close = True
420 else:
496 else:
421 # We /could/ only drain certain HTTP response codes. But 200
497 # We /could/ only drain certain HTTP response codes. But 200
422 # and non-200 wire protocol responses both require draining.
498 # and non-200 wire protocol responses both require draining.
423 # Since we have a capped reader in place for all situations
499 # Since we have a capped reader in place for all situations
424 # where we drain, it is safe to read from that stream. We'll
500 # where we drain, it is safe to read from that stream. We'll
425 # either do a drain or no-op if we're already at EOF.
501 # either do a drain or no-op if we're already at EOF.
426 drain = True
502 drain = True
427
503
428 if close:
504 if close:
429 self.headers.append((r'Connection', r'Close'))
505 self.headers.append((r'Connection', r'Close'))
430
506
431 if drain:
507 if drain:
432 assert isinstance(self.req.bodyfh, util.cappedreader)
508 assert isinstance(self.req.bodyfh, util.cappedreader)
433 while True:
509 while True:
434 chunk = self.req.bodyfh.read(32768)
510 chunk = self.req.bodyfh.read(32768)
435 if not chunk:
511 if not chunk:
436 break
512 break
437
513
438 self.server_write = self._start_response(
514 self.server_write = self._start_response(
439 pycompat.sysstr(status), self.headers)
515 pycompat.sysstr(status), self.headers)
440 self._start_response = None
516 self._start_response = None
441 self.headers = []
517 self.headers = []
442 if body is not None:
518 if body is not None:
443 self.write(body)
519 self.write(body)
444 self.server_write = None
520 self.server_write = None
445
521
446 def write(self, thing):
522 def write(self, thing):
447 if thing:
523 if thing:
448 try:
524 try:
449 self.server_write(thing)
525 self.server_write(thing)
450 except socket.error as inst:
526 except socket.error as inst:
451 if inst[0] != errno.ECONNRESET:
527 if inst[0] != errno.ECONNRESET:
452 raise
528 raise
453
529
454 def flush(self):
530 def flush(self):
455 return None
531 return None
456
532
457 def wsgiapplication(app_maker):
533 def wsgiapplication(app_maker):
458 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
534 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
459 can and should now be used as a WSGI application.'''
535 can and should now be used as a WSGI application.'''
460 application = app_maker()
536 application = app_maker()
461 def run_wsgi(env, respond):
537 def run_wsgi(env, respond):
462 return application(env, respond)
538 return application(env, respond)
463 return run_wsgi
539 return run_wsgi
@@ -1,655 +1,655 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import contextlib
9 import contextlib
10 import struct
10 import struct
11 import sys
11 import sys
12 import threading
12 import threading
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 hook,
18 hook,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireproto,
21 wireproto,
22 wireprototypes,
22 wireprototypes,
23 )
23 )
24
24
25 stringio = util.stringio
25 stringio = util.stringio
26
26
27 urlerr = util.urlerr
27 urlerr = util.urlerr
28 urlreq = util.urlreq
28 urlreq = util.urlreq
29
29
30 HTTP_OK = 200
30 HTTP_OK = 200
31
31
32 HGTYPE = 'application/mercurial-0.1'
32 HGTYPE = 'application/mercurial-0.1'
33 HGTYPE2 = 'application/mercurial-0.2'
33 HGTYPE2 = 'application/mercurial-0.2'
34 HGERRTYPE = 'application/hg-error'
34 HGERRTYPE = 'application/hg-error'
35
35
36 SSHV1 = wireprototypes.SSHV1
36 SSHV1 = wireprototypes.SSHV1
37 SSHV2 = wireprototypes.SSHV2
37 SSHV2 = wireprototypes.SSHV2
38
38
39 def decodevaluefromheaders(req, headerprefix):
39 def decodevaluefromheaders(req, headerprefix):
40 """Decode a long value from multiple HTTP request headers.
40 """Decode a long value from multiple HTTP request headers.
41
41
42 Returns the value as a bytes, not a str.
42 Returns the value as a bytes, not a str.
43 """
43 """
44 chunks = []
44 chunks = []
45 i = 1
45 i = 1
46 while True:
46 while True:
47 v = req.headers.get(b'%s-%d' % (headerprefix, i))
47 v = req.headers.get(b'%s-%d' % (headerprefix, i))
48 if v is None:
48 if v is None:
49 break
49 break
50 chunks.append(pycompat.bytesurl(v))
50 chunks.append(pycompat.bytesurl(v))
51 i += 1
51 i += 1
52
52
53 return ''.join(chunks)
53 return ''.join(chunks)
54
54
55 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
55 class httpv1protocolhandler(wireprototypes.baseprotocolhandler):
56 def __init__(self, wsgireq, req, ui, checkperm):
56 def __init__(self, wsgireq, req, ui, checkperm):
57 self._wsgireq = wsgireq
57 self._wsgireq = wsgireq
58 self._req = req
58 self._req = req
59 self._ui = ui
59 self._ui = ui
60 self._checkperm = checkperm
60 self._checkperm = checkperm
61
61
62 @property
62 @property
63 def name(self):
63 def name(self):
64 return 'http-v1'
64 return 'http-v1'
65
65
66 def getargs(self, args):
66 def getargs(self, args):
67 knownargs = self._args()
67 knownargs = self._args()
68 data = {}
68 data = {}
69 keys = args.split()
69 keys = args.split()
70 for k in keys:
70 for k in keys:
71 if k == '*':
71 if k == '*':
72 star = {}
72 star = {}
73 for key in knownargs.keys():
73 for key in knownargs.keys():
74 if key != 'cmd' and key not in keys:
74 if key != 'cmd' and key not in keys:
75 star[key] = knownargs[key][0]
75 star[key] = knownargs[key][0]
76 data['*'] = star
76 data['*'] = star
77 else:
77 else:
78 data[k] = knownargs[k][0]
78 data[k] = knownargs[k][0]
79 return [data[k] for k in keys]
79 return [data[k] for k in keys]
80
80
81 def _args(self):
81 def _args(self):
82 args = util.rapply(pycompat.bytesurl, self._wsgireq.form.copy())
82 args = self._req.qsparams.asdictoflists()
83 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
83 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
84 if postlen:
84 if postlen:
85 args.update(urlreq.parseqs(
85 args.update(urlreq.parseqs(
86 self._req.bodyfh.read(postlen), keep_blank_values=True))
86 self._req.bodyfh.read(postlen), keep_blank_values=True))
87 return args
87 return args
88
88
89 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
89 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
90 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
91 return args
91 return args
92
92
93 def forwardpayload(self, fp):
93 def forwardpayload(self, fp):
94 # Existing clients *always* send Content-Length.
94 # Existing clients *always* send Content-Length.
95 length = int(self._req.headers[b'Content-Length'])
95 length = int(self._req.headers[b'Content-Length'])
96
96
97 # If httppostargs is used, we need to read Content-Length
97 # If httppostargs is used, we need to read Content-Length
98 # minus the amount that was consumed by args.
98 # minus the amount that was consumed by args.
99 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
99 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
100 for s in util.filechunkiter(self._req.bodyfh, limit=length):
100 for s in util.filechunkiter(self._req.bodyfh, limit=length):
101 fp.write(s)
101 fp.write(s)
102
102
103 @contextlib.contextmanager
103 @contextlib.contextmanager
104 def mayberedirectstdio(self):
104 def mayberedirectstdio(self):
105 oldout = self._ui.fout
105 oldout = self._ui.fout
106 olderr = self._ui.ferr
106 olderr = self._ui.ferr
107
107
108 out = util.stringio()
108 out = util.stringio()
109
109
110 try:
110 try:
111 self._ui.fout = out
111 self._ui.fout = out
112 self._ui.ferr = out
112 self._ui.ferr = out
113 yield out
113 yield out
114 finally:
114 finally:
115 self._ui.fout = oldout
115 self._ui.fout = oldout
116 self._ui.ferr = olderr
116 self._ui.ferr = olderr
117
117
118 def client(self):
118 def client(self):
119 return 'remote:%s:%s:%s' % (
119 return 'remote:%s:%s:%s' % (
120 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
120 self._wsgireq.env.get('wsgi.url_scheme') or 'http',
121 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
121 urlreq.quote(self._wsgireq.env.get('REMOTE_HOST', '')),
122 urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
122 urlreq.quote(self._wsgireq.env.get('REMOTE_USER', '')))
123
123
124 def addcapabilities(self, repo, caps):
124 def addcapabilities(self, repo, caps):
125 caps.append('httpheader=%d' %
125 caps.append('httpheader=%d' %
126 repo.ui.configint('server', 'maxhttpheaderlen'))
126 repo.ui.configint('server', 'maxhttpheaderlen'))
127 if repo.ui.configbool('experimental', 'httppostargs'):
127 if repo.ui.configbool('experimental', 'httppostargs'):
128 caps.append('httppostargs')
128 caps.append('httppostargs')
129
129
130 # FUTURE advertise 0.2rx once support is implemented
130 # FUTURE advertise 0.2rx once support is implemented
131 # FUTURE advertise minrx and mintx after consulting config option
131 # FUTURE advertise minrx and mintx after consulting config option
132 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
132 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
133
133
134 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
134 compengines = wireproto.supportedcompengines(repo.ui, util.SERVERROLE)
135 if compengines:
135 if compengines:
136 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
136 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
137 for e in compengines)
137 for e in compengines)
138 caps.append('compression=%s' % comptypes)
138 caps.append('compression=%s' % comptypes)
139
139
140 return caps
140 return caps
141
141
142 def checkperm(self, perm):
142 def checkperm(self, perm):
143 return self._checkperm(perm)
143 return self._checkperm(perm)
144
144
145 # This method exists mostly so that extensions like remotefilelog can
145 # This method exists mostly so that extensions like remotefilelog can
146 # disable a kludgey legacy method only over http. As of early 2018,
146 # disable a kludgey legacy method only over http. As of early 2018,
147 # there are no other known users, so with any luck we can discard this
147 # there are no other known users, so with any luck we can discard this
148 # hook if remotefilelog becomes a first-party extension.
148 # hook if remotefilelog becomes a first-party extension.
149 def iscmd(cmd):
149 def iscmd(cmd):
150 return cmd in wireproto.commands
150 return cmd in wireproto.commands
151
151
152 def handlewsgirequest(rctx, wsgireq, req, res, checkperm):
152 def handlewsgirequest(rctx, wsgireq, req, res, checkperm):
153 """Possibly process a wire protocol request.
153 """Possibly process a wire protocol request.
154
154
155 If the current request is a wire protocol request, the request is
155 If the current request is a wire protocol request, the request is
156 processed by this function.
156 processed by this function.
157
157
158 ``wsgireq`` is a ``wsgirequest`` instance.
158 ``wsgireq`` is a ``wsgirequest`` instance.
159 ``req`` is a ``parsedrequest`` instance.
159 ``req`` is a ``parsedrequest`` instance.
160 ``res`` is a ``wsgiresponse`` instance.
160 ``res`` is a ``wsgiresponse`` instance.
161
161
162 Returns a bool indicating if the request was serviced. If set, the caller
162 Returns a bool indicating if the request was serviced. If set, the caller
163 should stop processing the request, as a response has already been issued.
163 should stop processing the request, as a response has already been issued.
164 """
164 """
165 # Avoid cycle involving hg module.
165 # Avoid cycle involving hg module.
166 from .hgweb import common as hgwebcommon
166 from .hgweb import common as hgwebcommon
167
167
168 repo = rctx.repo
168 repo = rctx.repo
169
169
170 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
170 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
171 # string parameter. If it isn't present, this isn't a wire protocol
171 # string parameter. If it isn't present, this isn't a wire protocol
172 # request.
172 # request.
173 if 'cmd' not in req.querystringdict:
173 if 'cmd' not in req.qsparams:
174 return False
174 return False
175
175
176 cmd = req.querystringdict['cmd'][0]
176 cmd = req.qsparams['cmd']
177
177
178 # The "cmd" request parameter is used by both the wire protocol and hgweb.
178 # The "cmd" request parameter is used by both the wire protocol and hgweb.
179 # While not all wire protocol commands are available for all transports,
179 # While not all wire protocol commands are available for all transports,
180 # if we see a "cmd" value that resembles a known wire protocol command, we
180 # if we see a "cmd" value that resembles a known wire protocol command, we
181 # route it to a protocol handler. This is better than routing possible
181 # route it to a protocol handler. This is better than routing possible
182 # wire protocol requests to hgweb because it prevents hgweb from using
182 # wire protocol requests to hgweb because it prevents hgweb from using
183 # known wire protocol commands and it is less confusing for machine
183 # known wire protocol commands and it is less confusing for machine
184 # clients.
184 # clients.
185 if not iscmd(cmd):
185 if not iscmd(cmd):
186 return False
186 return False
187
187
188 # The "cmd" query string argument is only valid on the root path of the
188 # The "cmd" query string argument is only valid on the root path of the
189 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
189 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
190 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
190 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
191 # in this case. We send an HTTP 404 for backwards compatibility reasons.
191 # in this case. We send an HTTP 404 for backwards compatibility reasons.
192 if req.dispatchpath:
192 if req.dispatchpath:
193 res.status = hgwebcommon.statusmessage(404)
193 res.status = hgwebcommon.statusmessage(404)
194 res.headers['Content-Type'] = HGTYPE
194 res.headers['Content-Type'] = HGTYPE
195 # TODO This is not a good response to issue for this request. This
195 # TODO This is not a good response to issue for this request. This
196 # is mostly for BC for now.
196 # is mostly for BC for now.
197 res.setbodybytes('0\n%s\n' % b'Not Found')
197 res.setbodybytes('0\n%s\n' % b'Not Found')
198 return True
198 return True
199
199
200 proto = httpv1protocolhandler(wsgireq, req, repo.ui,
200 proto = httpv1protocolhandler(wsgireq, req, repo.ui,
201 lambda perm: checkperm(rctx, wsgireq, perm))
201 lambda perm: checkperm(rctx, wsgireq, perm))
202
202
203 # The permissions checker should be the only thing that can raise an
203 # The permissions checker should be the only thing that can raise an
204 # ErrorResponse. It is kind of a layer violation to catch an hgweb
204 # ErrorResponse. It is kind of a layer violation to catch an hgweb
205 # exception here. So consider refactoring into a exception type that
205 # exception here. So consider refactoring into a exception type that
206 # is associated with the wire protocol.
206 # is associated with the wire protocol.
207 try:
207 try:
208 _callhttp(repo, wsgireq, req, res, proto, cmd)
208 _callhttp(repo, wsgireq, req, res, proto, cmd)
209 except hgwebcommon.ErrorResponse as e:
209 except hgwebcommon.ErrorResponse as e:
210 for k, v in e.headers:
210 for k, v in e.headers:
211 res.headers[k] = v
211 res.headers[k] = v
212 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
212 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
213 # TODO This response body assumes the failed command was
213 # TODO This response body assumes the failed command was
214 # "unbundle." That assumption is not always valid.
214 # "unbundle." That assumption is not always valid.
215 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
215 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e))
216
216
217 return True
217 return True
218
218
219 def _httpresponsetype(ui, req, prefer_uncompressed):
219 def _httpresponsetype(ui, req, prefer_uncompressed):
220 """Determine the appropriate response type and compression settings.
220 """Determine the appropriate response type and compression settings.
221
221
222 Returns a tuple of (mediatype, compengine, engineopts).
222 Returns a tuple of (mediatype, compengine, engineopts).
223 """
223 """
224 # Determine the response media type and compression engine based
224 # Determine the response media type and compression engine based
225 # on the request parameters.
225 # on the request parameters.
226 protocaps = decodevaluefromheaders(req, 'X-HgProto').split(' ')
226 protocaps = decodevaluefromheaders(req, 'X-HgProto').split(' ')
227
227
228 if '0.2' in protocaps:
228 if '0.2' in protocaps:
229 # All clients are expected to support uncompressed data.
229 # All clients are expected to support uncompressed data.
230 if prefer_uncompressed:
230 if prefer_uncompressed:
231 return HGTYPE2, util._noopengine(), {}
231 return HGTYPE2, util._noopengine(), {}
232
232
233 # Default as defined by wire protocol spec.
233 # Default as defined by wire protocol spec.
234 compformats = ['zlib', 'none']
234 compformats = ['zlib', 'none']
235 for cap in protocaps:
235 for cap in protocaps:
236 if cap.startswith('comp='):
236 if cap.startswith('comp='):
237 compformats = cap[5:].split(',')
237 compformats = cap[5:].split(',')
238 break
238 break
239
239
240 # Now find an agreed upon compression format.
240 # Now find an agreed upon compression format.
241 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
241 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
242 if engine.wireprotosupport().name in compformats:
242 if engine.wireprotosupport().name in compformats:
243 opts = {}
243 opts = {}
244 level = ui.configint('server', '%slevel' % engine.name())
244 level = ui.configint('server', '%slevel' % engine.name())
245 if level is not None:
245 if level is not None:
246 opts['level'] = level
246 opts['level'] = level
247
247
248 return HGTYPE2, engine, opts
248 return HGTYPE2, engine, opts
249
249
250 # No mutually supported compression format. Fall back to the
250 # No mutually supported compression format. Fall back to the
251 # legacy protocol.
251 # legacy protocol.
252
252
253 # Don't allow untrusted settings because disabling compression or
253 # Don't allow untrusted settings because disabling compression or
254 # setting a very high compression level could lead to flooding
254 # setting a very high compression level could lead to flooding
255 # the server's network or CPU.
255 # the server's network or CPU.
256 opts = {'level': ui.configint('server', 'zliblevel')}
256 opts = {'level': ui.configint('server', 'zliblevel')}
257 return HGTYPE, util.compengines['zlib'], opts
257 return HGTYPE, util.compengines['zlib'], opts
258
258
259 def _callhttp(repo, wsgireq, req, res, proto, cmd):
259 def _callhttp(repo, wsgireq, req, res, proto, cmd):
260 # Avoid cycle involving hg module.
260 # Avoid cycle involving hg module.
261 from .hgweb import common as hgwebcommon
261 from .hgweb import common as hgwebcommon
262
262
263 def genversion2(gen, engine, engineopts):
263 def genversion2(gen, engine, engineopts):
264 # application/mercurial-0.2 always sends a payload header
264 # application/mercurial-0.2 always sends a payload header
265 # identifying the compression engine.
265 # identifying the compression engine.
266 name = engine.wireprotosupport().name
266 name = engine.wireprotosupport().name
267 assert 0 < len(name) < 256
267 assert 0 < len(name) < 256
268 yield struct.pack('B', len(name))
268 yield struct.pack('B', len(name))
269 yield name
269 yield name
270
270
271 for chunk in gen:
271 for chunk in gen:
272 yield chunk
272 yield chunk
273
273
274 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
274 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
275 if code == HTTP_OK:
275 if code == HTTP_OK:
276 res.status = '200 Script output follows'
276 res.status = '200 Script output follows'
277 else:
277 else:
278 res.status = hgwebcommon.statusmessage(code)
278 res.status = hgwebcommon.statusmessage(code)
279
279
280 res.headers['Content-Type'] = contenttype
280 res.headers['Content-Type'] = contenttype
281
281
282 if bodybytes is not None:
282 if bodybytes is not None:
283 res.setbodybytes(bodybytes)
283 res.setbodybytes(bodybytes)
284 if bodygen is not None:
284 if bodygen is not None:
285 res.setbodygen(bodygen)
285 res.setbodygen(bodygen)
286
286
287 if not wireproto.commands.commandavailable(cmd, proto):
287 if not wireproto.commands.commandavailable(cmd, proto):
288 setresponse(HTTP_OK, HGERRTYPE,
288 setresponse(HTTP_OK, HGERRTYPE,
289 _('requested wire protocol command is not available over '
289 _('requested wire protocol command is not available over '
290 'HTTP'))
290 'HTTP'))
291 return
291 return
292
292
293 proto.checkperm(wireproto.commands[cmd].permission)
293 proto.checkperm(wireproto.commands[cmd].permission)
294
294
295 rsp = wireproto.dispatch(repo, proto, cmd)
295 rsp = wireproto.dispatch(repo, proto, cmd)
296
296
297 if isinstance(rsp, bytes):
297 if isinstance(rsp, bytes):
298 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
298 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
299 elif isinstance(rsp, wireprototypes.bytesresponse):
299 elif isinstance(rsp, wireprototypes.bytesresponse):
300 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
300 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
301 elif isinstance(rsp, wireprototypes.streamreslegacy):
301 elif isinstance(rsp, wireprototypes.streamreslegacy):
302 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
302 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
303 elif isinstance(rsp, wireprototypes.streamres):
303 elif isinstance(rsp, wireprototypes.streamres):
304 gen = rsp.gen
304 gen = rsp.gen
305
305
306 # This code for compression should not be streamres specific. It
306 # This code for compression should not be streamres specific. It
307 # is here because we only compress streamres at the moment.
307 # is here because we only compress streamres at the moment.
308 mediatype, engine, engineopts = _httpresponsetype(
308 mediatype, engine, engineopts = _httpresponsetype(
309 repo.ui, req, rsp.prefer_uncompressed)
309 repo.ui, req, rsp.prefer_uncompressed)
310 gen = engine.compressstream(gen, engineopts)
310 gen = engine.compressstream(gen, engineopts)
311
311
312 if mediatype == HGTYPE2:
312 if mediatype == HGTYPE2:
313 gen = genversion2(gen, engine, engineopts)
313 gen = genversion2(gen, engine, engineopts)
314
314
315 setresponse(HTTP_OK, mediatype, bodygen=gen)
315 setresponse(HTTP_OK, mediatype, bodygen=gen)
316 elif isinstance(rsp, wireprototypes.pushres):
316 elif isinstance(rsp, wireprototypes.pushres):
317 rsp = '%d\n%s' % (rsp.res, rsp.output)
317 rsp = '%d\n%s' % (rsp.res, rsp.output)
318 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
318 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
319 elif isinstance(rsp, wireprototypes.pusherr):
319 elif isinstance(rsp, wireprototypes.pusherr):
320 rsp = '0\n%s\n' % rsp.res
320 rsp = '0\n%s\n' % rsp.res
321 res.drain = True
321 res.drain = True
322 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
322 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
323 elif isinstance(rsp, wireprototypes.ooberror):
323 elif isinstance(rsp, wireprototypes.ooberror):
324 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
324 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
325 else:
325 else:
326 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
326 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
327
327
328 def _sshv1respondbytes(fout, value):
328 def _sshv1respondbytes(fout, value):
329 """Send a bytes response for protocol version 1."""
329 """Send a bytes response for protocol version 1."""
330 fout.write('%d\n' % len(value))
330 fout.write('%d\n' % len(value))
331 fout.write(value)
331 fout.write(value)
332 fout.flush()
332 fout.flush()
333
333
334 def _sshv1respondstream(fout, source):
334 def _sshv1respondstream(fout, source):
335 write = fout.write
335 write = fout.write
336 for chunk in source.gen:
336 for chunk in source.gen:
337 write(chunk)
337 write(chunk)
338 fout.flush()
338 fout.flush()
339
339
340 def _sshv1respondooberror(fout, ferr, rsp):
340 def _sshv1respondooberror(fout, ferr, rsp):
341 ferr.write(b'%s\n-\n' % rsp)
341 ferr.write(b'%s\n-\n' % rsp)
342 ferr.flush()
342 ferr.flush()
343 fout.write(b'\n')
343 fout.write(b'\n')
344 fout.flush()
344 fout.flush()
345
345
346 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
346 class sshv1protocolhandler(wireprototypes.baseprotocolhandler):
347 """Handler for requests services via version 1 of SSH protocol."""
347 """Handler for requests services via version 1 of SSH protocol."""
348 def __init__(self, ui, fin, fout):
348 def __init__(self, ui, fin, fout):
349 self._ui = ui
349 self._ui = ui
350 self._fin = fin
350 self._fin = fin
351 self._fout = fout
351 self._fout = fout
352
352
353 @property
353 @property
354 def name(self):
354 def name(self):
355 return wireprototypes.SSHV1
355 return wireprototypes.SSHV1
356
356
357 def getargs(self, args):
357 def getargs(self, args):
358 data = {}
358 data = {}
359 keys = args.split()
359 keys = args.split()
360 for n in xrange(len(keys)):
360 for n in xrange(len(keys)):
361 argline = self._fin.readline()[:-1]
361 argline = self._fin.readline()[:-1]
362 arg, l = argline.split()
362 arg, l = argline.split()
363 if arg not in keys:
363 if arg not in keys:
364 raise error.Abort(_("unexpected parameter %r") % arg)
364 raise error.Abort(_("unexpected parameter %r") % arg)
365 if arg == '*':
365 if arg == '*':
366 star = {}
366 star = {}
367 for k in xrange(int(l)):
367 for k in xrange(int(l)):
368 argline = self._fin.readline()[:-1]
368 argline = self._fin.readline()[:-1]
369 arg, l = argline.split()
369 arg, l = argline.split()
370 val = self._fin.read(int(l))
370 val = self._fin.read(int(l))
371 star[arg] = val
371 star[arg] = val
372 data['*'] = star
372 data['*'] = star
373 else:
373 else:
374 val = self._fin.read(int(l))
374 val = self._fin.read(int(l))
375 data[arg] = val
375 data[arg] = val
376 return [data[k] for k in keys]
376 return [data[k] for k in keys]
377
377
378 def forwardpayload(self, fpout):
378 def forwardpayload(self, fpout):
379 # We initially send an empty response. This tells the client it is
379 # We initially send an empty response. This tells the client it is
380 # OK to start sending data. If a client sees any other response, it
380 # OK to start sending data. If a client sees any other response, it
381 # interprets it as an error.
381 # interprets it as an error.
382 _sshv1respondbytes(self._fout, b'')
382 _sshv1respondbytes(self._fout, b'')
383
383
384 # The file is in the form:
384 # The file is in the form:
385 #
385 #
386 # <chunk size>\n<chunk>
386 # <chunk size>\n<chunk>
387 # ...
387 # ...
388 # 0\n
388 # 0\n
389 count = int(self._fin.readline())
389 count = int(self._fin.readline())
390 while count:
390 while count:
391 fpout.write(self._fin.read(count))
391 fpout.write(self._fin.read(count))
392 count = int(self._fin.readline())
392 count = int(self._fin.readline())
393
393
394 @contextlib.contextmanager
394 @contextlib.contextmanager
395 def mayberedirectstdio(self):
395 def mayberedirectstdio(self):
396 yield None
396 yield None
397
397
398 def client(self):
398 def client(self):
399 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
399 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
400 return 'remote:ssh:' + client
400 return 'remote:ssh:' + client
401
401
402 def addcapabilities(self, repo, caps):
402 def addcapabilities(self, repo, caps):
403 return caps
403 return caps
404
404
405 def checkperm(self, perm):
405 def checkperm(self, perm):
406 pass
406 pass
407
407
408 class sshv2protocolhandler(sshv1protocolhandler):
408 class sshv2protocolhandler(sshv1protocolhandler):
409 """Protocol handler for version 2 of the SSH protocol."""
409 """Protocol handler for version 2 of the SSH protocol."""
410
410
411 @property
411 @property
412 def name(self):
412 def name(self):
413 return wireprototypes.SSHV2
413 return wireprototypes.SSHV2
414
414
415 def _runsshserver(ui, repo, fin, fout, ev):
415 def _runsshserver(ui, repo, fin, fout, ev):
416 # This function operates like a state machine of sorts. The following
416 # This function operates like a state machine of sorts. The following
417 # states are defined:
417 # states are defined:
418 #
418 #
419 # protov1-serving
419 # protov1-serving
420 # Server is in protocol version 1 serving mode. Commands arrive on
420 # Server is in protocol version 1 serving mode. Commands arrive on
421 # new lines. These commands are processed in this state, one command
421 # new lines. These commands are processed in this state, one command
422 # after the other.
422 # after the other.
423 #
423 #
424 # protov2-serving
424 # protov2-serving
425 # Server is in protocol version 2 serving mode.
425 # Server is in protocol version 2 serving mode.
426 #
426 #
427 # upgrade-initial
427 # upgrade-initial
428 # The server is going to process an upgrade request.
428 # The server is going to process an upgrade request.
429 #
429 #
430 # upgrade-v2-filter-legacy-handshake
430 # upgrade-v2-filter-legacy-handshake
431 # The protocol is being upgraded to version 2. The server is expecting
431 # The protocol is being upgraded to version 2. The server is expecting
432 # the legacy handshake from version 1.
432 # the legacy handshake from version 1.
433 #
433 #
434 # upgrade-v2-finish
434 # upgrade-v2-finish
435 # The upgrade to version 2 of the protocol is imminent.
435 # The upgrade to version 2 of the protocol is imminent.
436 #
436 #
437 # shutdown
437 # shutdown
438 # The server is shutting down, possibly in reaction to a client event.
438 # The server is shutting down, possibly in reaction to a client event.
439 #
439 #
440 # And here are their transitions:
440 # And here are their transitions:
441 #
441 #
442 # protov1-serving -> shutdown
442 # protov1-serving -> shutdown
443 # When server receives an empty request or encounters another
443 # When server receives an empty request or encounters another
444 # error.
444 # error.
445 #
445 #
446 # protov1-serving -> upgrade-initial
446 # protov1-serving -> upgrade-initial
447 # An upgrade request line was seen.
447 # An upgrade request line was seen.
448 #
448 #
449 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
449 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
450 # Upgrade to version 2 in progress. Server is expecting to
450 # Upgrade to version 2 in progress. Server is expecting to
451 # process a legacy handshake.
451 # process a legacy handshake.
452 #
452 #
453 # upgrade-v2-filter-legacy-handshake -> shutdown
453 # upgrade-v2-filter-legacy-handshake -> shutdown
454 # Client did not fulfill upgrade handshake requirements.
454 # Client did not fulfill upgrade handshake requirements.
455 #
455 #
456 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
456 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
457 # Client fulfilled version 2 upgrade requirements. Finishing that
457 # Client fulfilled version 2 upgrade requirements. Finishing that
458 # upgrade.
458 # upgrade.
459 #
459 #
460 # upgrade-v2-finish -> protov2-serving
460 # upgrade-v2-finish -> protov2-serving
461 # Protocol upgrade to version 2 complete. Server can now speak protocol
461 # Protocol upgrade to version 2 complete. Server can now speak protocol
462 # version 2.
462 # version 2.
463 #
463 #
464 # protov2-serving -> protov1-serving
464 # protov2-serving -> protov1-serving
465 # Ths happens by default since protocol version 2 is the same as
465 # Ths happens by default since protocol version 2 is the same as
466 # version 1 except for the handshake.
466 # version 1 except for the handshake.
467
467
468 state = 'protov1-serving'
468 state = 'protov1-serving'
469 proto = sshv1protocolhandler(ui, fin, fout)
469 proto = sshv1protocolhandler(ui, fin, fout)
470 protoswitched = False
470 protoswitched = False
471
471
472 while not ev.is_set():
472 while not ev.is_set():
473 if state == 'protov1-serving':
473 if state == 'protov1-serving':
474 # Commands are issued on new lines.
474 # Commands are issued on new lines.
475 request = fin.readline()[:-1]
475 request = fin.readline()[:-1]
476
476
477 # Empty lines signal to terminate the connection.
477 # Empty lines signal to terminate the connection.
478 if not request:
478 if not request:
479 state = 'shutdown'
479 state = 'shutdown'
480 continue
480 continue
481
481
482 # It looks like a protocol upgrade request. Transition state to
482 # It looks like a protocol upgrade request. Transition state to
483 # handle it.
483 # handle it.
484 if request.startswith(b'upgrade '):
484 if request.startswith(b'upgrade '):
485 if protoswitched:
485 if protoswitched:
486 _sshv1respondooberror(fout, ui.ferr,
486 _sshv1respondooberror(fout, ui.ferr,
487 b'cannot upgrade protocols multiple '
487 b'cannot upgrade protocols multiple '
488 b'times')
488 b'times')
489 state = 'shutdown'
489 state = 'shutdown'
490 continue
490 continue
491
491
492 state = 'upgrade-initial'
492 state = 'upgrade-initial'
493 continue
493 continue
494
494
495 available = wireproto.commands.commandavailable(request, proto)
495 available = wireproto.commands.commandavailable(request, proto)
496
496
497 # This command isn't available. Send an empty response and go
497 # This command isn't available. Send an empty response and go
498 # back to waiting for a new command.
498 # back to waiting for a new command.
499 if not available:
499 if not available:
500 _sshv1respondbytes(fout, b'')
500 _sshv1respondbytes(fout, b'')
501 continue
501 continue
502
502
503 rsp = wireproto.dispatch(repo, proto, request)
503 rsp = wireproto.dispatch(repo, proto, request)
504
504
505 if isinstance(rsp, bytes):
505 if isinstance(rsp, bytes):
506 _sshv1respondbytes(fout, rsp)
506 _sshv1respondbytes(fout, rsp)
507 elif isinstance(rsp, wireprototypes.bytesresponse):
507 elif isinstance(rsp, wireprototypes.bytesresponse):
508 _sshv1respondbytes(fout, rsp.data)
508 _sshv1respondbytes(fout, rsp.data)
509 elif isinstance(rsp, wireprototypes.streamres):
509 elif isinstance(rsp, wireprototypes.streamres):
510 _sshv1respondstream(fout, rsp)
510 _sshv1respondstream(fout, rsp)
511 elif isinstance(rsp, wireprototypes.streamreslegacy):
511 elif isinstance(rsp, wireprototypes.streamreslegacy):
512 _sshv1respondstream(fout, rsp)
512 _sshv1respondstream(fout, rsp)
513 elif isinstance(rsp, wireprototypes.pushres):
513 elif isinstance(rsp, wireprototypes.pushres):
514 _sshv1respondbytes(fout, b'')
514 _sshv1respondbytes(fout, b'')
515 _sshv1respondbytes(fout, b'%d' % rsp.res)
515 _sshv1respondbytes(fout, b'%d' % rsp.res)
516 elif isinstance(rsp, wireprototypes.pusherr):
516 elif isinstance(rsp, wireprototypes.pusherr):
517 _sshv1respondbytes(fout, rsp.res)
517 _sshv1respondbytes(fout, rsp.res)
518 elif isinstance(rsp, wireprototypes.ooberror):
518 elif isinstance(rsp, wireprototypes.ooberror):
519 _sshv1respondooberror(fout, ui.ferr, rsp.message)
519 _sshv1respondooberror(fout, ui.ferr, rsp.message)
520 else:
520 else:
521 raise error.ProgrammingError('unhandled response type from '
521 raise error.ProgrammingError('unhandled response type from '
522 'wire protocol command: %s' % rsp)
522 'wire protocol command: %s' % rsp)
523
523
524 # For now, protocol version 2 serving just goes back to version 1.
524 # For now, protocol version 2 serving just goes back to version 1.
525 elif state == 'protov2-serving':
525 elif state == 'protov2-serving':
526 state = 'protov1-serving'
526 state = 'protov1-serving'
527 continue
527 continue
528
528
529 elif state == 'upgrade-initial':
529 elif state == 'upgrade-initial':
530 # We should never transition into this state if we've switched
530 # We should never transition into this state if we've switched
531 # protocols.
531 # protocols.
532 assert not protoswitched
532 assert not protoswitched
533 assert proto.name == wireprototypes.SSHV1
533 assert proto.name == wireprototypes.SSHV1
534
534
535 # Expected: upgrade <token> <capabilities>
535 # Expected: upgrade <token> <capabilities>
536 # If we get something else, the request is malformed. It could be
536 # If we get something else, the request is malformed. It could be
537 # from a future client that has altered the upgrade line content.
537 # from a future client that has altered the upgrade line content.
538 # We treat this as an unknown command.
538 # We treat this as an unknown command.
539 try:
539 try:
540 token, caps = request.split(b' ')[1:]
540 token, caps = request.split(b' ')[1:]
541 except ValueError:
541 except ValueError:
542 _sshv1respondbytes(fout, b'')
542 _sshv1respondbytes(fout, b'')
543 state = 'protov1-serving'
543 state = 'protov1-serving'
544 continue
544 continue
545
545
546 # Send empty response if we don't support upgrading protocols.
546 # Send empty response if we don't support upgrading protocols.
547 if not ui.configbool('experimental', 'sshserver.support-v2'):
547 if not ui.configbool('experimental', 'sshserver.support-v2'):
548 _sshv1respondbytes(fout, b'')
548 _sshv1respondbytes(fout, b'')
549 state = 'protov1-serving'
549 state = 'protov1-serving'
550 continue
550 continue
551
551
552 try:
552 try:
553 caps = urlreq.parseqs(caps)
553 caps = urlreq.parseqs(caps)
554 except ValueError:
554 except ValueError:
555 _sshv1respondbytes(fout, b'')
555 _sshv1respondbytes(fout, b'')
556 state = 'protov1-serving'
556 state = 'protov1-serving'
557 continue
557 continue
558
558
559 # We don't see an upgrade request to protocol version 2. Ignore
559 # We don't see an upgrade request to protocol version 2. Ignore
560 # the upgrade request.
560 # the upgrade request.
561 wantedprotos = caps.get(b'proto', [b''])[0]
561 wantedprotos = caps.get(b'proto', [b''])[0]
562 if SSHV2 not in wantedprotos:
562 if SSHV2 not in wantedprotos:
563 _sshv1respondbytes(fout, b'')
563 _sshv1respondbytes(fout, b'')
564 state = 'protov1-serving'
564 state = 'protov1-serving'
565 continue
565 continue
566
566
567 # It looks like we can honor this upgrade request to protocol 2.
567 # It looks like we can honor this upgrade request to protocol 2.
568 # Filter the rest of the handshake protocol request lines.
568 # Filter the rest of the handshake protocol request lines.
569 state = 'upgrade-v2-filter-legacy-handshake'
569 state = 'upgrade-v2-filter-legacy-handshake'
570 continue
570 continue
571
571
572 elif state == 'upgrade-v2-filter-legacy-handshake':
572 elif state == 'upgrade-v2-filter-legacy-handshake':
573 # Client should have sent legacy handshake after an ``upgrade``
573 # Client should have sent legacy handshake after an ``upgrade``
574 # request. Expected lines:
574 # request. Expected lines:
575 #
575 #
576 # hello
576 # hello
577 # between
577 # between
578 # pairs 81
578 # pairs 81
579 # 0000...-0000...
579 # 0000...-0000...
580
580
581 ok = True
581 ok = True
582 for line in (b'hello', b'between', b'pairs 81'):
582 for line in (b'hello', b'between', b'pairs 81'):
583 request = fin.readline()[:-1]
583 request = fin.readline()[:-1]
584
584
585 if request != line:
585 if request != line:
586 _sshv1respondooberror(fout, ui.ferr,
586 _sshv1respondooberror(fout, ui.ferr,
587 b'malformed handshake protocol: '
587 b'malformed handshake protocol: '
588 b'missing %s' % line)
588 b'missing %s' % line)
589 ok = False
589 ok = False
590 state = 'shutdown'
590 state = 'shutdown'
591 break
591 break
592
592
593 if not ok:
593 if not ok:
594 continue
594 continue
595
595
596 request = fin.read(81)
596 request = fin.read(81)
597 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
597 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
598 _sshv1respondooberror(fout, ui.ferr,
598 _sshv1respondooberror(fout, ui.ferr,
599 b'malformed handshake protocol: '
599 b'malformed handshake protocol: '
600 b'missing between argument value')
600 b'missing between argument value')
601 state = 'shutdown'
601 state = 'shutdown'
602 continue
602 continue
603
603
604 state = 'upgrade-v2-finish'
604 state = 'upgrade-v2-finish'
605 continue
605 continue
606
606
607 elif state == 'upgrade-v2-finish':
607 elif state == 'upgrade-v2-finish':
608 # Send the upgrade response.
608 # Send the upgrade response.
609 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
609 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
610 servercaps = wireproto.capabilities(repo, proto)
610 servercaps = wireproto.capabilities(repo, proto)
611 rsp = b'capabilities: %s' % servercaps.data
611 rsp = b'capabilities: %s' % servercaps.data
612 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
612 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
613 fout.flush()
613 fout.flush()
614
614
615 proto = sshv2protocolhandler(ui, fin, fout)
615 proto = sshv2protocolhandler(ui, fin, fout)
616 protoswitched = True
616 protoswitched = True
617
617
618 state = 'protov2-serving'
618 state = 'protov2-serving'
619 continue
619 continue
620
620
621 elif state == 'shutdown':
621 elif state == 'shutdown':
622 break
622 break
623
623
624 else:
624 else:
625 raise error.ProgrammingError('unhandled ssh server state: %s' %
625 raise error.ProgrammingError('unhandled ssh server state: %s' %
626 state)
626 state)
627
627
628 class sshserver(object):
628 class sshserver(object):
629 def __init__(self, ui, repo, logfh=None):
629 def __init__(self, ui, repo, logfh=None):
630 self._ui = ui
630 self._ui = ui
631 self._repo = repo
631 self._repo = repo
632 self._fin = ui.fin
632 self._fin = ui.fin
633 self._fout = ui.fout
633 self._fout = ui.fout
634
634
635 # Log write I/O to stdout and stderr if configured.
635 # Log write I/O to stdout and stderr if configured.
636 if logfh:
636 if logfh:
637 self._fout = util.makeloggingfileobject(
637 self._fout = util.makeloggingfileobject(
638 logfh, self._fout, 'o', logdata=True)
638 logfh, self._fout, 'o', logdata=True)
639 ui.ferr = util.makeloggingfileobject(
639 ui.ferr = util.makeloggingfileobject(
640 logfh, ui.ferr, 'e', logdata=True)
640 logfh, ui.ferr, 'e', logdata=True)
641
641
642 hook.redirect(True)
642 hook.redirect(True)
643 ui.fout = repo.ui.fout = ui.ferr
643 ui.fout = repo.ui.fout = ui.ferr
644
644
645 # Prevent insertion/deletion of CRs
645 # Prevent insertion/deletion of CRs
646 util.setbinary(self._fin)
646 util.setbinary(self._fin)
647 util.setbinary(self._fout)
647 util.setbinary(self._fout)
648
648
649 def serve_forever(self):
649 def serve_forever(self):
650 self.serveuntil(threading.Event())
650 self.serveuntil(threading.Event())
651 sys.exit(0)
651 sys.exit(0)
652
652
653 def serveuntil(self, ev):
653 def serveuntil(self, ev):
654 """Serve until a threading.Event is set."""
654 """Serve until a threading.Event is set."""
655 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
655 _runsshserver(self._ui, self._repo, self._fin, self._fout, ev)
General Comments 0
You need to be logged in to leave comments. Login now