From b56cfb372d73fb1d46bb4939a8427445ffff0d92 Mon Sep 17 00:00:00 2001 From: Charles Winebrinner Date: Sat, 17 Nov 2012 13:42:46 -0600 Subject: [PATCH 1/4] PEP8'd rocket.py.footer (to be removed from rocket.py). --- gluon/rocket.py.footer | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/gluon/rocket.py.footer b/gluon/rocket.py.footer index 3977d3dd..d6be8ef3 100644 --- a/gluon/rocket.py.footer +++ b/gluon/rocket.py.footer @@ -1,21 +1,22 @@ +# The following code is not part of Rocket but was added to +# web2py for testing purposes. -# -# the following code is not part of Rocket but was added in web2py for testing purposes -# def demo_app(environ, start_response): global static_folder import os - types = {'htm': 'text/html','html': 'text/html','gif': 'image/gif', - 'jpg': 'image/jpeg','png': 'image/png','pdf': 'applications/pdf'} + types = {'htm': 'text/html', 'html': 'text/html', + 'gif': 'image/gif', 'jpg': 'image/jpeg', + 'png': 'image/png', 'pdf': 'applications/pdf'} if static_folder: if not static_folder.startswith('/'): - static_folder = os.path.join(os.getcwd(),static_folder) - path = os.path.join(static_folder, environ['PATH_INFO'][1:] or 'index.html') - type = types.get(path.split('.')[-1],'text') + static_folder = os.path.join(os.getcwd(), static_folder) + path = os.path.join( + static_folder, environ['PATH_INFO'][1:] or 'index.html') + type = types.get(path.split('.')[-1], 'text') if os.path.exists(path): try: - data = open(path,'rb').read() + data = open(path, 'rb').read() start_response('200 OK', [('Content-Type', type)]) except IOError: start_response('404 NOT FOUND', []) @@ -25,24 +26,26 @@ def demo_app(environ, start_response): data = '500 INTERNAL SERVER ERROR' else: start_response('200 OK', [('Content-Type', 'text/html')]) - data = '

Hello from Rocket Web Server

' + data = '

Hello from Rocket

