2012-10-14 13 views
7

Der Versuch, ein Web-Frontend für eine Python3 gesicherte Anwendung zu erstellen. Die Anwendung erfordert eine bidirektionale Streaming, die wie eine gute Gelegenheit, klang in WebSockets zu suchen.Websocket Implementierung in Python 3

Meine erste Neigung war es, etwas bereits vorhandenes zu verwenden, und die Beispielanwendungen von mod-pywebsocket haben sich als wertvoll erwiesen. Leider ihre API scheint nicht leicht, sich zu Erweiterung zu verleihen, und es ist Python2.

rund um die Blogosphäre Blick viele Menschen ihre eigenen websocket Server für frühere Versionen des WebSocket-Protokoll geschrieben, die meisten nicht implementieren die Sicherheitsschlüssel-Hash so dont‘Arbeit.

Lese RFC 6455 entschied ich mich an ihn einen Stich zu nehmen und kam mit dem folgenden:

#!/usr/bin/env python3 

""" 
A partial implementation of RFC 6455 
http://tools.ietf.org/pdf/rfc6455.pdf 
Brian Thorne 2012 
""" 

import socket 
import threading 
import time 
import base64 
import hashlib 

def calculate_websocket_hash(key): 
    magic_websocket_string = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 
    result_string = key + magic_websocket_string 
    sha1_digest = hashlib.sha1(result_string).digest() 
    response_data = base64.encodestring(sha1_digest) 
    response_string = response_data.decode('utf8') 
    return response_string 

def is_bit_set(int_type, offset): 
    mask = 1 << offset 
    return not 0 == (int_type & mask) 

def set_bit(int_type, offset): 
    return int_type | (1 << offset) 

def bytes_to_int(data): 
    # note big-endian is the standard network byte order 
    return int.from_bytes(data, byteorder='big') 


def pack(data): 
    """pack bytes for sending to client""" 
    frame_head = bytearray(2) 

    # set final fragment 
    frame_head[0] = set_bit(frame_head[0], 7) 

    # set opcode 1 = text 
    frame_head[0] = set_bit(frame_head[0], 0) 

    # payload length 
    assert len(data) < 126, "haven't implemented that yet" 
    frame_head[1] = len(data) 

    # add data 
    frame = frame_head + data.encode('utf-8') 
    print(list(hex(b) for b in frame)) 
    return frame 

def receive(s): 
    """receive data from client""" 

    # read the first two bytes 
    frame_head = s.recv(2) 

    # very first bit indicates if this is the final fragment 
    print("final fragment: ", is_bit_set(frame_head[0], 7)) 

    # bits 4-7 are the opcode (0x01 -> text) 
    print("opcode: ", frame_head[0] & 0x0f) 

    # mask bit, from client will ALWAYS be 1 
    assert is_bit_set(frame_head[1], 7) 

    # length of payload 
    # 7 bits, or 7 bits + 16 bits, or 7 bits + 64 bits 
    payload_length = frame_head[1] & 0x7F 
    if payload_length == 126: 
     raw = s.recv(2) 
     payload_length = bytes_to_int(raw) 
    elif payload_length == 127: 
     raw = s.recv(8) 
     payload_length = bytes_to_int(raw) 
    print('Payload is {} bytes'.format(payload_length)) 

    """masking key 
    All frames sent from the client to the server are masked by a 
    32-bit nounce value that is contained within the frame 
    """ 
    masking_key = s.recv(4) 
    print("mask: ", masking_key, bytes_to_int(masking_key)) 

    # finally get the payload data: 
    masked_data_in = s.recv(payload_length) 
    data = bytearray(payload_length) 

    # The ith byte is the XOR of byte i of the data with 
    # masking_key[i % 4] 
    for i, b in enumerate(masked_data_in): 
     data[i] = b^masking_key[i%4] 

    return data 

def handle(s): 
    client_request = s.recv(4096) 

    # get to the key 
    for line in client_request.splitlines(): 
     if b'Sec-WebSocket-Key:' in line: 
      key = line.split(b': ')[1] 
      break 
    response_string = calculate_websocket_hash(key) 

    header = '''HTTP/1.1 101 Switching Protocols\r 
Upgrade: websocket\r 
Connection: Upgrade\r 
Sec-WebSocket-Accept: {}\r 
\r 
'''.format(response_string) 
    s.send(header.encode()) 

    # this works 
    print(receive(s)) 

    # this doesn't 
    s.send(pack('Hello')) 

    s.close() 

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
s.bind(('', 9876)) 
s.listen(1) 

while True: 
    t,_ = s.accept() 
    threading.Thread(target=handle, args = (t,)).start() 

diese grundlegenden Testseite verwenden (die mit mod-pywebsocket funktioniert):

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
    <title>Web Socket Example</title> 
    <meta charset="UTF-8"> 
</head> 
<body> 
    <div id="serveroutput"></div> 
    <form id="form"> 
     <input type="text" value="Hello World!" id="msg" /> 
     <input type="submit" value="Send" onclick="sendMsg()" /> 
    </form> 
