First, untested implementation of websocket protocol support.

This commit is contained in:
Griatch 2014-04-13 13:18:32 +02:00
parent 3b1c66dcbc
commit 01d15f13ee
6 changed files with 811 additions and 7 deletions

View file

@ -239,4 +239,4 @@ class Msdp(object):
Send oob data to Evennia
"""
#print "msdp data_in:", funcname, args, kwargs
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))

View file

@ -42,17 +42,20 @@ TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSOCKET_PORTS = settings.WEBSOCKET_PORTS
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
WEBSOCKET_INTERFACES = settings.WEBSOCKET_INTERFACES
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_ENABLED = settings.WEBSOCKET_ENABLED and WEBSOCKET_PORTS and WEBSOCKET_INTERFACES
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
@ -165,6 +168,7 @@ if AMP_ENABLED:
amp_client.setName('evennia_amp')
PORTAL.services.addService(amp_client)
# We group all the various services under the same twisted app.
# These will gradually be started as they are initialized below.
@ -189,6 +193,7 @@ if TELNET_ENABLED:
print ' telnet%s: %s' % (ifacestr, port)
if SSL_ENABLED:
# Start SSL game connection (requires PyOpenSSL).
@ -236,6 +241,7 @@ if SSH_ENABLED:
print " ssl%s: %s" % (ifacestr, port)
if WEBSERVER_ENABLED:
# Start a reverse proxy to relay data to the Server-side webserver
@ -264,6 +270,30 @@ if WEBSERVER_ENABLED:
PORTAL.services.addService(proxy_service)
print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport)
if WEBSOCKET_ENABLED:
# websocket support is experimental!
# start websocket ports for real-time web communication
from src.server.portal import websocket
from src.utils.txws import WebSocketFactory
for interface in WEBSOCKET_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(WEBSOCKET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in WEBSOCKET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = WebSocketFactory(protocol.ServerFactory())
factory.protocol = websocket.WebSocketProtocol
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, factory, interface=interface)
websocket_service.setName('EvenniaWebSocket%s' % pstring)
PORTAL.services.addService(websocket_service)
print ' websocket%s: %s' % (ifacestr, port)
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
plugin_module.start_plugin_services(PORTAL)

View file

@ -84,12 +84,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
def connectionLost(self, reason):
"""
This is executed when the connection is lost for
whatever reason. It can also be called directly, from
this is executed when the connection is lost for
whatever reason. it can also be called directly, from
the disconnect method
"""
self.sessionhandler.disconnect(self)
self.transport.loseConnection()
self.transport.loseconnection()
def dataReceived(self, data):
"""
@ -97,6 +97,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
starts with IAC (a telnet command) or not. All other data will
be handled in line mode. Some clients also sends an erroneous
line break after IAC, which we must watch out for.
OOB protocols (MSDP etc) already intercept subnegotiations
on their own, never entering this method. They will relay
their parsed data directly to self.data_in.
"""
if data and data[0] == IAC or self.iaw_mode:

View file

@ -0,0 +1,121 @@
"""
Websockets Protocol
This implements WebSockets (http://en.wikipedia.org/wiki/WebSocket)
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS).
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
Communication over the websocket interface is done with normal text
communication. A special case is OOB-style communication; to do this
the client must send data on the following form:
OOB(oobfunc, args, kwargs)
or
OOB[(oobfunc, args, kwargs), ...]
where the tuple/list is sent json-encoded. The initial OOB-prefix
is used to identify this type of communication, all other data
is considered plain text (command input).
"""
import json
from twisted.internet.protocol import Protocol
from src.server.session import Session
from src.utils.logger import log_trace
from src.utils.utils import to_str
from src.utils.text2html import parse_html
class WebSocketProtocol(Protocol, Session):
"""
This is called when the connection is first established
"""
def connectionMade(self):
"""
This is called when the connection is first established.
"""
client_address = self.transport.client
self.init_session("websocket", client_address, self.factory.sessionhandler)
self.sessionhandler.connect(self)
def disconnect(self, reason=None):
"""
generic hook for the engine to call in order to
disconnect this protocol.
"""
if reason:
self.data_out(text=reason)
self.connectionLost(reason)
def connectionLost(self, reason):
"""
this is executed when the connection is lost for
whatever reason. it can also be called directly, from
the disconnect method
"""
self.sessionhandler.disconnect(self)
self.transport.loseconnection()
def dataReceived(self, string):
"""
Method called when data is coming in over
the websocket connection.
Type of data is identified by a 3-character
prefix.
OOB - This is an Out-of-band instruction. If so,
the remaining string should either be
a json packed tuple (oobfuncname, args, kwargs)
or a json-packed list of tuples
[(oobfuncname, args, kwargs), ...] to send to
the OOBhandler.
any other prefix (or lack of prefix) is considered
plain text data, to be treated like a game
input command.
"""
if string[:3] == "OOB":
string = string[3:]
try:
oobdata = json.loads(string)
if isinstance(oobdata, list):
for oobtuple in oobdata:
self.data_in(oob=oobtuple)
elif isinstance(oobdata, tuple):
self.data_in(oob=oobtuple)
else:
raise RuntimeError("OOB data is not list or tuple.")
except:
log_trace("Websocket malformed OOB request: %s" % oobdata)
else:
# plain text input
self.data_in(text=string)
def data_in(self, text=None, **kwargs):
"""
Data Websocket -> Server
"""
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player.
generic hook method for engine to call in order to send data
through the websocket connection.
valid webclient kwargs:
oob=<string> - supply an Out-of-Band instruction.
raw=True - no parsing at all (leave ansi-to-html markers unparsed)
nomarkup=True - clean out all ansi/html markers and tokens
"""
try:
text = to_str(text if text else "", encoding=self.encoding)
except Exception, e:
self.sendLine(str(e))
if "oob" in kwargs:
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
self.sendLine("OOB" + json.dumps(oobstruct))
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if raw:
self.sendLine(text)
else:
self.sendLine(parse_html(text, strip_ansi=nomarkup))