' return [data] + def demo(): from optparse import OptionParser parser = OptionParser() - parser.add_option("-i", "--ip", dest="ip",default="127.0.0.1", + parser.add_option("-i", "--ip", dest="ip", default="127.0.0.1", help="ip address of the network interface") - parser.add_option("-p", "--port", dest="port",default="8000", + parser.add_option("-p", "--port", dest="port", default="8000", help="post where to run web server") - parser.add_option("-s", "--static", dest="static",default=None, + parser.add_option("-s", "--static", dest="static", default=None, help="folder containing static files") (options, args) = parser.parse_args() global static_folder static_folder = options.static print 'Rocket running on %s:%s' % (options.ip, options.port) - r=Rocket((options.ip,int(options.port)),'wsgi', {'wsgi_app':demo_app}) + r = Rocket((options.ip, int(options.port)), 'wsgi', {'wsgi_app': demo_app}) r.start() -if __name__=='__main__': + +if __name__ == '__main__': demo() From 22ca52a1cbd7c216271caacc19c7e4b346a110a8 Mon Sep 17 00:00:00 2001 From: Charles Winebrinner Date: Sat, 17 Nov 2012 13:44:21 -0600 Subject: [PATCH 2/4] Fixed a typo in utils.py. --- gluon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/utils.py b/gluon/utils.py index d2ee474d..9052141e 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -263,7 +263,7 @@ def is_valid_ip_address(address): # deal with special cases if address.lower() in ('127.0.0.1', 'localhost', '::1', '::ffff:127.0.0.1'): return True - elif address.lower() in ('unkown', ''): + elif address.lower() in ('unknown', ''): return False elif address.count('.') == 3: # assume IPv4 if address.startswith('::ffff:'): From 9f5391bbc0fa6746da5028681224ff54ce8251b2 Mon Sep 17 00:00:00 2001 From: Charles Winebrinner Date: Sat, 17 Nov 2012 13:46:08 -0600 Subject: [PATCH 3/4] Merged explorigin/Rocket and brought web2py's rocket more in line with upstream. --- gluon/rocket.py | 290 +++++++++++++++++++++--------------------------- 1 file changed, 127 insertions(+), 163 deletions(-) diff --git a/gluon/rocket.py b/gluon/rocket.py index 3f88f40b..373c46b3 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -10,10 +10,9 @@ import errno import socket import logging import platform -import traceback # Define Constants -VERSION = '1.2.5' +VERSION = '1.2.6' SERVER_NAME = socket.gethostname() SERVER_SOFTWARE = 'Rocket %s' % VERSION HTTP_SERVER_SOFTWARE = '%s Python/%s' % ( @@ -79,8 +78,8 @@ __all__ = ['VERSION', 'SERVER_SOFTWARE', 'HTTP_SERVER_SOFTWARE', 'BUF_SIZE', 'IS_JYTHON', 'IGNORE_ERRORS_ON_CLOSE', 'DEFAULTS', 'PY3K', 'b', 'u', 'Rocket', 'CherryPyWSGIServer', 'SERVER_NAME', 'NullHandler'] -# Monolithic build...end of module: rocket\__init__.py -# Monolithic build...start of module: rocket\connection.py +# Monolithic build...end of module: rocket/__init__.py +# Monolithic build...start of module: rocket/connection.py # Import System Modules import sys @@ -118,7 +117,7 @@ class Connection(object): ] def __init__(self, sock_tuple, port, secure=False): - self.client_addr, self.client_port = sock_tuple[1] + self.client_addr, self.client_port = sock_tuple[1][:2] self.server_port = port self.socket = sock_tuple[0] self.start_time = time.time() @@ -133,7 +132,6 @@ class Connection(object): self.socket.settimeout(SOCKET_TIMEOUT) - self.sendall = self.socket.sendall self.shutdown = self.socket.shutdown self.fileno = self.socket.fileno self.setblocking = self.socket.setblocking @@ -141,6 +139,26 @@ class Connection(object): self.send = self.socket.send self.makefile = self.socket.makefile + if sys.platform == 'darwin': + self.sendall = self._sendall_darwin + else: + self.sendall = self.socket.sendall + + def _sendall_darwin(self, buf): + pending = len(buf) + offset = 0 + while pending: + try: + sent = self.socket.send(buf[offset:]) + pending -= sent + offset += sent + except socket.error: + import errno + info = sys.exc_info() + if info[1].args[0] != errno.EAGAIN: + raise + return offset + # FIXME - this is not ready for prime-time yet. # def makefile(self, buf_size=BUF_SIZE): # return FileLikeSocket(self, buf_size) @@ -157,9 +175,8 @@ class Connection(object): pass self.socket.close() - -# Monolithic build...end of module: rocket\connection.py -# Monolithic build...start of module: rocket\filelike.py +# Monolithic build...end of module: rocket/connection.py +# Monolithic build...start of module: rocket/filelike.py # Import System Modules import socket @@ -282,8 +299,8 @@ class FileLikeSocket(object): self.conn = None self.content_length = None -# Monolithic build...end of module: rocket\filelike.py -# Monolithic build...start of module: rocket\futures.py +# Monolithic build...end of module: rocket/filelike.py +# Monolithic build...start of module: rocket/futures.py # Import System Modules import time @@ -396,8 +413,8 @@ class FuturesMiddleware(object): environ["wsgiorg.futures"] = self.executor.futures return self.app(environ, start_response) -# Monolithic build...end of module: rocket\futures.py -# Monolithic build...start of module: rocket\listener.py +# Monolithic build...end of module: rocket/futures.py +# Monolithic build...start of module: rocket/listener.py # Import System Modules import os @@ -442,7 +459,10 @@ class Listener(Thread): self.err_log.addHandler(NullHandler()) # Build the socket - listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if ':' in self.addr: + listener = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + else: + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if not listener: self.err_log.error("Failed to get socket.") @@ -520,12 +540,10 @@ class Listener(Thread): certfile=self.interface[3], server_side=True, ssl_version=ssl.PROTOCOL_SSLv23) - except SSLError: # Generally this happens when an HTTP request is received on a # secure socket. We don't do anything because it will be detected # by Worker and dealt with appropriately. - # self.err_log.error('SSL Error: %s' % traceback.format_exc()) pass return sock @@ -576,8 +594,9 @@ class Listener(Thread): self.secure)) except socket.timeout: - # socket.timeout will be raised every THREAD_STOP_CHECK_INTERVAL - # seconds. When that happens, we check if it's time to die. + # socket.timeout will be raised every + # THREAD_STOP_CHECK_INTERVAL seconds. When that happens, + # we check if it's time to die. if not self.ready: if __debug__: @@ -588,8 +607,8 @@ class Listener(Thread): except: self.err_log.error(traceback.format_exc()) -# Monolithic build...end of module: rocket\listener.py -# Monolithic build...start of module: rocket\main.py +# Monolithic build...end of module: rocket/listener.py +# Monolithic build...start of module: rocket/main.py # Import System Modules import sys @@ -606,7 +625,6 @@ except ImportError: # Import Package Modules # package imports removed in monolithic build - # Setup Logging log = logging.getLogger('Rocket') log.addHandler(NullHandler()) @@ -618,13 +636,13 @@ class Rocket(object): def __init__(self, interfaces=('127.0.0.1', 8000), - method = 'wsgi', - app_info = None, - min_threads = None, - max_threads = None, - queue_size = None, - timeout = 600, - handle_signals = True): + method='wsgi', + app_info=None, + min_threads=None, + max_threads=None, + queue_size=None, + timeout=600, + handle_signals=True): self.handle_signals = handle_signals self.startstop_lock = Lock() @@ -801,8 +819,8 @@ def CherryPyWSGIServer(bind_addr, queue_size=request_queue_size, timeout=timeout) -# Monolithic build...end of module: rocket\main.py -# Monolithic build...start of module: rocket\monitor.py +# Monolithic build...end of module: rocket/main.py +# Monolithic build...start of module: rocket/monitor.py # Import System Modules import time @@ -983,8 +1001,8 @@ class Monitor(Thread): # Place a None sentry value to cause the monitor to die. self.monitor_queue.put(None) -# Monolithic build...end of module: rocket\monitor.py -# Monolithic build...start of module: rocket\threadpool.py +# Monolithic build...end of module: rocket/monitor.py +# Monolithic build...start of module: rocket/threadpool.py # Import System Modules import logging @@ -1146,8 +1164,8 @@ class ThreadPool: self.grow(queueSize) -# Monolithic build...end of module: rocket\threadpool.py -# Monolithic build...start of module: rocket\worker.py +# Monolithic build...end of module: rocket/threadpool.py +# Monolithic build...start of module: rocket/worker.py # Import System Modules import re @@ -1199,7 +1217,7 @@ $ """, re.X) LOG_LINE = '%(client_ip)s - "%(request_line)s" - %(status)s %(size)s' RESPONSE = '''\ -HTTP/1.1 %s +%s %s Content-Length: %i Content-Type: %s @@ -1207,7 +1225,7 @@ Content-Type: %s ''' if IS_JYTHON: HTTP_METHODS = set(['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', - 'DELETE', 'TRACE', 'CONNECT']) + 'DELETE', 'TRACE', 'CONNECT']) class Worker(Thread): @@ -1232,6 +1250,7 @@ class Worker(Thread): self.status = "200 OK" self.closeConnection = True self.request_line = "" + self.protocol = 'HTTP/1.1' # Request Log self.req_log = logging.getLogger('Rocket.Requests') @@ -1316,26 +1335,19 @@ class Worker(Thread): self.err_log.debug('Serving a request') try: self.run_app(conn) - log_info = dict(client_ip=conn.client_addr, - time=datetime.now().strftime('%c'), - status=self.status.split(' ')[0], - size=self.size, - request_line=self.request_line) - self.req_log.info(LOG_LINE % log_info) except: exc = sys.exc_info() handled = self._handleError(*exc) if handled: break - else: - if self.request_line: - log_info = dict(client_ip=conn.client_addr, - time=datetime.now( - ).strftime('%c'), - status=self.status.split(' ')[0], - size=self.size, - request_line=self.request_line + ' - not stopping') - self.req_log.info(LOG_LINE % log_info) + finally: + if self.request_line: + log_info = dict(client_ip=conn.client_addr, + time=datetime.now().strftime('%c'), + status=self.status.split(' ')[0], + size=self.size, + request_line=self.request_line) + self.req_log.info(LOG_LINE % log_info) if self.closeConnection: try: @@ -1353,7 +1365,8 @@ class Worker(Thread): def send_response(self, status): stat_msg = status.split(' ', 1)[1] - msg = RESPONSE % (status, + msg = RESPONSE % (self.protocol, + status, len(stat_msg), 'text/plain', stat_msg) @@ -1361,23 +1374,12 @@ class Worker(Thread): self.conn.sendall(b(msg)) except socket.timeout: self.closeConnection = True - self.err_log.error( - 'Tried to send "%s" to client but received timeout error' - % status) + msg = 'Tried to send "%s" to client but received timeout error' + self.err_log.error(msg % status) except socket.error: self.closeConnection = True - self.err_log.error( - 'Tried to send "%s" to client but received socket error' - % status) - - #def kill(self): - # if self.isAlive() and hasattr(self, 'conn'): - # try: - # self.conn.shutdown(socket.SHUT_RDWR) - # except socket.error: - # info = sys.exc_info() - # if info[1].args[0] != socket.EBADF: - # self.err_log.debug('Error on shutdown: '+str(info)) + msg = 'Tried to send "%s" to client but received socket error' + self.err_log.error(msg % status) def read_request_line(self, sock_file): self.request_line = '' @@ -1396,10 +1398,11 @@ class Worker(Thread): if PY3K: d = d.decode('ISO-8859-1') except socket.timeout: - raise SocketTimeout("Socket timed out before request.") + raise SocketTimeout('Socket timed out before request.') except TypeError: raise SocketClosed( - "ssl bug caused closer of socket, upgrade to python 2.7") + 'SSL bug caused closure of socket. See ' + '"https://groups.google.com/d/topic/web2py/P_Gw0JxWzCs".') d = d.strip() @@ -1433,6 +1436,7 @@ class Worker(Thread): req['path'] = r'%2F'.join( [unquote(x) for x in re_SLASH.split(v)]) + self.protocol = req['protocol'] return req def _read_request_line_jython(self, d): @@ -1440,7 +1444,7 @@ class Worker(Thread): try: method, uri, proto = d.split(' ') if not proto.startswith('HTTP') or \ - proto[-3:] not in ('1.0', '1.1') or \ + proto[-3:] not in ('1.0', '1.1') or \ method not in HTTP_METHODS: self.send_response('400 Bad Request') raise BadRequest @@ -1473,36 +1477,42 @@ class Worker(Thread): host=host) return req - def read_headers(self, sock_file, environ): + def read_headers(self, sock_file): try: + headers = dict() lname = None + lval = None while True: l = sock_file.readline() + if PY3K: try: l = str(l, 'ISO-8859-1') except UnicodeDecodeError: self.err_log.warning( - 'Invalid request header: ' + repr(l)) + 'Client sent invalid header: ' + repr(l)) if l.strip().replace('\0', '') == '': break - elif l[0] in ' \t' and lname: + + if l[0] in ' \t' and lname: # Some headers take more than one line - environ[lname] += ' ' + l.strip() + lval += ' ' + l.strip() else: # HTTP header values are latin-1 encoded l = l.split(':', 1) # HTTP header names are us-ascii encoded - lname = str( - 'HTTP_' + l[0].strip().upper().replace('-', '_')) - lval = str(l[-1].strip()) - environ[lname] = lval + lname = l[0].strip().upper().replace('-', '_') + lval = l[-1].strip() + + headers[str(lname)] = str(lval) except socket.timeout: raise SocketTimeout("Socket timed out before request.") + return headers + class SocketTimeout(Exception): "Exception for when a socket times out between requests." @@ -1520,6 +1530,7 @@ class SocketClosed(Exception): class ChunkedReader(object): + def __init__(self, sock_file): self.stream = sock_file self.chunk_size = 0 @@ -1571,8 +1582,11 @@ def get_method(method): methods = dict(wsgi=WSGIWorker) return methods[method.lower()] -# Monolithic build...end of module: rocket\worker.py -# Monolithic build...start of module: rocket\methods\wsgi.py +# Monolithic build...end of module: rocket/worker.py +# Monolithic build...start of module: rocket/methods/__init__.py + +# Monolithic build...end of module: rocket/methods/__init__.py +# Monolithic build...start of module: rocket/methods/wsgi.py # Import System Modules import sys @@ -1583,7 +1597,6 @@ from wsgiref.util import FileWrapper # Import Package Modules # package imports removed in monolithic build - if PY3K: from email.utils import formatdate else: @@ -1640,15 +1653,16 @@ class WSGIWorker(Worker): environ = self.base_environ.copy() # Grab the headers - self.read_headers(sock_file, environ) + for k, v in self.read_headers(sock_file).iteritems(): + environ[str('HTTP_' + k)] = v # Add CGI Variables - environ['SERVER_PORT'] = str(conn.server_port) - environ['REMOTE_PORT'] = str(conn.client_port) - environ['REMOTE_ADDR'] = str(conn.client_addr) environ['REQUEST_METHOD'] = request['method'] environ['PATH_INFO'] = request['path'] environ['SERVER_PROTOCOL'] = request['protocol'] + environ['SERVER_PORT'] = str(conn.server_port) + environ['REMOTE_PORT'] = str(conn.client_port) + environ['REMOTE_ADDR'] = str(conn.client_addr) environ['QUERY_STRING'] = request['query_string'] if 'HTTP_CONTENT_LENGTH' in environ: environ['CONTENT_LENGTH'] = environ['HTTP_CONTENT_LENGTH'] @@ -1662,16 +1676,14 @@ class WSGIWorker(Worker): if conn.ssl: environ['wsgi.url_scheme'] = 'https' environ['HTTPS'] = 'on' - else: - environ['wsgi.url_scheme'] = 'http' - - if conn.ssl: try: peercert = conn.socket.getpeercert(binary_form=True) environ['SSL_CLIENT_RAW_CERT'] = \ peercert and ssl.DER_cert_to_PEM_cert(peercert) except Exception: print sys.exc_info()[1] + else: + environ['wsgi.url_scheme'] = 'http' if environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked': environ['wsgi.input'] = ChunkedReader(sock_file) @@ -1684,36 +1696,36 @@ class WSGIWorker(Worker): h_set = self.header_set # Does the app want us to send output chunked? - self.chunked = h_set.get('transfer-encoding', '').lower() == 'chunked' + self.chunked = h_set.get('Transfer-Encoding', '').lower() == 'chunked' # Add a Date header if it's not there already - if not 'date' in h_set: + if not 'Date' in h_set: h_set['Date'] = formatdate(usegmt=True) # Add a Server header if it's not there already - if not 'server' in h_set: + if not 'Server' in h_set: h_set['Server'] = HTTP_SERVER_SOFTWARE - if 'content-length' in h_set: - self.size = int(h_set['content-length']) + if 'Content-Length' in h_set: + self.size = int(h_set['Content-Length']) else: s = int(self.status.split(' ')[0]) - if s < 200 or s not in (204, 205, 304): - if not self.chunked: - if sections == 1: - # Add a Content-Length header if it's not there already - h_set['Content-Length'] = str(len(data)) - self.size = len(data) - else: - # If they sent us more than one section, we blow chunks - h_set['Transfer-Encoding'] = 'Chunked' - self.chunked = True - if __debug__: - self.err_log.debug('Adding header...' - 'Transfer-Encoding: Chunked') + if (s < 200 or s not in (204, 205, 304)) and not self.chunked: + if sections == 1 or self.protocol != 'HTTP/1.1': + # Add a Content-Length header because it's not there + self.size = len(data) + h_set['Content-Length'] = str(self.size) + else: + # If they sent us more than one section, we blow chunks + h_set['Transfer-Encoding'] = 'Chunked' + self.chunked = True + if __debug__: + self.err_log.debug('Adding header...' + 'Transfer-Encoding: Chunked') - if 'connection' not in h_set: - # If the application did not provide a connection header, fill it in + if 'Connection' not in h_set: + # If the application did not provide a connection header, + # fill it in client_conn = self.environ.get('HTTP_CONNECTION', '').lower() if self.environ['SERVER_PROTOCOL'] == 'HTTP/1.1': # HTTP = 1.1 defaults to keep-alive connections @@ -1722,11 +1734,12 @@ class WSGIWorker(Worker): else: h_set['Connection'] = 'keep-alive' else: - # HTTP < 1.1 supports keep-alive but it's quirky so we don't support it + # HTTP < 1.1 supports keep-alive but it's quirky + # so we don't support it h_set['Connection'] = 'close' # Close our connection if we need to. - self.closeConnection = h_set.get('connection', '').lower() == 'close' + self.closeConnection = h_set.get('Connection', '').lower() == 'close' # Build our output headers header_data = HEADER_RESPONSE % (self.status, str(h_set)) @@ -1758,6 +1771,8 @@ class WSGIWorker(Worker): self.conn.sendall(b('%x\r\n%s\r\n' % (len(data), data))) else: self.conn.sendall(data) + except socket.timeout: + self.closeConnection = True except socket.error: # But some clients will close the connection before that # resulting in a socket error. @@ -1853,55 +1868,4 @@ class WSGIWorker(Worker): sock_file.close() -# Monolithic build...end of module: rocket\methods\wsgi.py - -# -# the following code is not part of Rocket but was added in web2py for testing purposes -# - - -def demo_app(environ, start_response): - global static_folder - import os - types = {'htm': 'text/html', 'html': 'text/html', 'gif': 'image/gif', - 'jpg': 'image/jpeg', 'png': 'image/png', 'pdf': 'applications/pdf'} - if static_folder: - if not static_folder.startswith('/'): - static_folder = os.path.join(os.getcwd(), static_folder) - path = os.path.join( - static_folder, environ['PATH_INFO'][1:] or 'index.html') - type = types.get(path.split('.')[-1], 'text') - if os.path.exists(path): - try: - data = open(path, 'rb').read() - start_response('200 OK', [('Content-Type', type)]) - except IOError: - start_response('404 NOT FOUND', []) - data = '404 NOT FOUND' - else: - start_response('500 INTERNAL SERVER ERROR', []) - data = '500 INTERNAL SERVER ERROR' - else: - start_response('200 OK', [('Content-Type', 'text/html')]) - data = '

