##// END OF EJS Templates
hgweb: use native strings when interfacing with stdlib headers...
Augie Fackler -
r37609:b5ca5d34 default
parent child Browse files
Show More
@@ -1,359 +1,359 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web 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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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 os
12 import os
13 import socket
13 import socket
14 import sys
14 import sys
15 import traceback
15 import traceback
16 import wsgiref.validate
16 import wsgiref.validate
17
17
18 from ..i18n import _
18 from ..i18n import _
19
19
20 from .. import (
20 from .. import (
21 encoding,
21 encoding,
22 error,
22 error,
23 pycompat,
23 pycompat,
24 util,
24 util,
25 )
25 )
26
26
27 httpservermod = util.httpserver
27 httpservermod = util.httpserver
28 socketserver = util.socketserver
28 socketserver = util.socketserver
29 urlerr = util.urlerr
29 urlerr = util.urlerr
30 urlreq = util.urlreq
30 urlreq = util.urlreq
31
31
32 from . import (
32 from . import (
33 common,
33 common,
34 )
34 )
35
35
36 def _splitURI(uri):
36 def _splitURI(uri):
37 """Return path and query that has been split from uri
37 """Return path and query that has been split from uri
38
38
39 Just like CGI environment, the path is unquoted, the query is
39 Just like CGI environment, the path is unquoted, the query is
40 not.
40 not.
41 """
41 """
42 if r'?' in uri:
42 if r'?' in uri:
43 path, query = uri.split(r'?', 1)
43 path, query = uri.split(r'?', 1)
44 else:
44 else:
45 path, query = uri, r''
45 path, query = uri, r''
46 return urlreq.unquote(path), query
46 return urlreq.unquote(path), query
47
47
48 class _error_logger(object):
48 class _error_logger(object):
49 def __init__(self, handler):
49 def __init__(self, handler):
50 self.handler = handler
50 self.handler = handler
51 def flush(self):
51 def flush(self):
52 pass
52 pass
53 def write(self, str):
53 def write(self, str):
54 self.writelines(str.split('\n'))
54 self.writelines(str.split('\n'))
55 def writelines(self, seq):
55 def writelines(self, seq):
56 for msg in seq:
56 for msg in seq:
57 self.handler.log_error("HG error: %s", msg)
57 self.handler.log_error("HG error: %s", msg)
58
58
59 class _httprequesthandler(httpservermod.basehttprequesthandler):
59 class _httprequesthandler(httpservermod.basehttprequesthandler):
60
60
61 url_scheme = 'http'
61 url_scheme = 'http'
62
62
63 @staticmethod
63 @staticmethod
64 def preparehttpserver(httpserver, ui):
64 def preparehttpserver(httpserver, ui):
65 """Prepare .socket of new HTTPServer instance"""
65 """Prepare .socket of new HTTPServer instance"""
66
66
67 def __init__(self, *args, **kargs):
67 def __init__(self, *args, **kargs):
68 self.protocol_version = r'HTTP/1.1'
68 self.protocol_version = r'HTTP/1.1'
69 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
70
70
71 def _log_any(self, fp, format, *args):
71 def _log_any(self, fp, format, *args):
72 fp.write(pycompat.sysbytes(
72 fp.write(pycompat.sysbytes(
73 r"%s - - [%s] %s" % (self.client_address[0],
73 r"%s - - [%s] %s" % (self.client_address[0],
74 self.log_date_time_string(),
74 self.log_date_time_string(),
75 format % args)) + '\n')
75 format % args)) + '\n')
76 fp.flush()
76 fp.flush()
77
77
78 def log_error(self, format, *args):
78 def log_error(self, format, *args):
79 self._log_any(self.server.errorlog, format, *args)
79 self._log_any(self.server.errorlog, format, *args)
80
80
81 def log_message(self, format, *args):
81 def log_message(self, format, *args):
82 self._log_any(self.server.accesslog, format, *args)
82 self._log_any(self.server.accesslog, format, *args)
83
83
84 def log_request(self, code=r'-', size=r'-'):
84 def log_request(self, code=r'-', size=r'-'):
85 xheaders = []
85 xheaders = []
86 if util.safehasattr(self, 'headers'):
86 if util.safehasattr(self, 'headers'):
87 xheaders = [h for h in self.headers.items()
87 xheaders = [h for h in self.headers.items()
88 if h[0].startswith(r'x-')]
88 if h[0].startswith(r'x-')]
89 self.log_message(r'"%s" %s %s%s',
89 self.log_message(r'"%s" %s %s%s',
90 self.requestline, str(code), str(size),
90 self.requestline, str(code), str(size),
91 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
92
92
93 def do_write(self):
93 def do_write(self):
94 try:
94 try:
95 self.do_hgweb()
95 self.do_hgweb()
96 except socket.error as inst:
96 except socket.error as inst:
97 if inst[0] != errno.EPIPE:
97 if inst[0] != errno.EPIPE:
98 raise
98 raise
99
99
100 def do_POST(self):
100 def do_POST(self):
101 try:
101 try:
102 self.do_write()
102 self.do_write()
103 except Exception:
103 except Exception:
104 self._start_response("500 Internal Server Error", [])
104 self._start_response("500 Internal Server Error", [])
105 self._write("Internal Server Error")
105 self._write("Internal Server Error")
106 self._done()
106 self._done()
107 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 tb = r"".join(traceback.format_exception(*sys.exc_info()))
108 # We need a native-string newline to poke in the log
108 # We need a native-string newline to poke in the log
109 # message, because we won't get a newline when using an
109 # message, because we won't get a newline when using an
110 # r-string. This is the easy way out.
110 # r-string. This is the easy way out.
111 newline = chr(10)
111 newline = chr(10)
112 self.log_error(r"Exception happened during processing "
112 self.log_error(r"Exception happened during processing "
113 r"request '%s':%s%s", self.path, newline, tb)
113 r"request '%s':%s%s", self.path, newline, tb)
114
114
115 def do_PUT(self):
115 def do_PUT(self):
116 self.do_POST()
116 self.do_POST()
117
117
118 def do_GET(self):
118 def do_GET(self):
119 self.do_POST()
119 self.do_POST()
120
120
121 def do_hgweb(self):
121 def do_hgweb(self):
122 self.sent_headers = False
122 self.sent_headers = False
123 path, query = _splitURI(self.path)
123 path, query = _splitURI(self.path)
124
124
125 env = {}
125 env = {}
126 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
126 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
127 env[r'REQUEST_METHOD'] = self.command
127 env[r'REQUEST_METHOD'] = self.command
128 env[r'SERVER_NAME'] = self.server.server_name
128 env[r'SERVER_NAME'] = self.server.server_name
129 env[r'SERVER_PORT'] = str(self.server.server_port)
129 env[r'SERVER_PORT'] = str(self.server.server_port)
130 env[r'REQUEST_URI'] = self.path
130 env[r'REQUEST_URI'] = self.path
131 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
131 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
132 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
132 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
133 env[r'REMOTE_HOST'] = self.client_address[0]
133 env[r'REMOTE_HOST'] = self.client_address[0]
134 env[r'REMOTE_ADDR'] = self.client_address[0]
134 env[r'REMOTE_ADDR'] = self.client_address[0]
135 env[r'QUERY_STRING'] = query or r''
135 env[r'QUERY_STRING'] = query or r''
136
136
137 if pycompat.ispy3:
137 if pycompat.ispy3:
138 if self.headers.get_content_type() is None:
138 if self.headers.get_content_type() is None:
139 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
139 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
140 else:
140 else:
141 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
141 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
142 length = self.headers.get('content-length')
142 length = self.headers.get(r'content-length')
143 else:
143 else:
144 if self.headers.typeheader is None:
144 if self.headers.typeheader is None:
145 env[r'CONTENT_TYPE'] = self.headers.type
145 env[r'CONTENT_TYPE'] = self.headers.type
146 else:
146 else:
147 env[r'CONTENT_TYPE'] = self.headers.typeheader
147 env[r'CONTENT_TYPE'] = self.headers.typeheader
148 length = self.headers.getheader('content-length')
148 length = self.headers.getheader(r'content-length')
149 if length:
149 if length:
150 env[r'CONTENT_LENGTH'] = length
150 env[r'CONTENT_LENGTH'] = length
151 for header in [h for h in self.headers.keys()
151 for header in [h for h in self.headers.keys()
152 if h not in ('content-type', 'content-length')]:
152 if h not in (r'content-type', r'content-length')]:
153 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
153 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
154 hval = self.headers.get(header)
154 hval = self.headers.get(header)
155 hval = hval.replace(r'\n', r'').strip()
155 hval = hval.replace(r'\n', r'').strip()
156 if hval:
156 if hval:
157 env[hkey] = hval
157 env[hkey] = hval
158 env[r'SERVER_PROTOCOL'] = self.request_version
158 env[r'SERVER_PROTOCOL'] = self.request_version
159 env[r'wsgi.version'] = (1, 0)
159 env[r'wsgi.version'] = (1, 0)
160 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
160 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
161 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
161 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
162 self.rfile = common.continuereader(self.rfile, self.wfile.write)
162 self.rfile = common.continuereader(self.rfile, self.wfile.write)
163
163
164 env[r'wsgi.input'] = self.rfile
164 env[r'wsgi.input'] = self.rfile
165 env[r'wsgi.errors'] = _error_logger(self)
165 env[r'wsgi.errors'] = _error_logger(self)
166 env[r'wsgi.multithread'] = isinstance(self.server,
166 env[r'wsgi.multithread'] = isinstance(self.server,
167 socketserver.ThreadingMixIn)
167 socketserver.ThreadingMixIn)
168 env[r'wsgi.multiprocess'] = isinstance(self.server,
168 env[r'wsgi.multiprocess'] = isinstance(self.server,
169 socketserver.ForkingMixIn)
169 socketserver.ForkingMixIn)
170 env[r'wsgi.run_once'] = 0
170 env[r'wsgi.run_once'] = 0
171
171
172 wsgiref.validate.check_environ(env)
172 wsgiref.validate.check_environ(env)
173
173
174 self.saved_status = None
174 self.saved_status = None
175 self.saved_headers = []
175 self.saved_headers = []
176 self.length = None
176 self.length = None
177 self._chunked = None
177 self._chunked = None
178 for chunk in self.server.application(env, self._start_response):
178 for chunk in self.server.application(env, self._start_response):
179 self._write(chunk)
179 self._write(chunk)
180 if not self.sent_headers:
180 if not self.sent_headers:
181 self.send_headers()
181 self.send_headers()
182 self._done()
182 self._done()
183
183
184 def send_headers(self):
184 def send_headers(self):
185 if not self.saved_status:
185 if not self.saved_status:
186 raise AssertionError("Sending headers before "
186 raise AssertionError("Sending headers before "
187 "start_response() called")
187 "start_response() called")
188 saved_status = self.saved_status.split(None, 1)
188 saved_status = self.saved_status.split(None, 1)
189 saved_status[0] = int(saved_status[0])
189 saved_status[0] = int(saved_status[0])
190 self.send_response(*saved_status)
190 self.send_response(*saved_status)
191 self.length = None
191 self.length = None
192 self._chunked = False
192 self._chunked = False
193 for h in self.saved_headers:
193 for h in self.saved_headers:
194 self.send_header(*h)
194 self.send_header(*h)
195 if h[0].lower() == 'content-length':
195 if h[0].lower() == 'content-length':
196 self.length = int(h[1])
196 self.length = int(h[1])
197 if (self.length is None and
197 if (self.length is None and
198 saved_status[0] != common.HTTP_NOT_MODIFIED):
198 saved_status[0] != common.HTTP_NOT_MODIFIED):
199 self._chunked = (not self.close_connection and
199 self._chunked = (not self.close_connection and
200 self.request_version == "HTTP/1.1")
200 self.request_version == "HTTP/1.1")
201 if self._chunked:
201 if self._chunked:
202 self.send_header(r'Transfer-Encoding', r'chunked')
202 self.send_header(r'Transfer-Encoding', r'chunked')
203 else:
203 else:
204 self.send_header(r'Connection', r'close')
204 self.send_header(r'Connection', r'close')
205 self.end_headers()
205 self.end_headers()
206 self.sent_headers = True
206 self.sent_headers = True
207
207
208 def _start_response(self, http_status, headers, exc_info=None):
208 def _start_response(self, http_status, headers, exc_info=None):
209 code, msg = http_status.split(None, 1)
209 code, msg = http_status.split(None, 1)
210 code = int(code)
210 code = int(code)
211 self.saved_status = http_status
211 self.saved_status = http_status
212 bad_headers = ('connection', 'transfer-encoding')
212 bad_headers = ('connection', 'transfer-encoding')
213 self.saved_headers = [h for h in headers
213 self.saved_headers = [h for h in headers
214 if h[0].lower() not in bad_headers]
214 if h[0].lower() not in bad_headers]
215 return self._write
215 return self._write
216
216
217 def _write(self, data):
217 def _write(self, data):
218 if not self.saved_status:
218 if not self.saved_status:
219 raise AssertionError("data written before start_response() called")
219 raise AssertionError("data written before start_response() called")
220 elif not self.sent_headers:
220 elif not self.sent_headers:
221 self.send_headers()
221 self.send_headers()
222 if self.length is not None:
222 if self.length is not None:
223 if len(data) > self.length:
223 if len(data) > self.length:
224 raise AssertionError("Content-length header sent, but more "
224 raise AssertionError("Content-length header sent, but more "
225 "bytes than specified are being written.")
225 "bytes than specified are being written.")
226 self.length = self.length - len(data)
226 self.length = self.length - len(data)
227 elif self._chunked and data:
227 elif self._chunked and data:
228 data = '%x\r\n%s\r\n' % (len(data), data)
228 data = '%x\r\n%s\r\n' % (len(data), data)
229 self.wfile.write(data)
229 self.wfile.write(data)
230 self.wfile.flush()
230 self.wfile.flush()
231
231
232 def _done(self):
232 def _done(self):
233 if self._chunked:
233 if self._chunked:
234 self.wfile.write('0\r\n\r\n')
234 self.wfile.write('0\r\n\r\n')
235 self.wfile.flush()
235 self.wfile.flush()
236
236
237 def version_string(self):
237 def version_string(self):
238 if self.server.serverheader:
238 if self.server.serverheader:
239 return self.server.serverheader
239 return self.server.serverheader
240 return httpservermod.basehttprequesthandler.version_string(self)
240 return httpservermod.basehttprequesthandler.version_string(self)
241
241
242 class _httprequesthandlerssl(_httprequesthandler):
242 class _httprequesthandlerssl(_httprequesthandler):
243 """HTTPS handler based on Python's ssl module"""
243 """HTTPS handler based on Python's ssl module"""
244
244
245 url_scheme = 'https'
245 url_scheme = 'https'
246
246
247 @staticmethod
247 @staticmethod
248 def preparehttpserver(httpserver, ui):
248 def preparehttpserver(httpserver, ui):
249 try:
249 try:
250 from .. import sslutil
250 from .. import sslutil
251 sslutil.modernssl
251 sslutil.modernssl
252 except ImportError:
252 except ImportError:
253 raise error.Abort(_("SSL support is unavailable"))
253 raise error.Abort(_("SSL support is unavailable"))
254
254
255 certfile = ui.config('web', 'certificate')
255 certfile = ui.config('web', 'certificate')
256
256
257 # These config options are currently only meant for testing. Use
257 # These config options are currently only meant for testing. Use
258 # at your own risk.
258 # at your own risk.
259 cafile = ui.config('devel', 'servercafile')
259 cafile = ui.config('devel', 'servercafile')
260 reqcert = ui.configbool('devel', 'serverrequirecert')
260 reqcert = ui.configbool('devel', 'serverrequirecert')
261
261
262 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
262 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
263 ui,
263 ui,
264 certfile=certfile,
264 certfile=certfile,
265 cafile=cafile,
265 cafile=cafile,
266 requireclientcert=reqcert)
266 requireclientcert=reqcert)
267
267
268 def setup(self):
268 def setup(self):
269 self.connection = self.request
269 self.connection = self.request
270 self.rfile = self.request.makefile(r"rb", self.rbufsize)
270 self.rfile = self.request.makefile(r"rb", self.rbufsize)
271 self.wfile = self.request.makefile(r"wb", self.wbufsize)
271 self.wfile = self.request.makefile(r"wb", self.wbufsize)
272
272
273 try:
273 try:
274 import threading
274 import threading
275 threading.activeCount() # silence pyflakes and bypass demandimport
275 threading.activeCount() # silence pyflakes and bypass demandimport
276 _mixin = socketserver.ThreadingMixIn
276 _mixin = socketserver.ThreadingMixIn
277 except ImportError:
277 except ImportError:
278 if util.safehasattr(os, "fork"):
278 if util.safehasattr(os, "fork"):
279 _mixin = socketserver.ForkingMixIn
279 _mixin = socketserver.ForkingMixIn
280 else:
280 else:
281 class _mixin(object):
281 class _mixin(object):
282 pass
282 pass
283
283
284 def openlog(opt, default):
284 def openlog(opt, default):
285 if opt and opt != '-':
285 if opt and opt != '-':
286 return open(opt, 'ab')
286 return open(opt, 'ab')
287 return default
287 return default
288
288
289 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
289 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
290
290
291 # SO_REUSEADDR has broken semantics on windows
291 # SO_REUSEADDR has broken semantics on windows
292 if pycompat.iswindows:
292 if pycompat.iswindows:
293 allow_reuse_address = 0
293 allow_reuse_address = 0
294
294
295 def __init__(self, ui, app, addr, handler, **kwargs):
295 def __init__(self, ui, app, addr, handler, **kwargs):
296 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
296 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
297 self.daemon_threads = True
297 self.daemon_threads = True
298 self.application = app
298 self.application = app
299
299
300 handler.preparehttpserver(self, ui)
300 handler.preparehttpserver(self, ui)
301
301
302 prefix = ui.config('web', 'prefix')
302 prefix = ui.config('web', 'prefix')
303 if prefix:
303 if prefix:
304 prefix = '/' + prefix.strip('/')
304 prefix = '/' + prefix.strip('/')
305 self.prefix = prefix
305 self.prefix = prefix
306
306
307 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
307 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
308 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
308 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
309 self.accesslog = alog
309 self.accesslog = alog
310 self.errorlog = elog
310 self.errorlog = elog
311
311
312 self.addr, self.port = self.socket.getsockname()[0:2]
312 self.addr, self.port = self.socket.getsockname()[0:2]
313 self.fqaddr = socket.getfqdn(addr[0])
313 self.fqaddr = socket.getfqdn(addr[0])
314
314
315 self.serverheader = ui.config('web', 'server-header')
315 self.serverheader = ui.config('web', 'server-header')
316
316
317 class IPv6HTTPServer(MercurialHTTPServer):
317 class IPv6HTTPServer(MercurialHTTPServer):
318 address_family = getattr(socket, 'AF_INET6', None)
318 address_family = getattr(socket, 'AF_INET6', None)
319 def __init__(self, *args, **kwargs):
319 def __init__(self, *args, **kwargs):
320 if self.address_family is None:
320 if self.address_family is None:
321 raise error.RepoError(_('IPv6 is not available on this system'))
321 raise error.RepoError(_('IPv6 is not available on this system'))
322 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
322 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
323
323
324 def create_server(ui, app):
324 def create_server(ui, app):
325
325
326 if ui.config('web', 'certificate'):
326 if ui.config('web', 'certificate'):
327 handler = _httprequesthandlerssl
327 handler = _httprequesthandlerssl
328 else:
328 else:
329 handler = _httprequesthandler
329 handler = _httprequesthandler
330
330
331 if ui.configbool('web', 'ipv6'):
331 if ui.configbool('web', 'ipv6'):
332 cls = IPv6HTTPServer
332 cls = IPv6HTTPServer
333 else:
333 else:
334 cls = MercurialHTTPServer
334 cls = MercurialHTTPServer
335
335
336 # ugly hack due to python issue5853 (for threaded use)
336 # ugly hack due to python issue5853 (for threaded use)
337 try:
337 try:
338 import mimetypes
338 import mimetypes
339 mimetypes.init()
339 mimetypes.init()
340 except UnicodeDecodeError:
340 except UnicodeDecodeError:
341 # Python 2.x's mimetypes module attempts to decode strings
341 # Python 2.x's mimetypes module attempts to decode strings
342 # from Windows' ANSI APIs as ascii (fail), then re-encode them
342 # from Windows' ANSI APIs as ascii (fail), then re-encode them
343 # as ascii (clown fail), because the default Python Unicode
343 # as ascii (clown fail), because the default Python Unicode
344 # codec is hardcoded as ascii.
344 # codec is hardcoded as ascii.
345
345
346 sys.argv # unwrap demand-loader so that reload() works
346 sys.argv # unwrap demand-loader so that reload() works
347 reload(sys) # resurrect sys.setdefaultencoding()
347 reload(sys) # resurrect sys.setdefaultencoding()
348 oldenc = sys.getdefaultencoding()
348 oldenc = sys.getdefaultencoding()
349 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
349 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
350 mimetypes.init()
350 mimetypes.init()
351 sys.setdefaultencoding(oldenc)
351 sys.setdefaultencoding(oldenc)
352
352
353 address = ui.config('web', 'address')
353 address = ui.config('web', 'address')
354 port = util.getport(ui.config('web', 'port'))
354 port = util.getport(ui.config('web', 'port'))
355 try:
355 try:
356 return cls(ui, app, (address, port), handler)
356 return cls(ui, app, (address, port), handler)
357 except socket.error as inst:
357 except socket.error as inst:
358 raise error.Abort(_("cannot start server at '%s:%d': %s")
358 raise error.Abort(_("cannot start server at '%s:%d': %s")
359 % (address, port, encoding.strtolocal(inst.args[1])))
359 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now