<script> 
    var form = document.getElementById('form'); 
    var msg = document.getElementById('msg'); 
    var output = document.getElementById('serveroutput'); 
    var s = new WebSocket("ws://"+window.location.hostname+":9876"); 
    s.onopen = function(e) { 
     console.log("opened"); 
     out('Connected.'); 
    } 
    s.onclose = function(e) { 
     console.log("closed"); 
     out('Connection closed.'); 
    } 
    s.onmessage = function(e) { 
     console.log("got: " + e.data); 
     out(e.data); 
    } 
    form.onsubmit = function(e) { 
     e.preventDefault(); 
     msg.value = ''; 
     window.scrollTop = window.scrollHeight; 
    } 
    function sendMsg() { 
     s.send(msg.value); 
    } 
    function out(text) { 
     var el = document.createElement('p'); 
     el.innerHTML = text; 
     output.appendChild(el); 
    } 
    msg.focus(); 
</script> 
</body> 
</html> 

Dies empfängt Daten und demaskiert es korrekt, aber ich kann den Sendepfad nicht zum Arbeiten bringen.

Als Test „Hallo“ an die Steckdose zu schreiben, berechnet das Programm über das Bytes in Bezug auf die Buchse geschrieben werden:

['0x81', '0x5', '0x48', '0x65', '0x6c', '0x6c', '0x6f'] 

den in section 5.7 des RFC den Hex-Wert angegeben entsprechen. Leider wird der Rahmen in den Chrome-Entwicklertools nicht angezeigt.

Jede Idee, was ich vermisst habe? Oder ein aktuell funktionierendes Python3-Websocket-Beispiel? Ich bekomme

Unexpected LF in Value at ... 

in der Javascript-Konsole

+0

Tornado unterstützt beide Websockets und Python 3. http://www.tornadoweb.org/documentation/websocket.html –

+0

Danke Thomas. Ich hätte gerne zuerst eine eigenständige Implementierung - es geht darum, das Protokoll zu verstehen und ein Problem für mich zu lösen. Betrachtet man den [Tornado-Quellcode] (https://github.com/facebook/tornado/blob/master/tornado/websocket.py) sehe ich einen Header ** Sec-WebSocket-Protocol **, der von der Server zu dem Client, aber die [spec] (http://tools.ietf.org/html/rfc6455#section-4.2.2) sagt, dass dies optional ist. – Hardbyte

+0

Wenn ein Client ein Unterprotokoll anfordert, wird erwartet, dass der Server es zurückmeldet (immer vorausgesetzt, dass es das Unterprotokoll unterstützt). Andernfalls könnte ein Handshake-Fehler auftreten, der wahrscheinlich nicht mit den Problemen beim Senden von Nachrichten zusammenhängt. – simonc

Antwort

7

Wenn ich versuche, auf Ihren Python-Code von Safari 6.0.1 auf Lion sprechen. Ich habe auch eine IndexError Ausnahme von dem Python-Code.

Wenn ich mit Ihrem Python-Code von Chrome Version 24.0.1290.1 ​​Entwickler auf Lion sprechen, bekomme ich keine Javascript-Fehler. In Ihrem Javascript werden die onopen() und onclose() Methoden aufgerufen, aber nicht die onmessage(). Der Python-Code löst keine Ausnahmen aus und scheint die Nachricht empfangen und gesendet zu haben, also genau das Verhalten, das Sie sehen. Chrome der Lage ist, um zu sehen, Ihre Antwortnachricht dh

got: Hello 

Da Safari nicht den abschließenden LF in der Kopfzeile wie habe ich es versucht, zu entfernen, das heißt

header = '''HTTP/1.1 101 Switching Protocols\r 
Upgrade: websocket\r 
Connection: Upgrade\r 
Sec-WebSocket-Accept: {}\r 
'''.format(response_string) 

Wenn ich diese Änderung vornehmen erscheint in der JavaScript-Konsole.

Safari funktioniert immer noch nicht. Jetzt ist es ein anderes Problem, wenn ich versuche, eine Nachricht zu senden.

websocket.html:36 INVALID_STATE_ERR: DOM Exception 11: An attempt was made to use an object that is not, or is no longer, usable. 

Keiner der Javascript websocket Event-Handler immer Feuer und ich bin immer noch die IndexError Ausnahme von Python zu sehen.

Zum Schluss. Ihr Python-Code funktionierte aufgrund eines zusätzlichen LF in Ihrer Header-Antwort nicht mit Chrome. Es gibt noch etwas anderes, weil der Code, der mit Chrome funktioniert, nicht mit Safari funktioniert.

aktualisieren

Ich habe das zugrunde liegende Problem gearbeitet und haben jetzt das Beispiel in Safari und Chrome arbeiten.

base64.encodestring() fügt immer einen abschließenden \n zu seiner Rückkehr hinzu. Dies ist die Quelle der LF, über die Safari sich beschwert hat.

Anruf .strip() auf den Rückgabewert calculate_websocket_hash und die Verwendung Ihrer ursprünglichen Header-Vorlage funktioniert ordnungsgemäß auf Safari und Chrome.

+0

Awesome, nach Das Entfernen dieser zusätzlichen CRLF funktioniert jetzt für Firefox und Chrome. – Hardbyte