Hello from Rocket Web Server

' - return [data] - - -def demo(): - from optparse import OptionParser - parser = OptionParser() - parser.add_option("-i", "--ip", dest="ip", default="127.0.0.1", - help="ip address of the network interface") - parser.add_option("-p", "--port", dest="port", default="8000", - help="post where to run web server") - parser.add_option("-s", "--static", dest="static", default=None, - help="folder containing static files") - (options, args) = parser.parse_args() - global static_folder - static_folder = options.static - print 'Rocket running on %s:%s' % (options.ip, options.port) - r = Rocket((options.ip, int(options.port)), 'wsgi', {'wsgi_app': demo_app}) - r.start() - -if __name__ == '__main__': - demo() +# Monolithic build...end of module: rocket/methods/wsgi.py From b388f41f8015f422426cf11186ccc4d52ba51e28 Mon Sep 17 00:00:00 2001 From: Charles Winebrinner Date: Sun, 18 Nov 2012 01:03:04 -0600 Subject: [PATCH 4/4] Added IPv6 support for Rocket, refactored some code, and fixed some bugs. --- gluon/main.py | 27 +++++---- gluon/utils.py | 13 ++++ gluon/widget.py | 153 +++++++++++++++++++++++++++++------------------- 3 files changed, 122 insertions(+), 71 deletions(-) diff --git a/gluon/main.py b/gluon/main.py index aa64fd22..bbdaac97 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -127,7 +127,7 @@ def get_client(env): guess the client address from the environment variables first tries 'http_x_forwarded_for', secondly 'remote_addr' - if all fails assume '127.0.0.1' (running locally) + if all fails, assume '127.0.0.1' or '::1' (running locally) """ g = regex_client.search(env.get('http_x_forwarded_for', '')) if g: @@ -136,8 +136,10 @@ def get_client(env): g = regex_client.search(env.get('remote_addr', '')) if g: client = g.group() + elif env.http_host.startswith('['): # IPv6 + client = '::1' else: - client = '127.0.0.1' + client = '127.0.0.1' # IPv4 if not is_valid_ip_address(client): raise HTTP(400, "Bad Request (request.client=%s)" % client) return client @@ -432,20 +434,23 @@ def wsgibase(environ, responder): app = request.application # must go after url_in! if not global_settings.local_hosts: - local_hosts = ['127.0.0.1', '::ffff:127.0.0.1'] + local_hosts = set(['127.0.0.1', '::ffff:127.0.0.1', '::1']) if not global_settings.web2py_runtime_gae: try: - local_hosts.append(socket.gethostname()) - except TypeError: - pass - try: + fqdn = socket.getfqdn() + local_hosts.add(socket.gethostname()) + local_hosts.add(fqdn) + local_hosts.update([ + ip[4][0] for ip in socket.getaddrinfo( + fqdn, 0)]) if env.server_name: - local_hosts += [ - env.server_name, - socket.gethostbyname(env.server_name)] + local_hosts.add(env.server_name) + local_hosts.update([ + ip[4][0] for ip in socket.getaddrinfo( + env.server_name, 0)]) except (socket.gaierror, TypeError): pass - global_settings.local_hosts = local_hosts + global_settings.local_hosts = list(local_hosts) else: local_hosts = global_settings.local_hosts client = get_client(env) diff --git a/gluon/utils.py b/gluon/utils.py index 9052141e..c78fa80c 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -287,3 +287,16 @@ def is_valid_ip_address(address): return False else: # do not know what to do? assume it is a valid address return True + + +def is_loopback_ip_address(ip): + """Determines whether the IP address appears to be a loopback address. + + This assumes that the IP is valid. The IPv6 check is limited to '::1'. + + """ + if not ip: + return False + if ip.count('.') == 3: # IPv4 + return ip.startswith('127') or ip.startswith('::ffff:127') + return ip == '::1' # IPv6 diff --git a/gluon/widget.py b/gluon/widget.py index dfd44cca..75e0d100 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -28,6 +28,7 @@ import main from fileutils import w2p_pack, read_file, write_file from settings import global_settings from shell import run, test +from utils import is_valid_ip_address, is_loopback_ip_address try: import Tkinter @@ -99,26 +100,34 @@ class IO(object): self.buffer.write(data) -def try_start_browser(url): - """ Try to start the default browser """ +def get_url(host, path='/', proto='http', port=80): + if ':' in host: + host = '[%s]' % host + else: + host = host.replace('0.0.0.0', '127.0.0.1') + if path.startswith('/'): + path = path[1:] + if proto.endswith(':'): + proto = proto[:-1] + if not port or port == 80: + port = '' + else: + port = ':%s' % port + return '%s://%s%s/%s' % (proto, host, port, path) + +def start_browser(url, startup=False): + if startup: + print 'please visit:' + print '\t', url + print 'starting browser...' try: import webbrowser - url = url.replace('0.0.0.0','127.0.0.1') webbrowser.open(url) except: print 'warning: unable to detect your browser' -def start_browser(proto, ip, port): - """ Starts the default browser """ - print 'please visit:' - url = '%s://%s:%s' % (proto, ip, port) - print '\t', url - print 'starting browser...' - try_start_browser(url) - - def presentation(root): """ Draw the splash screen """ @@ -186,7 +195,7 @@ class web2pyDialog(object): httplog = os.path.join(self.options.folder, 'httpserver.log') # Building the Menu - item = lambda: try_start_browser(httplog) + item = lambda: start_browser(httplog) servermenu.add_command(label='View httpserver.log', command=item) @@ -207,7 +216,7 @@ class web2pyDialog(object): helpmenu = Tkinter.Menu(self.menu, tearoff=0) # Home Page - item = lambda: try_start_browser('http://www.web2py.com') + item = lambda: start_browser('http://www.web2py.com/') helpmenu.add_command(label='Home Page', command=item) @@ -237,7 +246,8 @@ class web2pyDialog(object): self.ips = {} self.selected_ip = Tkinter.StringVar() row = 0 - ips = [('127.0.0.1', 'Local')] + \ + ips = [('127.0.0.1', 'Local (IPv4)')] + \ + [('::1', 'Local (IPv6)')] if socket.has_ipv6 else [] + \ [(ip, 'Public') for ip in options.ips] + \ [('0.0.0.0', 'Public')] for ip, legend in ips: @@ -406,10 +416,9 @@ class web2pyDialog(object): if os.path.exists('applications/%s/__init__.py' % arq)] self.pagesmenu.delete(0, len(available_apps)) for arq in available_apps: - url = self.url + '/' + arq - start_browser = lambda u = url: try_start_browser(u) + url = self.url + arq self.pagesmenu.add_command(label=url, - command=start_browser) + command=lambda u=url: start_browser(u)) def quit(self, justHide=False): """ Finish the program execution """ @@ -449,8 +458,7 @@ class web2pyDialog(object): ip = self.selected_ip.get() - regexp = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' - if ip and not re.compile(regexp).match(ip): + if not is_valid_ip_address(ip): return self.error('invalid host ip address') try: @@ -464,7 +472,7 @@ class web2pyDialog(object): else: proto = 'http' - self.url = '%s://%s:%s' % (proto, ip, port) + self.url = get_url(ip, proto=proto, port=port) self.connect_pages() self.button_start.configure(state='disabled') @@ -501,7 +509,9 @@ class web2pyDialog(object): self.button_stop.configure(state='normal') if not options.taskbar: - thread.start_new_thread(start_browser, (proto, ip, port)) + thread.start_new_thread(start_browser, + (get_url(ip, proto=proto, port=port),), + dict(startup=True)) self.password.configure(state='readonly') [ip.configure(state='disabled') for ip in self.ips.values()] @@ -584,11 +594,13 @@ def console(): parser.description = description + msg = ('IP address of the server (e.g., 127.0.0.1 or ::1); ' + 'Note: This value is ignored when using the \'interfaces\' option.') parser.add_option('-i', '--ip', default='127.0.0.1', dest='ip', - help='ip address of the server (127.0.0.1)') + help=msg) parser.add_option('-p', '--port', @@ -597,8 +609,8 @@ def console(): type='int', help='port of server (8000)') - msg = 'password to be used for administration' - msg += ' (use -a "" to reuse the last password))' + msg = ('password to be used for administration ' + '(use -a "" to reuse the last password))') parser.add_option('-a', '--password', default='', @@ -617,11 +629,13 @@ def console(): dest='ssl_private_key', help='file that contains ssl private key') + msg = ('Use this file containing the CA certificate to validate X509 ' + 'certificates from clients') parser.add_option('--ca-cert', action='store', dest='ssl_ca_certificate', default=None, - help='Use this file containing the CA certificate to validate X509 certificates from clients') + help=msg) parser.add_option('-d', '--pid_filename', @@ -708,8 +722,8 @@ def console(): default=False, help='disable all output') - msg = 'set debug output level (0-100, 0 means all, 100 means none;' - msg += ' default is 30)' + msg = ('set debug output level (0-100, 0 means all, 100 means none; ' + 'default is 30)') parser.add_option('-D', '--debug', dest='debuglevel', @@ -717,18 +731,18 @@ def console(): type='int', help=msg) - msg = 'run web2py in interactive shell or IPython (if installed) with' - msg += ' specified appname (if app does not exist it will be created).' - msg += ' APPNAME like a/c/f (c,f optional)' + msg = ('run web2py in interactive shell or IPython (if installed) with ' + 'specified appname (if app does not exist it will be created). ' + 'APPNAME like a/c/f (c,f optional)') parser.add_option('-S', '--shell', dest='shell', metavar='APPNAME', help=msg) - msg = 'run web2py in interactive shell or bpython (if installed) with' - msg += ' specified appname (if app does not exist it will be created).' - msg += '\n Use combined with --shell' + msg = ('run web2py in interactive shell or bpython (if installed) with ' + 'specified appname (if app does not exist it will be created).\n' + 'Use combined with --shell') parser.add_option('-B', '--bpython', action='store_true', @@ -744,8 +758,8 @@ def console(): dest='plain', help=msg) - msg = 'auto import model files; default is False; should be used' - msg += ' with --shell option' + msg = ('auto import model files; default is False; should be used ' + 'with --shell option') parser.add_option('-M', '--import_models', action='store_true', @@ -753,8 +767,8 @@ def console(): dest='import_models', help=msg) - msg = 'run PYTHON_FILE in web2py environment;' - msg += ' should be used with --shell option' + msg = ('run PYTHON_FILE in web2py environment; ' + 'should be used with --shell option') parser.add_option('-R', '--run', dest='run', @@ -762,11 +776,11 @@ def console(): default='', help=msg) - msg = 'run scheduled tasks for the specified apps: expects a list of ' - msg += 'app names as -K app1,app2,app3 ' - msg += 'or a list of app:groups as -K app1:group1:group2,app2:group1 ' - msg += 'to override specific group_names. (only strings, no spaces ' - msg += 'allowed. Requires a scheduler defined in the models' + msg = ('run scheduled tasks for the specified apps: expects a list of ' + 'app names as -K app1,app2,app3 ' + 'or a list of app:groups as -K app1:group1:group2,app2:group1 ' + 'to override specific group_names. (only strings, no spaces ' + 'allowed. Requires a scheduler defined in the models') parser.add_option('-K', '--scheduler', dest='scheduler', @@ -781,8 +795,8 @@ def console(): dest='with_scheduler', help=msg) - msg = 'run doctests in web2py environment; ' +\ - 'TEST_PATH like a/c/f (c,f optional)' + msg = ('run doctests in web2py environment; ' + 'TEST_PATH like a/c/f (c,f optional)') parser.add_option('-T', '--test', dest='test', @@ -851,12 +865,14 @@ def console(): dest='nogui', help='text-only, no GUI') + msg = ('should be followed by a list of arguments to be passed to script, ' + 'to be used with -S, -A must be the last option') parser.add_option('-A', '--args', action='store', dest='args', default=None, - help='should be followed by a list of arguments to be passed to script, to be used with -S, -A must be the last option') + help=msg) parser.add_option('--no-banner', action='store_true', @@ -864,7 +880,10 @@ def console(): dest='nobanner', help='Do not print header banner') - msg = 'listen on multiple addresses: "ip:port:cert:key:ca_cert;ip2:port2:cert2:key2:ca_cert2;..." (:cert:key optional; no spaces)' + msg = ('listen on multiple addresses: ' + '"ip1:port1:key1:cert1:ca_cert1;ip2:port2:key2:cert2:ca_cert2;..." ' + '(:key:cert:ca_cert optional; no spaces; IPv6 addresses must be in ' + 'square [] brackets)') parser.add_option('--interfaces', action='store', dest='interfaces', @@ -891,9 +910,9 @@ def console(): global_settings.cmd_args = args try: - options.ips = [ - ip for ip in socket.gethostbyname_ex(socket.getfqdn())[2] - if ip != '127.0.0.1'] + options.ips = list(set([ + ip[4][0] for ip in socket.getaddrinfo(socket.getfqdn(), 0) + if not is_loopback_ip_address(ip[4][0])])) except socket.gaierror: options.ips = [] @@ -920,15 +939,22 @@ def console(): options.folder = os.path.abspath(options.folder) # accept --interfaces in the form - # "ip:port:cert:key;ip2:port2;ip3:port3:cert3:key3" - # (no spaces; optional cert:key indicate SSL) + # "ip1:port1:key1:cert1:ca_cert1;[ip2]:port2;ip3:port3:key3:cert3" + # (no spaces; optional key:cert indicate SSL) if isinstance(options.interfaces, str): - options.interfaces = [ - interface.split(':') for interface in options.interfaces.split(';')] - for interface in options.interfaces: - interface[1] = int(interface[1]) # numeric port - options.interfaces = [ - tuple(interface) for interface in options.interfaces] + interfaces = options.interfaces.split(';') + options.interfaces = [] + for interface in interfaces: + if interface.startswith('['): # IPv6 + ip, if_remainder = interface.split(']', 1) + ip = ip[1:] + if_remainder = if_remainder[1:].split(':') + if_remainder[0] = int(if_remainder[0]) # numeric port + options.interfaces.append(tuple([ip] + if_remainder)) + else: # IPv4 + interface = interface.split(':') + interface[1] = int(interface[1]) # numeric port + options.interfaces.append(tuple(interface)) # accepts --scheduler in the form # "app:group1,group2,app2:group1" @@ -1189,14 +1215,21 @@ end tell # ## start server - (ip, port) = (options.ip, int(options.port)) + # Use first interface IP and port if interfaces specified, since the + # interfaces option overrides the IP (and related) options. + if not options.interfaces: + (ip, port) = (options.ip, int(options.port)) + else: + first_if = options.interfaces[0] + (ip, port) = first_if[0], first_if[1] # Check for non default value for ssl inputs if (len(options.ssl_certificate) > 0) or (len(options.ssl_private_key) > 0): proto = 'https' else: proto = 'http' - url = '%s://%s:%s' % (proto, ip, port) + + url = get_url(ip, proto=proto, port=port) if not options.nobanner: print 'please visit:'