diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7faa742 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.md +include setup.py +include SimpleWebSocketServer/SimpleExampleServer.py +include SimpleWebSocketServer/SimpleHTTPSServer.py +include SimpleWebSocketServer/SimpleWebSocketServer.py +include SimpleWebSocketServer/__init__.py diff --git a/README.md b/README.md index 18675b3..7d699f0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ You can install SimpleWebSocketServer by running the following command... -`sudo pip install git+https://github.com/dpallot/simple-websocket-server.git` +`pip install SimpleWebSocketServer` Or by downloading the repository and running `sudo python setup.py install`. Installation via pip is suggested. @@ -25,10 +25,10 @@ class SimpleEcho(WebSocket): self.sendMessage(self.data) def handleConnected(self): - print self.address, 'connected' + print(self.address, 'connected') def handleClose(self): - print self.address, 'closed' + print(self.address, 'closed') server = SimpleWebSocketServer('', 8000, SimpleEcho) server.serveforever() @@ -49,14 +49,14 @@ class SimpleChat(WebSocket): client.sendMessage(self.address[0] + u' - ' + self.data) def handleConnected(self): - print self.address, 'connected' + print(self.address, 'connected') for client in clients: client.sendMessage(self.address[0] + u' - connected') clients.append(self) def handleClose(self): clients.remove(self) - print self.address, 'closed' + print(self.address, 'closed') for client in clients: client.sendMessage(self.address[0] + u' - disconnected') @@ -82,9 +82,9 @@ Chat Server (open up multiple *websocket.html* files) 1) Generate a certificate with key - openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem + openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout key.pem -2) Run the secure TSL/SSL server (in this case the cert.pem file is in the same directory) +2) Run the secure TLS/SSL server (in this case the cert.pem file is in the same directory) python SimpleExampleServer.py --example chat --ssl 1 --cert ./cert.pem @@ -106,8 +106,10 @@ handleConnected: called when handshake is complete - self.address: TCP address port tuple of the endpoint handleClose: called when the endpoint is closed or there is an error + - self.address: TCP address port tuple of the endpoint handleMessage: gets called when there is an incoming message from the client endpoint + - self.address: TCP address port tuple of the endpoint - self.opcode: the WebSocket frame type (STREAM, TEXT, BINARY) - self.data: bytearray (BINARY frame) or unicode string payload (TEXT frame) - self.request: HTTP details from the WebSocket handshake (refer to BaseHTTPRequestHandler) diff --git a/SimpleWebSocketServer/SimpleExampleServer.py b/SimpleWebSocketServer/SimpleExampleServer.py index 8031d3c..d025679 100644 --- a/SimpleWebSocketServer/SimpleExampleServer.py +++ b/SimpleWebSocketServer/SimpleExampleServer.py @@ -49,6 +49,7 @@ def handleClose(self): parser.add_option("--example", default='echo', type='string', action="store", dest="example", help="echo, chat") parser.add_option("--ssl", default=0, type='int', action="store", dest="ssl", help="ssl (1: on, 0: off (default))") parser.add_option("--cert", default='./cert.pem', type='string', action="store", dest="cert", help="cert (./cert.pem)") + parser.add_option("--key", default='./key.pem', type='string', action="store", dest="key", help="key (./key.pem)") parser.add_option("--ver", default=ssl.PROTOCOL_TLSv1, type=int, action="store", dest="ver", help="ssl version") (options, args) = parser.parse_args() @@ -58,7 +59,7 @@ def handleClose(self): cls = SimpleChat if options.ssl == 1: - server = SimpleSSLWebSocketServer(options.host, options.port, cls, options.cert, options.cert, version=options.ver) + server = SimpleSSLWebSocketServer(options.host, options.port, cls, options.cert, options.key, version=options.ver) else: server = SimpleWebSocketServer(options.host, options.port, cls) diff --git a/SimpleWebSocketServer/SimpleHTTPSServer.py b/SimpleWebSocketServer/SimpleHTTPSServer.py index 356ab6b..bc3ac20 100644 --- a/SimpleWebSocketServer/SimpleHTTPSServer.py +++ b/SimpleWebSocketServer/SimpleHTTPSServer.py @@ -3,10 +3,21 @@ Copyright (c) 2013 Dave P. ''' -import BaseHTTPServer, SimpleHTTPServer import ssl -# openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem -httpd = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler) -httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True, certfile='./cert.pem', keyfile='./cert.pem', ssl_version=ssl.PROTOCOL_TLSv1) -httpd.serve_forever() +try: + from BaseHTTPServer import HTTPServer +except: + from http.server import HTTPServer + +try: + from SimpleHTTPServer import SimpleHTTPRequestHandler +except: + from http.server import SimpleHTTPRequestHandler + +if __name__ == "__main__": + # openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout key.pem + httpd = HTTPServer(('', 443), SimpleHTTPRequestHandler) + httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True, certfile='./cert.pem', + keyfile='./key.pem', ssl_version=ssl.PROTOCOL_TLSv1) + httpd.serve_forever() diff --git a/SimpleWebSocketServer/SimpleWebSocketServer.py b/SimpleWebSocketServer/SimpleWebSocketServer.py index c505e60..ccb9949 100644 --- a/SimpleWebSocketServer/SimpleWebSocketServer.py +++ b/SimpleWebSocketServer/SimpleWebSocketServer.py @@ -31,7 +31,7 @@ def _check_unicode(val): if VER >= 3: return isinstance(val, str) else: - return isinstance(val, unicode) + return isinstance(val, basestring) class HTTPRequest(BaseHTTPRequestHandler): def __init__(self, request_text): @@ -53,6 +53,15 @@ def __init__(self, request_text): "Sec-WebSocket-Accept: %(acceptstr)s\r\n\r\n" ) +FAILED_HANDSHAKE_STR = ( + "HTTP/1.1 426 Upgrade Required\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Content-Type: text/plain\r\n\r\n" + "This service requires use of the WebSocket protocol\r\n" +) + GUID_STR = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' STREAM = 0x0 @@ -242,7 +251,11 @@ def _handleData(self): # do the HTTP header and handshake if self.handshaked is False: - data = self.client.recv(self.headertoread) + try: + data = self.client.recv(self.headertoread) + except (ssl.SSLWantReadError, ssl.SSLWantWriteError): + # SSL socket not ready to read yet, wait and try again + return if not data: raise Exception('remote socket closed') @@ -267,11 +280,18 @@ def _handleData(self): self.handshaked = True self.handleConnected() except Exception as e: + hStr = FAILED_HANDSHAKE_STR + self._sendBuffer(hStr.encode('ascii'), True) + self.client.close() raise Exception('handshake failed: %s', str(e)) # else do normal data else: - data = self.client.recv(8192) + try: + data = self.client.recv(16384) + except (ssl.SSLWantReadError, ssl.SSLWantWriteError): + # SSL socket not ready to read yet, wait and try again + return if not data: raise Exception("remote socket closed") @@ -305,7 +325,7 @@ def close(self, status = 1000, reason = u''): self.closed = True - def _sendBuffer(self, buff): + def _sendBuffer(self, buff, send_all = False): size = len(buff) tosend = size already_sent = 0 @@ -320,9 +340,17 @@ def _sendBuffer(self, buff): already_sent += sent tosend -= sent + except (ssl.SSLWantReadError, ssl.SSLWantWriteError): + # SSL socket not ready to send yet, wait and try again + if send_all: + continue + return buff[already_sent:] + except socket.error as e: # if we have full buffers then wait for them to drain and try again if e.errno in [errno.EAGAIN, errno.EWOULDBLOCK]: + if send_all: + continue return buff[already_sent:] else: raise e @@ -452,7 +480,7 @@ def _parseMessage(self, byte): try: self._handlePacket() finally: - self.state = self.HEADERB1 + self.state = HEADERB1 self.data = bytearray() # we have no mask and some payload @@ -573,9 +601,25 @@ def _parseMessage(self, byte): class SimpleWebSocketServer(object): def __init__(self, host, port, websocketclass, selectInterval = 0.1): self.websocketclass = websocketclass - self.serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if (host == ''): + host = None + + if host is None: + fam = socket.AF_INET6 + else: + fam = 0 + + hostInfo = socket.getaddrinfo(host, port, fam, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_PASSIVE) + self.serversocket = socket.socket(hostInfo[0][0], hostInfo[0][1], hostInfo[0][2]) self.serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.serversocket.bind((host, port)) + + if host is None: + # bind on both IPv4 and IPv6 localhost + # if we don't explicitly set this, the behaviour isn't guranteed on some platforms. e.g. Windows + self.serversocket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + + self.serversocket.bind(hostInfo[0][4]) self.serversocket.listen(5) self.selectInterval = selectInterval self.connections = {} @@ -592,91 +636,99 @@ def close(self): for desc, conn in self.connections.items(): conn.close() - conn.handleClose() + self._handleClose(conn) + def _handleClose(self, client): + client.client.close() + # only call handleClose when we have a successful websocket connection + if client.handshaked: + try: + client.handleClose() + except: + pass - def serveforever(self): - while True: - writers = [] - for fileno in self.listeners: - if fileno == self.serversocket: - continue - client = self.connections[fileno] - if client.sendq: - writers.append(fileno) + def serveonce(self): + writers = [] + for fileno in self.listeners: + if fileno == self.serversocket: + continue + client = self.connections[fileno] + if client.sendq: + writers.append(fileno) - if self.selectInterval: - rList, wList, xList = select(self.listeners, writers, self.listeners, self.selectInterval) - else: - rList, wList, xList = select(self.listeners, writers, self.listeners) + rList, wList, xList = select(self.listeners, writers, self.listeners, self.selectInterval) + + for ready in wList: + client = self.connections[ready] + try: + while client.sendq: + opcode, payload = client.sendq.popleft() + remaining = client._sendBuffer(payload) + if remaining is not None: + client.sendq.appendleft((opcode, remaining)) + break + else: + if opcode == CLOSE: + raise Exception('received client close') - for ready in wList: + except Exception as n: + self._handleClose(client) + del self.connections[ready] + self.listeners.remove(ready) + + for ready in rList: + if ready == self.serversocket: + sock = None + try: + sock, address = self.serversocket.accept() + newsock = self._decorateSocket(sock) + newsock.setblocking(0) + fileno = newsock.fileno() + self.connections[fileno] = self._constructWebSocket(newsock, address) + self.listeners.append(fileno) + except Exception as n: + if sock is not None: + sock.close() + else: + if ready not in self.connections: + continue client = self.connections[ready] try: - while client.sendq: - opcode, payload = client.sendq.popleft() - remaining = client._sendBuffer(payload) - if remaining is not None: - client.sendq.appendleft((opcode, remaining)) - break - else: - if opcode == CLOSE: - raise Exception('received client close') - + client._handleData() except Exception as n: - client.client.close() - client.handleClose() + self._handleClose(client) del self.connections[ready] self.listeners.remove(ready) - for ready in rList: - if ready == self.serversocket: - try: - sock, address = self.serversocket.accept() - newsock = self._decorateSocket(sock) - newsock.setblocking(0) - fileno = newsock.fileno() - self.connections[fileno] = self._constructWebSocket(newsock, address) - self.listeners.append(fileno) - except Exception as n: - if sock is not None: - sock.close() - else: - if ready not in self.connections: - continue - client = self.connections[ready] - try: - client._handleData() - except Exception as n: - client.client.close() - client.handleClose() - del self.connections[ready] - self.listeners.remove(ready) - - for failed in xList: - if failed == self.serversocket: - self.close() - raise Exception('server socket failed') - else: - if failed not in self.connections: - continue - client = self.connections[failed] - client.client.close() - client.handleClose() - del self.connections[failed] - self.listeners.remove(failed) + for failed in xList: + if failed == self.serversocket: + self.close() + raise Exception('server socket failed') + else: + if failed not in self.connections: + continue + client = self.connections[failed] + self._handleClose(client) + del self.connections[failed] + self.listeners.remove(failed) + def serveforever(self): + while True: + self.serveonce() class SimpleSSLWebSocketServer(SimpleWebSocketServer): - def __init__(self, host, port, websocketclass, certfile, - keyfile, version = ssl.PROTOCOL_TLSv1, selectInterval = 0.1): + def __init__(self, host, port, websocketclass, certfile = None, + keyfile = None, version = ssl.PROTOCOL_TLSv1_2, selectInterval = 0.1, ssl_context = None): SimpleWebSocketServer.__init__(self, host, port, websocketclass, selectInterval) - self.context = ssl.SSLContext(version) - self.context.load_cert_chain(certfile, keyfile) + if ssl_context is None: + self.context = ssl.SSLContext(version) + self.context.load_cert_chain(certfile, keyfile) + else: + self.context = ssl_context def close(self): super(SimpleSSLWebSocketServer, self).close() diff --git a/SimpleWebSocketServer/__init__.py b/SimpleWebSocketServer/__init__.py index 678693c..c7e52b8 100644 --- a/SimpleWebSocketServer/__init__.py +++ b/SimpleWebSocketServer/__init__.py @@ -1 +1,3 @@ from .SimpleWebSocketServer import * + +name="SimpleWebSocketServer" diff --git a/setup.py b/setup.py index 833b7b3..1131790 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,19 @@ -from distutils.core import setup - -setup( - name='SimpleWebSocketServer', - version='0.1.0', - author='Dave', - packages=['SimpleWebSocketServer'], - url='https://github.com/dpallot/simple-websocket-server/', - description='A Simple Websocket Server written in Python', - long_description=open('README.md').read() -) +from distutils.core import setup + +setup( + name='SimpleWebSocketServer', + version='0.1.1', + author='Dave Pallot', + author_email='d.e.pallot@gmail.com', + packages=['SimpleWebSocketServer'], + url='https://github.com/dpallot/simple-websocket-server/', + description='A Simple Websocket Server written in Python', + long_description_content_type='text/markdown', + long_description=open('README.md').read(), + classifiers=[ + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], +)