View file

@ -23,8 +23,7 @@ import os
SERVERNAME = "Evennia"
# Activate telnet service
TELNET_ENABLED = True
# A list of ports the Evennia telnet server listens on
# Can be one or many.
# A list of ports the Evennia telnet server listens on Can be one or many.
TELNET_PORTS = [4000]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
TELNET_INTERFACES = ['0.0.0.0']
@ -68,12 +67,18 @@ SSH_ENABLED = False
SSH_PORTS = [8022]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
SSH_INTERFACES = ['0.0.0.0']
# Actiave SSL protocol (SecureSocketLibrary)
# Activate SSL protocol (SecureSocketLibrary)
SSL_ENABLED = False
# Ports to use for SSL
SSL_PORTS = [4001]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
SSL_INTERFACES = ['0.0.0.0']
# Activate Websocket support
WEBSOCKET_ENABLED = False
# Ports to use for Websockets
WEBSOCKET_PORTS = [8021]
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
WEBSOCKET_INTERFACES = ['0.0.0.0.']
# The path that contains this settings.py file (no trailing slash).
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Path to the src directory containing the bulk of the codebase's code.

643
src/utils/txws.py Normal file
View file

@ -0,0 +1,643 @@
# Copyright (c) 2011 Oregon State University Open Source Lab
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
# USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
Blind reimplementation of WebSockets as a standalone wrapper for Twisted
protocols.
"""
__version__ = "0.7.1"
from base64 import b64encode, b64decode
from hashlib import md5, sha1
from string import digits
from struct import pack, unpack
from twisted.internet.interfaces import ISSLTransport
from twisted.protocols.policies import ProtocolWrapper, WrappingFactory
from twisted.python import log
from twisted.web.http import datetimeToString
class WSException(Exception):
"""
Something stupid happened here.
If this class escapes txWS, then something stupid happened in multiple
places.
"""
# Flavors of WS supported here.
# HYBI00 - Hixie-76, HyBi-00. Challenge/response after headers, very minimal
# framing. Tricky to start up, but very smooth sailing afterwards.
# HYBI07 - HyBi-07. Modern "standard" handshake. Bizarre masked frames, lots
# of binary data packing.
# HYBI10 - HyBi-10. Just like HyBi-07. No, seriously. *Exactly* the same,
# except for the protocol number.
# RFC6455 - RFC 6455. The official WebSocket protocol standard. The protocol
# number is 13, but otherwise it is identical to HyBi-07.
HYBI00, HYBI07, HYBI10, RFC6455 = range(4)
# States of the state machine. Because there are no reliable byte counts for
# any of this, we don't use StatefulProtocol; instead, we use custom state
# enumerations. Yay!
REQUEST, NEGOTIATING, CHALLENGE, FRAMES = range(4)
# Control frame specifiers. Some versions of WS have control signals sent
# in-band. Adorable, right?
NORMAL, CLOSE, PING, PONG = range(4)
opcode_types = {
0x0: NORMAL,
0x1: NORMAL,
0x2: NORMAL,
0x8: CLOSE,
0x9: PING,
0xa: PONG,
}
encoders = {
"base64": b64encode,
}
decoders = {
"base64": b64decode,
}
# Fake HTTP stuff, and a couple convenience methods for examining fake HTTP
# headers.
def http_headers(s):
"""
Create a dictionary of data from raw HTTP headers.
"""
d = {}
for line in s.split("\r\n"):
try:
key, value = [i.strip() for i in line.split(":", 1)]
d[key] = value
except ValueError:
pass
return d
def is_websocket(headers):
"""
Determine whether a given set of headers is asking for WebSockets.
"""
return ("upgrade" in headers.get("Connection", "").lower()
and headers.get("Upgrade").lower() == "websocket")
def is_hybi00(headers):
"""
Determine whether a given set of headers is HyBi-00-compliant.
Hixie-76 and HyBi-00 use a pair of keys in the headers to handshake with
servers.
"""
return "Sec-WebSocket-Key1" in headers and "Sec-WebSocket-Key2" in headers
# Authentication for WS.
def complete_hybi00(headers, challenge):
"""
Generate the response for a HyBi-00 challenge.
"""
key1 = headers["Sec-WebSocket-Key1"]
key2 = headers["Sec-WebSocket-Key2"]
first = int("".join(i for i in key1 if i in digits)) / key1.count(" ")
second = int("".join(i for i in key2 if i in digits)) / key2.count(" ")
nonce = pack(">II8s", first, second, challenge)
return md5(nonce).digest()
def make_accept(key):
"""
Create an "accept" response for a given key.
This dance is expected to somehow magically make WebSockets secure.
"""
guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
return sha1("%s%s" % (key, guid)).digest().encode("base64").strip()
# Frame helpers.
# Separated out to make unit testing a lot easier.
# Frames are bonghits in newer WS versions, so helpers are appreciated.
def make_hybi00_frame(buf):
"""
Make a HyBi-00 frame from some data.
This function does exactly zero checks to make sure that the data is safe
and valid text without any 0xff bytes.
"""
return "\x00%s\xff" % buf
def parse_hybi00_frames(buf):
"""
Parse HyBi-00 frames, returning unwrapped frames and any unmatched data.
This function does not care about garbage data on the wire between frames,
and will actively ignore it.
"""
start = buf.find("\x00")
tail = 0
frames = []
while start != -1:
end = buf.find("\xff", start + 1)
if end == -1:
# Incomplete frame, try again later.
break
else:
# Found a frame, put it in the list.
frame = buf[start + 1:end]
frames.append((NORMAL, frame))
tail = end + 1
start = buf.find("\x00", end + 1)
# Adjust the buffer and return.
buf = buf[tail:]
return frames, buf
def mask(buf, key):
"""
Mask or unmask a buffer of bytes with a masking key.
The key must be exactly four bytes long.
"""
# This is super-secure, I promise~
key = [ord(i) for i in key]
buf = list(buf)
for i, char in enumerate(buf):
buf[i] = chr(ord(char) ^ key[i % 4])
return "".join(buf)
def make_hybi07_frame(buf, opcode=0x1):
"""
Make a HyBi-07 frame.
This function always creates unmasked frames, and attempts to use the
smallest possible lengths.
"""
if len(buf) > 0xffff:
length = "\x7f%s" % pack(">Q", len(buf))
elif len(buf) > 0x7d:
length = "\x7e%s" % pack(">H", len(buf))
else:
length = chr(len(buf))
# Always make a normal packet.
header = chr(0x80 | opcode)
frame = "%s%s%s" % (header, length, buf)
return frame
def make_hybi07_frame_dwim(buf):
"""
Make a HyBi-07 frame with binary or text data according to the type of buf.
"""
# TODO: eliminate magic numbers.
if isinstance(buf, str):
return make_hybi07_frame(buf, opcode=0x2)
elif isinstance(buf, unicode):
return make_hybi07_frame(buf.encode("utf-8"), opcode=0x1)
else:
raise TypeError("In binary support mode, frame data must be either str or unicode")
def parse_hybi07_frames(buf):
"""
Parse HyBi-07 frames in a highly compliant manner.
"""
start = 0
frames = []
while True:
# If there's not at least two bytes in the buffer, bail.
if len(buf) - start < 2:
break
# Grab the header. This single byte holds some flags nobody cares
# about, and an opcode which nobody cares about.
header = ord(buf[start])
if header & 0x70:
# At least one of the reserved flags is set. Pork chop sandwiches!
raise WSException("Reserved flag in HyBi-07 frame (%d)" % header)
frames.append(("", CLOSE))
return frames, buf
# Get the opcode, and translate it to a local enum which we actually
# care about.
opcode = header & 0xf
try:
opcode = opcode_types[opcode]
except KeyError:
raise WSException("Unknown opcode %d in HyBi-07 frame" % opcode)
# Get the payload length and determine whether we need to look for an
# extra length.
length = ord(buf[start + 1])
masked = length & 0x80
length &= 0x7f
# The offset we're gonna be using to walk through the frame. We use
# this because the offset is variable depending on the length and
# mask.
offset = 2
# Extra length fields.
if length == 0x7e:
if len(buf) - start < 4:
break
length = buf[start + 2:start + 4]
length = unpack(">H", length)[0]
offset += 2
elif length == 0x7f:
if len(buf) - start < 10:
break
# Protocol bug: The top bit of this long long *must* be cleared;
# that is, it is expected to be interpreted as signed. That's
# fucking stupid, if you don't mind me saying so, and so we're
# interpreting it as unsigned anyway. If you wanna send exabytes
# of data down the wire, then go ahead!
length = buf[start + 2:start + 10]
length = unpack(">Q", length)[0]
offset += 8
if masked:
if len(buf) - (start + offset) < 4:
break
key = buf[start + offset:start + offset + 4]
offset += 4
if len(buf) - (start + offset) < length:
break
data = buf[start + offset:start + offset + length]
if masked:
data = mask(data, key)
if opcode == CLOSE:
if len(data) >= 2:
# Gotta unpack the opcode and return usable data here.
data = unpack(">H", data[:2])[0], data[2:]
else:
# No reason given; use generic data.
data = 1000, "No reason given"
frames.append((opcode, data))
start += offset + length
return frames, buf[start:]
class WebSocketProtocol(ProtocolWrapper):
"""
Protocol which wraps another protocol to provide a WebSockets transport
layer.
"""
buf = ""
codec = None
location = "/"
host = "example.com"
origin = "http://example.com"
state = REQUEST
flavor = None
do_binary_frames = False
def __init__(self, *args, **kwargs):
ProtocolWrapper.__init__(self, *args, **kwargs)
self.pending_frames = []
def setBinaryMode(self, mode):
"""
If True, send str as binary and unicode as text.
Defaults to false for backwards compatibility.
"""
self.do_binary_frames = bool(mode)
def isSecure(self):
"""
Borrowed technique for determining whether this connection is over
SSL/TLS.
"""
return ISSLTransport(self.transport, None) is not None
def sendCommonPreamble(self):
"""
Send the preamble common to all WebSockets connections.
This might go away in the future if WebSockets continue to diverge.
"""
self.transport.writeSequence([
"HTTP/1.1 101 FYI I am not a webserver\r\n",
"Server: TwistedWebSocketWrapper/1.0\r\n",
"Date: %s\r\n" % datetimeToString(),
"Upgrade: WebSocket\r\n",
"Connection: Upgrade\r\n",
])
def sendHyBi00Preamble(self):
"""
Send a HyBi-00 preamble.
"""
protocol = "wss" if self.isSecure() else "ws"
self.sendCommonPreamble()
self.transport.writeSequence([
"Sec-WebSocket-Origin: %s\r\n" % self.origin,
"Sec-WebSocket-Location: %s://%s%s\r\n" % (protocol, self.host,
self.location),
"WebSocket-Protocol: %s\r\n" % self.codec,
"Sec-WebSocket-Protocol: %s\r\n" % self.codec,
"\r\n",
])
def sendHyBi07Preamble(self):
"""
Send a HyBi-07 preamble.
"""
self.sendCommonPreamble()
challenge = self.headers["Sec-WebSocket-Key"]
response = make_accept(challenge)
self.transport.write("Sec-WebSocket-Accept: %s\r\n\r\n" % response)
def parseFrames(self):
"""
Find frames in incoming data and pass them to the underlying protocol.
"""
if self.flavor == HYBI00:
parser = parse_hybi00_frames
elif self.flavor in (HYBI07, HYBI10, RFC6455):
parser = parse_hybi07_frames
else:
raise WSException("Unknown flavor %r" % self.flavor)
try:
frames, self.buf = parser(self.buf)
except WSException, wse:
# Couldn't parse all the frames, something went wrong, let's bail.
self.close(wse.args[0])
return
for frame in frames:
opcode, data = frame
if opcode == NORMAL:
# Business as usual. Decode the frame, if we have a decoder.
if self.codec:
data = decoders[self.codec](data)
# Pass the frame to the underlying protocol.
ProtocolWrapper.dataReceived(self, data)
elif opcode == CLOSE:
# The other side wants us to close. I wonder why?
reason, text = data
log.msg("Closing connection: %r (%d)" % (text, reason))
# Close the connection.
self.close()
def sendFrames(self):
"""
Send all pending frames.
"""
if self.state != FRAMES:
return
if self.flavor == HYBI00:
maker = make_hybi00_frame
elif self.flavor in (HYBI07, HYBI10, RFC6455):
if self.do_binary_frames:
maker = make_hybi07_frame_dwim
else:
maker = make_hybi07_frame
else:
raise WSException("Unknown flavor %r" % self.flavor)
for frame in self.pending_frames:
# Encode the frame before sending it.
if self.codec:
frame = encoders[self.codec](frame)
packet = maker(frame)
self.transport.write(packet)
self.pending_frames = []
def validateHeaders(self):
"""
Check received headers for sanity and correctness, and stash any data
from them which will be required later.
"""
# Obvious but necessary.
if not is_websocket(self.headers):
log.msg("Not handling non-WS request")
return False
# Stash host and origin for those browsers that care about it.
if "Host" in self.headers:
self.host = self.headers["Host"]
if "Origin" in self.headers:
self.origin = self.headers["Origin"]
# Check whether a codec is needed. WS calls this a "protocol" for
# reasons I cannot fathom. Newer versions of noVNC (0.4+) sets
# multiple comma-separated codecs, handle this by chosing first one
# we can encode/decode.
protocols = None
if "WebSocket-Protocol" in self.headers:
protocols = self.headers["WebSocket-Protocol"]
elif "Sec-WebSocket-Protocol" in self.headers:
protocols = self.headers["Sec-WebSocket-Protocol"]
if isinstance(protocols, basestring):
protocols = [p.strip() for p in protocols.split(',')]
for protocol in protocols:
if protocol in encoders or protocol in decoders:
log.msg("Using WS protocol %s!" % protocol)
self.codec = protocol
break
log.msg("Couldn't handle WS protocol %s!" % protocol)
if not self.codec:
return False
# Start the next phase of the handshake for HyBi-00.
if is_hybi00(self.headers):
log.msg("Starting HyBi-00/Hixie-76 handshake")
self.flavor = HYBI00
self.state = CHALLENGE
# Start the next phase of the handshake for HyBi-07+.
if "Sec-WebSocket-Version" in self.headers:
version = self.headers["Sec-WebSocket-Version"]
if version == "7":
log.msg("Starting HyBi-07 conversation")
self.sendHyBi07Preamble()
self.flavor = HYBI07
self.state = FRAMES
elif version == "8":
log.msg("Starting HyBi-10 conversation")
self.sendHyBi07Preamble()
self.flavor = HYBI10
self.state = FRAMES
elif version == "13":
log.msg("Starting RFC 6455 conversation")
self.sendHyBi07Preamble()
self.flavor = RFC6455
self.state = FRAMES
else:
log.msg("Can't support protocol version %s!" % version)
return False
return True
def dataReceived(self, data):
self.buf += data
oldstate = None
while oldstate != self.state:
oldstate = self.state
# Handle initial requests. These look very much like HTTP
# requests, but aren't. We need to capture the request path for
# those browsers which want us to echo it back to them (Chrome,
# mainly.)
# These lines look like:
# GET /some/path/to/a/websocket/resource HTTP/1.1
if self.state == REQUEST:
if "\r\n" in self.buf:
request, chaff, self.buf = self.buf.partition("\r\n")
try:
verb, self.location, version = request.split(" ")
except ValueError:
self.loseConnection()
else:
self.state = NEGOTIATING
elif self.state == NEGOTIATING:
# Check to see if we've got a complete set of headers yet.
if "\r\n\r\n" in self.buf:
head, chaff, self.buf = self.buf.partition("\r\n\r\n")
self.headers = http_headers(head)
# Validate headers. This will cause a state change.
if not self.validateHeaders():
self.loseConnection()
elif self.state == CHALLENGE:
# Handle the challenge. This is completely exclusive to
# HyBi-00/Hixie-76.
if len(self.buf) >= 8:
challenge, self.buf = self.buf[:8], self.buf[8:]
response = complete_hybi00(self.headers, challenge)
self.sendHyBi00Preamble()
self.transport.write(response)
log.msg("Completed HyBi-00/Hixie-76 handshake")
# We're all finished here; start sending frames.
self.state = FRAMES
elif self.state == FRAMES:
self.parseFrames()
# Kick any pending frames. This is needed because frames might have
# started piling up early; we can get write()s from our protocol above
# when they makeConnection() immediately, before our browser client
# actually sends any data. In those cases, we need to manually kick
# pending frames.
if self.pending_frames:
self.sendFrames()
def write(self, data):
"""
Write to the transport.
This method will only be called by the underlying protocol.
"""
self.pending_frames.append(data)
self.sendFrames()
def writeSequence(self, data):
"""
Write a sequence of data to the transport.
This method will only be called by the underlying protocol.
"""
self.pending_frames.extend(data)
self.sendFrames()
def close(self, reason=""):
"""
Close the connection.
This includes telling the other side we're closing the connection.
If the other side didn't signal that the connection is being closed,
then we might not see their last message, but since their last message
should, according to the spec, be a simple acknowledgement, it
shouldn't be a problem.
"""
# Send a closing frame. It's only polite. (And might keep the browser
# from hanging.)
if self.flavor in (HYBI07, HYBI10, RFC6455):
frame = make_hybi07_frame(reason, opcode=0x8)
self.transport.write(frame)
self.loseConnection()
class WebSocketFactory(WrappingFactory):
"""
Factory which wraps another factory to provide WebSockets transports for
all of its protocols.
"""
protocol = WebSocketProtocol