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