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