mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 792831 - WebSocket permessage compression extension, r=jduell
This commit is contained in:
parent
4c280fb16f
commit
2296e00d57
@ -0,0 +1,17 @@
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import common
|
||||
|
||||
def web_socket_do_extra_handshake(request):
|
||||
if request.ws_requested_extensions is not None:
|
||||
for extension_request in request.ws_requested_extensions:
|
||||
if extension_request.name() == "permessage-deflate":
|
||||
raise ValueError('permessage-deflate should not be offered')
|
||||
|
||||
def web_socket_transfer_data(request):
|
||||
while True:
|
||||
rcvd = msgutil.receive_message(request)
|
||||
opcode = request.ws_stream.get_last_received_opcode()
|
||||
if (opcode == common.OPCODE_BINARY):
|
||||
msgutil.send_message(request, rcvd, binary=True)
|
||||
elif (opcode == common.OPCODE_TEXT):
|
||||
msgutil.send_message(request, rcvd)
|
@ -0,0 +1,23 @@
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import common
|
||||
|
||||
def web_socket_do_extra_handshake(request):
|
||||
deflate_removed = False
|
||||
|
||||
if request.ws_extension_processors is not None:
|
||||
for extension_processor in request.ws_extension_processors:
|
||||
if extension_processor.name() == "deflate":
|
||||
request.ws_extension_processors.remove(extension_processor)
|
||||
deflate_removed = True
|
||||
|
||||
if deflate_removed is False:
|
||||
raise ValueError('deflate extension processor not found')
|
||||
|
||||
def web_socket_transfer_data(request):
|
||||
while True:
|
||||
rcvd = msgutil.receive_message(request)
|
||||
opcode = request.ws_stream.get_last_received_opcode()
|
||||
if (opcode == common.OPCODE_BINARY):
|
||||
msgutil.send_message(request, rcvd, binary=True)
|
||||
elif (opcode == common.OPCODE_TEXT):
|
||||
msgutil.send_message(request, rcvd)
|
22
dom/base/test/file_websocket_permessage_deflate_wsh.py
Normal file
22
dom/base/test/file_websocket_permessage_deflate_wsh.py
Normal file
@ -0,0 +1,22 @@
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import common
|
||||
|
||||
def web_socket_do_extra_handshake(request):
|
||||
pmce_offered = False
|
||||
|
||||
if request.ws_requested_extensions is not None:
|
||||
for extension_request in request.ws_requested_extensions:
|
||||
if extension_request.name() == "permessage-deflate":
|
||||
pmce_offered = True
|
||||
|
||||
if pmce_offered is False:
|
||||
raise ValueError('permessage-deflate not offered')
|
||||
|
||||
def web_socket_transfer_data(request):
|
||||
while True:
|
||||
rcvd = msgutil.receive_message(request)
|
||||
opcode = request.ws_stream.get_last_received_opcode()
|
||||
if (opcode == common.OPCODE_BINARY):
|
||||
msgutil.send_message(request, rcvd, binary=True)
|
||||
elif (opcode == common.OPCODE_TEXT):
|
||||
msgutil.send_message(request, rcvd)
|
@ -194,6 +194,9 @@ support-files =
|
||||
file_websocket_basic_wsh.py
|
||||
file_websocket_hello_wsh.py
|
||||
file_websocket_http_resource.txt
|
||||
file_websocket_permessage_deflate_wsh.py
|
||||
file_websocket_permessage_deflate_disabled_wsh.py
|
||||
file_websocket_permessage_deflate_rejected_wsh.py
|
||||
file_websocket_wsh.py
|
||||
file_x-frame-options_main.html
|
||||
file_x-frame-options_page.sjs
|
||||
@ -734,6 +737,8 @@ skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android'
|
||||
[test_websocket_hello.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android'
|
||||
[test_websocket_permessage_deflate.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android'
|
||||
[test_x-frame-options.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s # b2g(observerservice issue) b2g-debug(observerservice issue) b2g-desktop(observerservice issue)
|
||||
[test_xbl_userdata.xhtml]
|
||||
|
105
dom/base/test/test_websocket_permessage_deflate.html
Normal file
105
dom/base/test/test_websocket_permessage_deflate.html
Normal file
@ -0,0 +1,105 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic test of permessage compression websocket extension</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body onload="testDeflate()">
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792831">Mozilla Bug </a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var ws;
|
||||
var textMessage = "This is a text message";
|
||||
var binaryMessage = "This is a binary message";
|
||||
var testIdx = 0;
|
||||
var sendText = true;
|
||||
|
||||
tests = [
|
||||
// enable PMCE
|
||||
[ true, true, "ws://mochi.test:8888/tests/dom/base/test/file_websocket_permessage_deflate" ],
|
||||
// disable PMCE
|
||||
[ false, false, "ws://mochi.test:8888/tests/dom/base/test/file_websocket_permessage_deflate_disabled" ],
|
||||
// server rejects offered PMCE
|
||||
[ true, false, "ws://mochi.test:8888/tests/dom/base/test/file_websocket_permessage_deflate_rejected" ] ]
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
function str2ab(str) {
|
||||
var buf = new ArrayBuffer(str.length*2);
|
||||
var bufView = new Uint16Array(buf);
|
||||
for (var i=0, strLen=str.length; i<strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (sendText) {
|
||||
ws.send(textMessage);
|
||||
} else {
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.send(str2ab(binaryMessage));
|
||||
}
|
||||
}
|
||||
|
||||
function testDeflate() {
|
||||
SpecialPowers.setBoolPref("network.websocket.extensions.permessage-deflate", tests[testIdx][0]);
|
||||
|
||||
ws = new WebSocket(tests[testIdx][2]);
|
||||
|
||||
ws.onopen = function(e) {
|
||||
if (tests[testIdx][1]) {
|
||||
is(ws.extensions, "permessage-deflate", "permessage-deflate not negotiated!");
|
||||
} else {
|
||||
is(ws.extensions, "", "permessage-deflate should not be negotiated!");
|
||||
}
|
||||
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
ws.onclose = function(e) {
|
||||
if (!e.wasClean) {
|
||||
ok(false, "Connection should be closed cleanly!");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = function(e) {
|
||||
ok(false, "onerror called!");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
if (sendText) {
|
||||
is(e.data, textMessage, "Text message not received successfully!");
|
||||
sendText = false;
|
||||
sendMessage();
|
||||
} else {
|
||||
ok(e.data instanceof ArrayBuffer, "Should receive an arraybuffer!");
|
||||
is(ab2str(e.data), binaryMessage, "Binary message not received successfully!");
|
||||
ws.close();
|
||||
|
||||
sendText = true;
|
||||
testIdx++;
|
||||
if (testIdx < tests.length) {
|
||||
testDeflate();
|
||||
} else {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -1355,11 +1355,9 @@ pref("network.websocket.timeout.ping.request", 0);
|
||||
// event is sent to the javascript websockets application
|
||||
pref("network.websocket.timeout.ping.response", 10);
|
||||
|
||||
// Defines whether or not to try and negotiate the stream-deflate compression
|
||||
// extension with the websocket server. Stream-Deflate has been removed from
|
||||
// the standards track document, but can still be used by servers who opt
|
||||
// into it.
|
||||
pref("network.websocket.extensions.stream-deflate", false);
|
||||
// Defines whether or not to try to negotiate the permessage compression
|
||||
// extension with the websocket server.
|
||||
pref("network.websocket.extensions.permessage-deflate", true);
|
||||
|
||||
// the maximum number of concurrent websocket sessions. By specification there
|
||||
// is never more than one handshake oustanding to an individual host at
|
||||
|
@ -713,6 +713,186 @@ private:
|
||||
};
|
||||
NS_IMPL_ISUPPORTS(CallOnTransportAvailable, nsIRunnable)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// PMCECompression
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
class PMCECompression
|
||||
{
|
||||
public:
|
||||
explicit PMCECompression(bool aNoContextTakeover)
|
||||
: mActive(false)
|
||||
, mNoContextTakeover(aNoContextTakeover)
|
||||
, mResetDeflater(false)
|
||||
, mMessageDeflated(false)
|
||||
{
|
||||
MOZ_COUNT_CTOR(PMCECompression);
|
||||
|
||||
mDeflater.zalloc = mInflater.zalloc = Z_NULL;
|
||||
mDeflater.zfree = mInflater.zfree = Z_NULL;
|
||||
mDeflater.opaque = mInflater.opaque = Z_NULL;
|
||||
|
||||
if (deflateInit2(&mDeflater, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15,
|
||||
8, Z_DEFAULT_STRATEGY) == Z_OK) {
|
||||
if (inflateInit2(&mInflater, -15) == Z_OK) {
|
||||
mActive = true;
|
||||
} else {
|
||||
deflateEnd(&mDeflater);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~PMCECompression()
|
||||
{
|
||||
MOZ_COUNT_DTOR(PMCECompression);
|
||||
|
||||
if (mActive) {
|
||||
inflateEnd(&mInflater);
|
||||
deflateEnd(&mDeflater);
|
||||
}
|
||||
}
|
||||
|
||||
bool Active()
|
||||
{
|
||||
return mActive;
|
||||
}
|
||||
|
||||
void SetMessageDeflated()
|
||||
{
|
||||
MOZ_ASSERT(!mMessageDeflated);
|
||||
mMessageDeflated = true;
|
||||
}
|
||||
bool IsMessageDeflated()
|
||||
{
|
||||
return mMessageDeflated;
|
||||
}
|
||||
|
||||
bool UsingContextTakeover()
|
||||
{
|
||||
return !mNoContextTakeover;
|
||||
}
|
||||
|
||||
nsresult Deflate(uint8_t *data, uint32_t dataLen, nsACString &_retval)
|
||||
{
|
||||
if (mResetDeflater || mNoContextTakeover) {
|
||||
if (deflateReset(&mDeflater) != Z_OK) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
mResetDeflater = false;
|
||||
}
|
||||
|
||||
mDeflater.avail_out = kBufferLen;
|
||||
mDeflater.next_out = mBuffer;
|
||||
mDeflater.avail_in = dataLen;
|
||||
mDeflater.next_in = data;
|
||||
|
||||
while (true) {
|
||||
int zerr = deflate(&mDeflater, Z_SYNC_FLUSH);
|
||||
|
||||
if (zerr != Z_OK) {
|
||||
mResetDeflater = true;
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
uint32_t deflated = kBufferLen - mDeflater.avail_out;
|
||||
if (deflated > 0) {
|
||||
_retval.Append(reinterpret_cast<char *>(mBuffer), deflated);
|
||||
}
|
||||
|
||||
mDeflater.avail_out = kBufferLen;
|
||||
mDeflater.next_out = mBuffer;
|
||||
|
||||
if (mDeflater.avail_in > 0) {
|
||||
continue; // There is still some data to deflate
|
||||
}
|
||||
|
||||
if (deflated == kBufferLen) {
|
||||
continue; // There was not enough space in the buffer
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (_retval.Length() < 4) {
|
||||
MOZ_ASSERT(false, "Expected trailing not found in deflated data!");
|
||||
mResetDeflater = true;
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
_retval.Truncate(_retval.Length() - 4);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult Inflate(uint8_t *data, uint32_t dataLen, nsACString &_retval)
|
||||
{
|
||||
mMessageDeflated = false;
|
||||
|
||||
Bytef trailingData[] = { 0x00, 0x00, 0xFF, 0xFF };
|
||||
bool trailingDataUsed = false;
|
||||
|
||||
mInflater.avail_out = kBufferLen;
|
||||
mInflater.next_out = mBuffer;
|
||||
mInflater.avail_in = dataLen;
|
||||
mInflater.next_in = data;
|
||||
|
||||
while (true) {
|
||||
int zerr = inflate(&mInflater, Z_NO_FLUSH);
|
||||
|
||||
if (zerr == Z_STREAM_END) {
|
||||
Bytef *saveNextIn = mInflater.next_in;
|
||||
uint32_t saveAvailIn = mInflater.avail_in;
|
||||
Bytef *saveNextOut = mInflater.next_out;
|
||||
uint32_t saveAvailOut = mInflater.avail_out;
|
||||
|
||||
inflateReset(&mInflater);
|
||||
|
||||
mInflater.next_in = saveNextIn;
|
||||
mInflater.avail_in = saveAvailIn;
|
||||
mInflater.next_out = saveNextOut;
|
||||
mInflater.avail_out = saveAvailOut;
|
||||
} else if (zerr != Z_OK && zerr != Z_BUF_ERROR) {
|
||||
return NS_ERROR_INVALID_CONTENT_ENCODING;
|
||||
}
|
||||
|
||||
uint32_t inflated = kBufferLen - mInflater.avail_out;
|
||||
if (inflated > 0) {
|
||||
_retval.Append(reinterpret_cast<char *>(mBuffer), inflated);
|
||||
}
|
||||
|
||||
mInflater.avail_out = kBufferLen;
|
||||
mInflater.next_out = mBuffer;
|
||||
|
||||
if (mInflater.avail_in > 0) {
|
||||
continue; // There is still some data to inflate
|
||||
}
|
||||
|
||||
if (inflated == kBufferLen) {
|
||||
continue; // There was not enough space in the buffer
|
||||
}
|
||||
|
||||
if (!trailingDataUsed) {
|
||||
trailingDataUsed = true;
|
||||
mInflater.avail_in = sizeof(trailingData);
|
||||
mInflater.next_in = trailingData;
|
||||
continue;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool mActive;
|
||||
bool mNoContextTakeover;
|
||||
bool mResetDeflater;
|
||||
bool mMessageDeflated;
|
||||
z_stream mDeflater;
|
||||
z_stream mInflater;
|
||||
const static uint32_t kBufferLen = 4096;
|
||||
uint8_t mBuffer[kBufferLen];
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// OutboundMessage
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -739,7 +919,7 @@ class OutboundMessage
|
||||
{
|
||||
public:
|
||||
OutboundMessage(WsMsgType type, nsCString *str)
|
||||
: mMsgType(type)
|
||||
: mMsgType(type), mDeflated(false), mOrigLength(0)
|
||||
{
|
||||
MOZ_COUNT_CTOR(OutboundMessage);
|
||||
mMsg.pString = str;
|
||||
@ -747,7 +927,8 @@ public:
|
||||
}
|
||||
|
||||
OutboundMessage(nsIInputStream *stream, uint32_t length)
|
||||
: mMsgType(kMsgTypeStream), mLength(length)
|
||||
: mMsgType(kMsgTypeStream), mLength(length), mDeflated(false)
|
||||
, mOrigLength(0)
|
||||
{
|
||||
MOZ_COUNT_CTOR(OutboundMessage);
|
||||
mMsg.pStream = stream;
|
||||
@ -777,6 +958,7 @@ public:
|
||||
|
||||
WsMsgType GetMsgType() const { return mMsgType; }
|
||||
int32_t Length() const { return mLength; }
|
||||
int32_t OrigLength() const { return mDeflated ? mOrigLength : mLength; }
|
||||
|
||||
uint8_t* BeginWriting() {
|
||||
NS_ABORT_IF_FALSE(mMsgType != kMsgTypeStream,
|
||||
@ -814,6 +996,48 @@ public:
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool DeflatePayload(PMCECompression *aCompressor)
|
||||
{
|
||||
NS_ABORT_IF_FALSE(mMsgType != kMsgTypeStream,
|
||||
"Stream should have been converted to string by now");
|
||||
MOZ_ASSERT(!mDeflated);
|
||||
|
||||
nsresult rv;
|
||||
|
||||
if (mLength == 0) {
|
||||
// Empty message
|
||||
return false;
|
||||
}
|
||||
|
||||
nsAutoPtr<nsCString> temp(new nsCString());
|
||||
rv = aCompressor->Deflate(BeginReading(), mLength, *temp);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("WebSocketChannel::OutboundMessage: Deflating payload failed "
|
||||
"[rv=0x%08x]\n", rv));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!aCompressor->UsingContextTakeover() && temp->Length() > mLength) {
|
||||
// When "client_no_context_takeover" was negotiated, do not send deflated
|
||||
// payload if it's larger that the original one. OTOH, it makes sense
|
||||
// to send the larger deflated payload when the sliding window is not
|
||||
// reset between messages because if we would skip some deflated block
|
||||
// we would need to empty the sliding window which could affect the
|
||||
// compression of the subsequent messages.
|
||||
LOG(("WebSocketChannel::OutboundMessage: Not deflating message since the "
|
||||
"deflated payload is larger than the original one [deflated=%d, "
|
||||
"original=%d]", temp->Length(), mLength));
|
||||
return false;
|
||||
}
|
||||
|
||||
mOrigLength = mLength;
|
||||
mDeflated = true;
|
||||
mLength = temp->Length();
|
||||
delete mMsg.pString;
|
||||
mMsg.pString = temp.forget();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
union {
|
||||
nsCString *pString;
|
||||
@ -821,6 +1045,8 @@ private:
|
||||
} mMsg;
|
||||
WsMsgType mMsgType;
|
||||
uint32_t mLength;
|
||||
bool mDeflated;
|
||||
uint32_t mOrigLength;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -849,132 +1075,6 @@ private:
|
||||
};
|
||||
NS_IMPL_ISUPPORTS(OutboundEnqueuer, nsIRunnable)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// nsWSCompression
|
||||
//
|
||||
// similar to nsDeflateConverter except for the mandatory FLUSH calls
|
||||
// required by websocket and the absence of the deflate termination
|
||||
// block which is appropriate because it would create data bytes after
|
||||
// sending the websockets CLOSE message.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
class nsWSCompression
|
||||
{
|
||||
public:
|
||||
nsWSCompression(nsIStreamListener *aListener,
|
||||
nsISupports *aContext)
|
||||
: mActive(false),
|
||||
mContext(aContext),
|
||||
mListener(aListener)
|
||||
{
|
||||
MOZ_COUNT_CTOR(nsWSCompression);
|
||||
|
||||
mZlib.zalloc = allocator;
|
||||
mZlib.zfree = destructor;
|
||||
mZlib.opaque = Z_NULL;
|
||||
|
||||
// Initialize the compressor - these are all the normal zlib
|
||||
// defaults except window size is set to -15 instead of +15.
|
||||
// This is the zlib way of specifying raw RFC 1951 output instead
|
||||
// of the zlib rfc 1950 format which has a 2 byte header and
|
||||
// adler checksum as a trailer
|
||||
|
||||
nsresult rv;
|
||||
mStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv);
|
||||
if (NS_SUCCEEDED(rv) && aContext && aListener &&
|
||||
deflateInit2(&mZlib, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8,
|
||||
Z_DEFAULT_STRATEGY) == Z_OK) {
|
||||
mActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
~nsWSCompression()
|
||||
{
|
||||
MOZ_COUNT_DTOR(nsWSCompression);
|
||||
|
||||
if (mActive)
|
||||
deflateEnd(&mZlib);
|
||||
}
|
||||
|
||||
bool Active()
|
||||
{
|
||||
return mActive;
|
||||
}
|
||||
|
||||
nsresult Deflate(uint8_t *buf1, uint32_t buf1Len,
|
||||
uint8_t *buf2, uint32_t buf2Len)
|
||||
{
|
||||
NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread,
|
||||
"not socket thread");
|
||||
NS_ABORT_IF_FALSE(mActive, "not active");
|
||||
|
||||
mZlib.avail_out = kBufferLen;
|
||||
mZlib.next_out = mBuffer;
|
||||
mZlib.avail_in = buf1Len;
|
||||
mZlib.next_in = buf1;
|
||||
|
||||
nsresult rv;
|
||||
|
||||
while (mZlib.avail_in > 0) {
|
||||
deflate(&mZlib, (buf2Len > 0) ? Z_NO_FLUSH : Z_SYNC_FLUSH);
|
||||
rv = PushData();
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
mZlib.avail_out = kBufferLen;
|
||||
mZlib.next_out = mBuffer;
|
||||
}
|
||||
|
||||
mZlib.avail_in = buf2Len;
|
||||
mZlib.next_in = buf2;
|
||||
|
||||
while (mZlib.avail_in > 0) {
|
||||
deflate(&mZlib, Z_SYNC_FLUSH);
|
||||
rv = PushData();
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
mZlib.avail_out = kBufferLen;
|
||||
mZlib.next_out = mBuffer;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
// use zlib data types
|
||||
static void *allocator(void *opaque, uInt items, uInt size)
|
||||
{
|
||||
return moz_xmalloc(items * size);
|
||||
}
|
||||
|
||||
static void destructor(void *opaque, void *addr)
|
||||
{
|
||||
moz_free(addr);
|
||||
}
|
||||
|
||||
nsresult PushData()
|
||||
{
|
||||
uint32_t bytesToWrite = kBufferLen - mZlib.avail_out;
|
||||
if (bytesToWrite > 0) {
|
||||
mStream->ShareData(reinterpret_cast<char *>(mBuffer), bytesToWrite);
|
||||
nsresult rv =
|
||||
mListener->OnDataAvailable(nullptr, mContext, mStream, 0, bytesToWrite);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool mActive;
|
||||
z_stream mZlib;
|
||||
nsCOMPtr<nsIStringInputStream> mStream;
|
||||
|
||||
nsISupports *mContext; /* weak ref */
|
||||
nsIStreamListener *mListener; /* weak ref */
|
||||
|
||||
const static int32_t kBufferLen = 4096;
|
||||
uint8_t mBuffer[kBufferLen];
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// WebSocketChannel
|
||||
@ -996,7 +1096,6 @@ WebSocketChannel::WebSocketChannel() :
|
||||
mStopped(0),
|
||||
mCalledOnStop(0),
|
||||
mPingOutstanding(0),
|
||||
mAllowCompression(1),
|
||||
mAutoFollowRedirects(0),
|
||||
mReleaseOnTransmit(0),
|
||||
mTCPClosed(0),
|
||||
@ -1004,6 +1103,7 @@ WebSocketChannel::WebSocketChannel() :
|
||||
mDataStarted(0),
|
||||
mIncrementedSessionCount(0),
|
||||
mDecrementedSessionCount(0),
|
||||
mAllowPMCE(1),
|
||||
mMaxMessageSize(INT32_MAX),
|
||||
mStopOnClose(NS_OK),
|
||||
mServerCloseCode(CLOSE_ABNORMAL),
|
||||
@ -1014,7 +1114,6 @@ WebSocketChannel::WebSocketChannel() :
|
||||
mBufferSize(kIncomingBufferInitialSize),
|
||||
mCurrentOut(nullptr),
|
||||
mCurrentOutSent(0),
|
||||
mCompressor(nullptr),
|
||||
mDynamicOutputSize(0),
|
||||
mDynamicOutput(nullptr),
|
||||
mPrivateBrowsing(false),
|
||||
@ -1052,7 +1151,6 @@ WebSocketChannel::~WebSocketChannel()
|
||||
|
||||
moz_free(mBuffer);
|
||||
moz_free(mDynamicOutput);
|
||||
delete mCompressor;
|
||||
delete mCurrentOut;
|
||||
|
||||
while ((mCurrentOut = (OutboundMessage *) mOutgoingPingMessages.PopFront()))
|
||||
@ -1315,6 +1413,8 @@ WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
|
||||
LOG(("WebSocketChannel::ProcessInput %p [%d %d]\n", this, count, mBuffered));
|
||||
NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "not socket thread");
|
||||
|
||||
nsresult rv;
|
||||
|
||||
// The purpose of ping/pong is to actively probe the peer so that an
|
||||
// unreachable peer is not mistaken for a period of idleness. This
|
||||
// implementation accepts any application level read activity as a sign of
|
||||
@ -1340,7 +1440,7 @@ WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
|
||||
while (avail >= 2) {
|
||||
int64_t payloadLength64 = mFramePtr[1] & 0x7F;
|
||||
uint8_t finBit = mFramePtr[0] & kFinalFragBit;
|
||||
uint8_t rsvBits = mFramePtr[0] & 0x70;
|
||||
uint8_t rsvBits = mFramePtr[0] & kRsvBitsMask;
|
||||
uint8_t maskBit = mFramePtr[1] & kMaskBit;
|
||||
uint8_t opcode = mFramePtr[0] & 0x0F;
|
||||
|
||||
@ -1408,8 +1508,17 @@ WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
|
||||
}
|
||||
|
||||
if (rsvBits) {
|
||||
LOG(("WebSocketChannel:: unexpected reserved bits %x\n", rsvBits));
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
// PMCE sets RSV1 bit in the first fragment when the non-control frame
|
||||
// is deflated
|
||||
if (mPMCECompressor && rsvBits == kRsv1Bit && mFragmentAccumulator == 0 &&
|
||||
!(opcode & kControlFrameMask)) {
|
||||
mPMCECompressor->SetMessageDeflated();
|
||||
LOG(("WebSocketChannel::ProcessInput: received deflated frame\n"));
|
||||
} else {
|
||||
LOG(("WebSocketChannel::ProcessInput: unexpected reserved bits %x\n",
|
||||
rsvBits));
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finBit || opcode == kContinuation) {
|
||||
@ -1473,12 +1582,27 @@ WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
|
||||
LOG(("WebSocketChannel:: ignoring read frame code %d after completion\n",
|
||||
opcode));
|
||||
} else if (opcode == kText) {
|
||||
LOG(("WebSocketChannel:: text frame received\n"));
|
||||
bool isDeflated = mPMCECompressor && mPMCECompressor->IsMessageDeflated();
|
||||
LOG(("WebSocketChannel:: %stext frame received\n",
|
||||
isDeflated ? "deflated " : ""));
|
||||
|
||||
if (mListener) {
|
||||
nsCString utf8Data;
|
||||
if (!utf8Data.Assign((const char *)payload, payloadLength,
|
||||
mozilla::fallible_t()))
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
if (isDeflated) {
|
||||
rv = mPMCECompressor->Inflate(payload, payloadLength, utf8Data);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
LOG(("WebSocketChannel:: message successfully inflated "
|
||||
"[origLength=%d, newLength=%d]\n", payloadLength,
|
||||
utf8Data.Length()));
|
||||
} else {
|
||||
if (!utf8Data.Assign((const char *)payload, payloadLength,
|
||||
mozilla::fallible_t())) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
// Section 8.1 says to fail connection if invalid utf-8 in text message
|
||||
if (!IsUTF8(utf8Data, false)) {
|
||||
@ -1568,12 +1692,31 @@ WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
|
||||
payloadLength = 0;
|
||||
}
|
||||
} else if (opcode == kBinary) {
|
||||
LOG(("WebSocketChannel:: binary frame received\n"));
|
||||
bool isDeflated = mPMCECompressor && mPMCECompressor->IsMessageDeflated();
|
||||
LOG(("WebSocketChannel:: %sbinary frame received\n",
|
||||
isDeflated ? "deflated " : ""));
|
||||
|
||||
if (mListener) {
|
||||
nsCString binaryData((const char *)payload, payloadLength);
|
||||
mTargetThread->Dispatch(new CallOnMessageAvailable(this, binaryData,
|
||||
payloadLength),
|
||||
NS_DISPATCH_NORMAL);
|
||||
nsCString binaryData;
|
||||
|
||||
if (isDeflated) {
|
||||
rv = mPMCECompressor->Inflate(payload, payloadLength, binaryData);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
LOG(("WebSocketChannel:: message successfully inflated "
|
||||
"[origLength=%d, newLength=%d]\n", payloadLength,
|
||||
binaryData.Length()));
|
||||
} else {
|
||||
if (!binaryData.Assign((const char *)payload, payloadLength,
|
||||
mozilla::fallible_t())) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
mTargetThread->Dispatch(
|
||||
new CallOnMessageAvailable(this, binaryData, binaryData.Length()),
|
||||
NS_DISPATCH_NORMAL);
|
||||
// To add the header to 'Networking Dashboard' log
|
||||
if (mConnectionLogService && !mPrivateBrowsing) {
|
||||
mConnectionLogService->NewMsgReceived(mHost, mSerial, count);
|
||||
@ -1858,6 +2001,19 @@ WebSocketChannel::PrimeNewOutgoingMessage()
|
||||
break;
|
||||
}
|
||||
|
||||
// deflate the payload if PMCE is negotiated
|
||||
if (mPMCECompressor &&
|
||||
(msgType == kMsgTypeString || msgType == kMsgTypeBinaryString)) {
|
||||
if (mCurrentOut->DeflatePayload(mPMCECompressor)) {
|
||||
// The payload was deflated successfully, set RSV1 bit
|
||||
mOutHeader[0] |= kRsv1Bit;
|
||||
|
||||
LOG(("WebSocketChannel::PrimeNewOutgoingMessage %p current msg %p was "
|
||||
"deflated [origLength=%d, newLength=%d].\n", this, mCurrentOut,
|
||||
mCurrentOut->OrigLength(), mCurrentOut->Length()));
|
||||
}
|
||||
}
|
||||
|
||||
if (mCurrentOut->Length() < 126) {
|
||||
mOutHeader[1] = mCurrentOut->Length() | kMaskBit;
|
||||
mHdrOutToSend = 6;
|
||||
@ -1919,22 +2075,6 @@ WebSocketChannel::PrimeNewOutgoingMessage()
|
||||
mCurrentOutSent = len;
|
||||
}
|
||||
|
||||
if (len && mCompressor) {
|
||||
// assume a 1/3 reduction in size for sizing the buffer
|
||||
// the buffer is used multiple times if necessary
|
||||
uint32_t currentHeaderSize = mHdrOutToSend;
|
||||
mHdrOutToSend = 0;
|
||||
|
||||
EnsureHdrOut(32 + (currentHeaderSize + len - mCurrentOutSent) / 2 * 3);
|
||||
mCompressor->Deflate(mOutHeader, currentHeaderSize,
|
||||
mCurrentOut->BeginReading() + mCurrentOutSent,
|
||||
len - mCurrentOutSent);
|
||||
|
||||
// All of the compressed data now resides in {mHdrOut, mHdrOutToSend}
|
||||
// so do not send the body again
|
||||
mCurrentOutSent = len;
|
||||
}
|
||||
|
||||
// Transmitting begins - mHdrOutToSend bytes from mOutHeader and
|
||||
// mCurrentOut->Length() bytes from mCurrentOut. The latter may be
|
||||
// coaleseced into the former for small messages or as the result of the
|
||||
@ -2121,11 +2261,7 @@ WebSocketChannel::StopSession(nsresult reason)
|
||||
mCancelable = nullptr;
|
||||
}
|
||||
|
||||
mInflateReader = nullptr;
|
||||
mInflateStream = nullptr;
|
||||
|
||||
delete mCompressor;
|
||||
mCompressor = nullptr;
|
||||
mPMCECompressor = nullptr;
|
||||
|
||||
if (!mCalledOnStop) {
|
||||
mCalledOnStop = 1;
|
||||
@ -2218,55 +2354,71 @@ WebSocketChannel::HandleExtensions()
|
||||
rv = mHttpChannel->GetResponseHeader(
|
||||
NS_LITERAL_CSTRING("Sec-WebSocket-Extensions"), extensions);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
LOG(("WebSocketChannel::HandleExtensions: received "
|
||||
"Sec-WebSocket-Extensions header: %s\n", extensions.get()));
|
||||
|
||||
extensions.CompressWhitespace();
|
||||
|
||||
if (!extensions.IsEmpty()) {
|
||||
if (!extensions.EqualsLiteral("deflate-stream")) {
|
||||
LOG(("WebSocketChannel::OnStartRequest: "
|
||||
"HTTP Sec-WebSocket-Exensions negotiated unknown value %s\n",
|
||||
if (StringBeginsWith(extensions,
|
||||
NS_LITERAL_CSTRING("permessage-deflate"))) {
|
||||
if (!mAllowPMCE) {
|
||||
LOG(("WebSocketChannel::HandleExtensions: "
|
||||
"Recvd permessage-deflate which wasn't offered\n"));
|
||||
AbortSession(NS_ERROR_ILLEGAL_VALUE);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
nsAutoCString param;
|
||||
|
||||
int32_t delimPos = extensions.FindChar(';');
|
||||
if (delimPos != kNotFound) {
|
||||
param = Substring(extensions, delimPos + 1);
|
||||
param.CompressWhitespace(true, false);
|
||||
extensions.Truncate(delimPos);
|
||||
extensions.CompressWhitespace(false, true);
|
||||
}
|
||||
|
||||
if (!extensions.EqualsLiteral("permessage-deflate")) {
|
||||
LOG(("WebSocketChannel::HandleExtensions: "
|
||||
"HTTP Sec-WebSocket-Extensions negotiated unknown value %s\n",
|
||||
extensions.get()));
|
||||
AbortSession(NS_ERROR_ILLEGAL_VALUE);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
bool noContextTakeover = false;
|
||||
if (!param.IsEmpty()) {
|
||||
if (param.EqualsLiteral("client_no_context_takeover")) {
|
||||
noContextTakeover = true;
|
||||
} else {
|
||||
LOG(("WebSocketChannel::HandleExtensions: "
|
||||
"HTTP permessage-deflate extension negotiated unknown "
|
||||
"parameter %s\n", param.get()));
|
||||
AbortSession(NS_ERROR_ILLEGAL_VALUE);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
mPMCECompressor = new PMCECompression(noContextTakeover);
|
||||
if (mPMCECompressor->Active()) {
|
||||
LOG(("WebSocketChannel::HandleExtensions: PMCE negotiated, %susing "
|
||||
"context takeover\n", noContextTakeover ? "NOT " : ""));
|
||||
} else {
|
||||
LOG(("WebSocketChannel::HandleExtensions: Cannot init PMCE "
|
||||
"compression object\n"));
|
||||
mPMCECompressor = nullptr;
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
} else {
|
||||
LOG(("WebSocketChannel::HandleExtensions: "
|
||||
"HTTP Sec-WebSocket-Extensions negotiated unknown value %s\n",
|
||||
extensions.get()));
|
||||
AbortSession(NS_ERROR_ILLEGAL_VALUE);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
if (!mAllowCompression) {
|
||||
LOG(("WebSocketChannel::HandleExtensions: "
|
||||
"Recvd Compression Extension that wasn't offered\n"));
|
||||
AbortSession(NS_ERROR_ILLEGAL_VALUE);
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIStreamConverterService> serv =
|
||||
do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("WebSocketChannel:: Cannot find compression service\n"));
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
rv = serv->AsyncConvertData("deflate", "uncompressed", this, nullptr,
|
||||
getter_AddRefs(mInflateReader));
|
||||
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("WebSocketChannel:: Cannot find inflate listener\n"));
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
mInflateStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv);
|
||||
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("WebSocketChannel:: Cannot find inflate stream\n"));
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
mCompressor = new nsWSCompression(this, mSocketOut);
|
||||
if (!mCompressor->Active()) {
|
||||
LOG(("WebSocketChannel:: Cannot init deflate object\n"));
|
||||
delete mCompressor;
|
||||
mCompressor = nullptr;
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
mNegotiatedExtensions = extensions;
|
||||
}
|
||||
}
|
||||
@ -2316,9 +2468,9 @@ WebSocketChannel::SetupRequest()
|
||||
mHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Sec-WebSocket-Protocol"),
|
||||
mProtocol, true);
|
||||
|
||||
if (mAllowCompression)
|
||||
if (mAllowPMCE)
|
||||
mHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Sec-WebSocket-Extensions"),
|
||||
NS_LITERAL_CSTRING("deflate-stream"),
|
||||
NS_LITERAL_CSTRING("permessage-deflate"),
|
||||
false);
|
||||
|
||||
uint8_t *secKey;
|
||||
@ -2843,10 +2995,10 @@ WebSocketChannel::AsyncOpen(nsIURI *aURI,
|
||||
if (NS_SUCCEEDED(rv) && !mClientSetPingTimeout) {
|
||||
mPingResponseTimeout = clamped(intpref, 1, 3600) * 1000;
|
||||
}
|
||||
rv = prefService->GetBoolPref("network.websocket.extensions.stream-deflate",
|
||||
rv = prefService->GetBoolPref("network.websocket.extensions.permessage-deflate",
|
||||
&boolpref);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
mAllowCompression = boolpref ? 1 : 0;
|
||||
mAllowPMCE = boolpref ? 1 : 0;
|
||||
}
|
||||
rv = prefService->GetBoolPref("network.websocket.auto-follow-http-redirects",
|
||||
&boolpref);
|
||||
@ -3293,11 +3445,8 @@ WebSocketChannel::OnInputStreamReady(nsIAsyncInputStream *aStream)
|
||||
|
||||
if (!mSocketIn) // did we we clean up the socket after scheduling InputReady?
|
||||
return NS_OK;
|
||||
|
||||
nsRefPtr<nsIStreamListener> deleteProtector1(mInflateReader);
|
||||
nsRefPtr<nsIStringInputStream> deleteProtector2(mInflateStream);
|
||||
|
||||
// this is after the http upgrade - so we are speaking websockets
|
||||
// this is after the http upgrade - so we are speaking websockets
|
||||
char buffer[2048];
|
||||
uint32_t count;
|
||||
nsresult rv;
|
||||
@ -3330,14 +3479,7 @@ WebSocketChannel::OnInputStreamReady(nsIAsyncInputStream *aStream)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mInflateReader) {
|
||||
mInflateStream->ShareData(buffer, count);
|
||||
rv = mInflateReader->OnDataAvailable(nullptr, mSocketIn, mInflateStream,
|
||||
0, count);
|
||||
} else {
|
||||
rv = ProcessInput((uint8_t *)buffer, count);
|
||||
}
|
||||
|
||||
rv = ProcessInput((uint8_t *)buffer, count);
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortSession(rv);
|
||||
return rv;
|
||||
@ -3412,9 +3554,9 @@ WebSocketChannel::OnOutputStreamReady(nsIAsyncOutputStream *aStream)
|
||||
} else {
|
||||
if (amtSent == toSend) {
|
||||
if (!mStopped) {
|
||||
mTargetThread->Dispatch(new CallAcknowledge(this,
|
||||
mCurrentOut->Length()),
|
||||
NS_DISPATCH_NORMAL);
|
||||
mTargetThread->Dispatch(
|
||||
new CallAcknowledge(this, mCurrentOut->OrigLength()),
|
||||
NS_DISPATCH_NORMAL);
|
||||
}
|
||||
DeleteCurrentOutGoingMessage();
|
||||
PrimeNewOutgoingMessage();
|
||||
@ -3442,74 +3584,10 @@ WebSocketChannel::OnDataAvailable(nsIRequest *aRequest,
|
||||
LOG(("WebSocketChannel::OnDataAvailable() %p [%p %p %p %llu %u]\n",
|
||||
this, aRequest, aContext, aInputStream, aOffset, aCount));
|
||||
|
||||
if (aContext == mSocketIn) {
|
||||
// This is the deflate decoder
|
||||
|
||||
LOG(("WebSocketChannel::OnDataAvailable: Deflate Data %u\n",
|
||||
aCount));
|
||||
|
||||
uint8_t buffer[2048];
|
||||
uint32_t maxRead;
|
||||
uint32_t count;
|
||||
nsresult rv = NS_OK; // aCount always > 0, so this just avoids warning
|
||||
|
||||
while (aCount > 0) {
|
||||
if (mStopped)
|
||||
return NS_BASE_STREAM_CLOSED;
|
||||
|
||||
maxRead = std::min(2048U, aCount);
|
||||
rv = aInputStream->Read((char *)buffer, maxRead, &count);
|
||||
LOG(("WebSocketChannel::OnDataAvailable: InflateRead read %u rv %x\n",
|
||||
count, rv));
|
||||
if (NS_FAILED(rv) || count == 0) {
|
||||
AbortSession(NS_ERROR_UNEXPECTED);
|
||||
break;
|
||||
}
|
||||
|
||||
aCount -= count;
|
||||
rv = ProcessInput(buffer, count);
|
||||
if (NS_FAILED(rv)) {
|
||||
AbortSession(rv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (aContext == mSocketOut) {
|
||||
// This is the deflate encoder
|
||||
|
||||
uint32_t maxRead;
|
||||
uint32_t count;
|
||||
nsresult rv;
|
||||
|
||||
while (aCount > 0) {
|
||||
if (mStopped)
|
||||
return NS_BASE_STREAM_CLOSED;
|
||||
|
||||
maxRead = std::min(2048U, aCount);
|
||||
EnsureHdrOut(mHdrOutToSend + aCount);
|
||||
rv = aInputStream->Read((char *)mHdrOut + mHdrOutToSend, maxRead, &count);
|
||||
LOG(("WebSocketChannel::OnDataAvailable: DeflateWrite read %u rv %x\n",
|
||||
count, rv));
|
||||
if (NS_FAILED(rv) || count == 0) {
|
||||
AbortSession(rv);
|
||||
break;
|
||||
}
|
||||
|
||||
mHdrOutToSend += count;
|
||||
aCount -= count;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, this is the HTTP OnDataAvailable Method, which means
|
||||
// this is http data in response to the upgrade request and
|
||||
// there should be no http response body if the upgrade succeeded
|
||||
|
||||
// This generally should be caught by a non 101 response code in
|
||||
// OnStartRequest().. so we can ignore the data here
|
||||
// This is the HTTP OnDataAvailable Method, which means this is http data in
|
||||
// response to the upgrade request and there should be no http response body
|
||||
// if the upgrade succeeded. This generally should be caught by a non 101
|
||||
// response code in OnStartRequest().. so we can ignore the data here
|
||||
|
||||
LOG(("WebSocketChannel::OnDataAvailable: HTTP data unexpected len>=%u\n",
|
||||
aCount));
|
||||
|
@ -43,7 +43,7 @@ namespace mozilla { namespace net {
|
||||
class OutboundMessage;
|
||||
class OutboundEnqueuer;
|
||||
class nsWSAdmissionManager;
|
||||
class nsWSCompression;
|
||||
class PMCECompression;
|
||||
class CallOnMessageAvailable;
|
||||
class CallOnStop;
|
||||
class CallOnServerClose;
|
||||
@ -118,6 +118,8 @@ public:
|
||||
const static uint32_t kControlFrameMask = 0x8;
|
||||
const static uint8_t kMaskBit = 0x80;
|
||||
const static uint8_t kFinalFragBit = 0x80;
|
||||
const static uint8_t kRsvBitsMask = 0x70;
|
||||
const static uint8_t kRsv1Bit = 0x40;
|
||||
|
||||
protected:
|
||||
virtual ~WebSocketChannel();
|
||||
@ -221,7 +223,6 @@ private:
|
||||
uint32_t mStopped : 1;
|
||||
uint32_t mCalledOnStop : 1;
|
||||
uint32_t mPingOutstanding : 1;
|
||||
uint32_t mAllowCompression : 1;
|
||||
uint32_t mAutoFollowRedirects : 1;
|
||||
uint32_t mReleaseOnTransmit : 1;
|
||||
uint32_t mTCPClosed : 1;
|
||||
@ -229,6 +230,7 @@ private:
|
||||
uint32_t mDataStarted : 1;
|
||||
uint32_t mIncrementedSessionCount : 1;
|
||||
uint32_t mDecrementedSessionCount : 1;
|
||||
uint32_t mAllowPMCE : 1;
|
||||
|
||||
int32_t mMaxMessageSize;
|
||||
nsresult mStopOnClose;
|
||||
@ -250,8 +252,6 @@ private:
|
||||
uint32_t mFragmentAccumulator;
|
||||
uint32_t mBuffered;
|
||||
uint32_t mBufferSize;
|
||||
nsCOMPtr<nsIStreamListener> mInflateReader;
|
||||
nsCOMPtr<nsIStringInputStream> mInflateStream;
|
||||
|
||||
// These are for the send buffers
|
||||
const static int32_t kCopyBreak = 1000;
|
||||
@ -264,7 +264,7 @@ private:
|
||||
uint32_t mHdrOutToSend;
|
||||
uint8_t *mHdrOut;
|
||||
uint8_t mOutHeader[kCopyBreak + 16];
|
||||
nsWSCompression *mCompressor;
|
||||
nsAutoPtr<PMCECompression> mPMCECompressor;
|
||||
uint32_t mDynamicOutputSize;
|
||||
uint8_t *mDynamicOutput;
|
||||
bool mPrivateBrowsing;
|
||||
|
@ -87,18 +87,20 @@ TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket += [
|
||||
'pywebsocket/mod_pywebsocket/common.py',
|
||||
'pywebsocket/mod_pywebsocket/dispatch.py',
|
||||
'pywebsocket/mod_pywebsocket/extensions.py',
|
||||
'pywebsocket/mod_pywebsocket/fast_masking.i',
|
||||
'pywebsocket/mod_pywebsocket/headerparserhandler.py',
|
||||
'pywebsocket/mod_pywebsocket/http_header_util.py',
|
||||
'pywebsocket/mod_pywebsocket/memorizingfile.py',
|
||||
'pywebsocket/mod_pywebsocket/msgutil.py',
|
||||
'pywebsocket/mod_pywebsocket/mux.py',
|
||||
'pywebsocket/mod_pywebsocket/stream.py',
|
||||
'pywebsocket/mod_pywebsocket/util.py',
|
||||
'pywebsocket/mod_pywebsocket/xhr_benchmark_handler.py',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket.handshake += [
|
||||
'pywebsocket/mod_pywebsocket/handshake/__init__.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/_base.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/draft75.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/hybi.py',
|
||||
'pywebsocket/mod_pywebsocket/handshake/hybi00.py',
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright 2009, Google Inc.
|
||||
Copyright 2012, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -1,9 +1,17 @@
|
||||
Install this package by:
|
||||
INSTALL
|
||||
|
||||
To install this package to the system, run this:
|
||||
$ python setup.py build
|
||||
$ sudo python setup.py install
|
||||
|
||||
If you're going to use this package as a normal user, run this instead:
|
||||
To install this package as a normal user, run this instead:
|
||||
$ python setup.py build
|
||||
$ python setup.py install --user
|
||||
|
||||
Then read document by:
|
||||
LAUNCH
|
||||
|
||||
To use pywebsocket as Apache module, run this to read the document:
|
||||
$ pydoc mod_pywebsocket
|
||||
|
||||
To use pywebsocket as standalone server, run this to read the document:
|
||||
$ pydoc mod_pywebsocket.standalone
|
||||
|
@ -4,7 +4,7 @@ This pywebsocket code is mostly unchanged from the source at
|
||||
|
||||
The current Mozilla code is based on
|
||||
|
||||
svnversion: 631 (supports RFC 6455)
|
||||
svnversion: 860 (supports RFC 6455, permessage compression extension)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
|
||||
@ -29,8 +29,8 @@ STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
|
||||
- Manually move the 'standalone.py' file from the mmod_pywebsocket/ directory to
|
||||
the parent directory (not sure why we moved it: probably no reason)
|
||||
|
||||
- hg add/rm appropriate files, and add/remove them from _MOD_PYWEBSOCKET_FILES
|
||||
and/or _HANDSHAKE_FILES in testing/mochitest/Makefile.am
|
||||
- hg add/rm appropriate files, and add/remove them from
|
||||
testing/mochitest/moz.build
|
||||
|
||||
- We need to apply the patch to hybi.py that makes HSTS work: (attached at end
|
||||
of this README)
|
||||
@ -52,12 +52,12 @@ PATCH TO hybi.py for HSTS support:
|
||||
diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
--- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
+++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
|
||||
@@ -227,16 +227,19 @@ class Handshaker(object):
|
||||
|
||||
def _check_version(self):
|
||||
unused_value = validate_mandatory_header(
|
||||
self._request, common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
str(common.VERSION_HYBI_LATEST), fail_status=426)
|
||||
@@ -299,16 +299,19 @@ class Handshaker(object):
|
||||
status=common.HTTP_STATUS_BAD_REQUEST)
|
||||
raise VersionException(
|
||||
'Unsupported version %r for header %s' %
|
||||
(version, common.SEC_WEBSOCKET_VERSION_HEADER),
|
||||
supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS)))
|
||||
|
||||
def _set_protocol(self):
|
||||
self._request.ws_protocol = None
|
||||
@ -68,11 +68,11 @@ diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/t
|
||||
protocol_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
|
||||
if not protocol_header:
|
||||
if protocol_header is None:
|
||||
self._request.ws_requested_protocols = None
|
||||
return
|
||||
|
||||
@@ -311,16 +314,21 @@ class Handshaker(object):
|
||||
@@ -396,16 +399,21 @@ class Handshaker(object):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
|
||||
self._request.ws_protocol))
|
||||
@ -80,15 +80,17 @@ diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/t
|
||||
len(self._request.ws_extensions) != 0):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
format_extensions(self._request.ws_extensions)))
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
+ # MOZILLA: Add HSTS header if requested to
|
||||
+ if self._request.sts is not None:
|
||||
+ response.append(format_header("Strict-Transport-Security",
|
||||
+ self._request.sts))
|
||||
+ # /MOZILLA
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append('\r\n')
|
||||
|
||||
raw_response = ''.join(response)
|
||||
self._logger.debug('Opening handshake response: %r', raw_response)
|
||||
self._request.connection.write(raw_response)
|
||||
|
||||
return ''.join(response)
|
||||
|
@ -34,7 +34,8 @@ mod_pywebsocket is a WebSocket extension for Apache HTTP Server
|
||||
intended for testing or experimental purposes. mod_python is required.
|
||||
|
||||
|
||||
Installation:
|
||||
Installation
|
||||
============
|
||||
|
||||
0. Prepare an Apache HTTP Server for which mod_python is enabled.
|
||||
|
||||
@ -60,11 +61,6 @@ Installation:
|
||||
<scan_dir> is useful in saving scan time when <websock_handlers>
|
||||
contains many non-WebSocket handler files.
|
||||
|
||||
If you want to support old handshake based on
|
||||
draft-hixie-thewebsocketprotocol-75:
|
||||
|
||||
PythonOption mod_pywebsocket.allow_draft75 On
|
||||
|
||||
If you want to allow handlers whose canonical path is not under the root
|
||||
directory (i.e. symbolic link is in root directory but its target is not),
|
||||
configure as follows:
|
||||
@ -89,7 +85,8 @@ Installation:
|
||||
3. Verify installation. You can use example/console.html to poke the server.
|
||||
|
||||
|
||||
Writing WebSocket handlers:
|
||||
Writing WebSocket handlers
|
||||
==========================
|
||||
|
||||
When a WebSocket request comes in, the resource name
|
||||
specified in the handshake is considered as if it is a file path under
|
||||
@ -118,28 +115,36 @@ extra handshake (web_socket_do_extra_handshake):
|
||||
- ws_resource
|
||||
- ws_origin
|
||||
- ws_version
|
||||
- ws_location (Hixie 75 and HyBi 00 only)
|
||||
- ws_extensions (Hybi 06 and later)
|
||||
- ws_location (HyBi 00 only)
|
||||
- ws_extensions (HyBi 06 and later)
|
||||
- ws_deflate (HyBi 06 and later)
|
||||
- ws_protocol
|
||||
- ws_requested_protocols (HyBi 06 and later)
|
||||
|
||||
The last two are a bit tricky.
|
||||
The last two are a bit tricky. See the next subsection.
|
||||
|
||||
|
||||
Subprotocol Negotiation
|
||||
-----------------------
|
||||
|
||||
For HyBi 06 and later, ws_protocol is always set to None when
|
||||
web_socket_do_extra_handshake is called. If ws_requested_protocols is not
|
||||
None, you must choose one subprotocol from this list and set it to
|
||||
ws_protocol.
|
||||
|
||||
For Hixie 75 and HyBi 00, when web_socket_do_extra_handshake is called,
|
||||
For HyBi 00, when web_socket_do_extra_handshake is called,
|
||||
ws_protocol is set to the value given by the client in
|
||||
Sec-WebSocket-Protocol (WebSocket-Protocol for Hixie 75) header or None if
|
||||
Sec-WebSocket-Protocol header or None if
|
||||
such header was not found in the opening handshake request. Finish extra
|
||||
handshake with ws_protocol untouched to accept the request subprotocol.
|
||||
Then, Sec-WebSocket-Protocol (or WebSocket-Protocol) header will be sent to
|
||||
Then, Sec-WebSocket-Protocol header will be sent to
|
||||
the client in response with the same value as requested. Raise an exception
|
||||
in web_socket_do_extra_handshake to reject the requested subprotocol.
|
||||
|
||||
|
||||
Data Transfer
|
||||
-------------
|
||||
|
||||
web_socket_transfer_data is called after the handshake completed
|
||||
successfully. A handler can receive/send messages from/to the client
|
||||
using request. mod_pywebsocket.msgutil module provides utilities
|
||||
@ -159,12 +164,16 @@ You can send a message by the following statement.
|
||||
|
||||
request.ws_stream.send_message(message)
|
||||
|
||||
|
||||
Closing Connection
|
||||
------------------
|
||||
|
||||
Executing the following statement or just return-ing from
|
||||
web_socket_transfer_data cause connection close.
|
||||
|
||||
request.ws_stream.close_connection()
|
||||
|
||||
When you're using IETF HyBi 00 or later protocol, close_connection will wait
|
||||
close_connection will wait
|
||||
for closing handshake acknowledgement coming from the client. When it
|
||||
couldn't receive a valid acknowledgement, raises an exception.
|
||||
|
||||
@ -176,8 +185,39 @@ use in web_socket_passive_closing_handshake.
|
||||
- ws_close_code
|
||||
- ws_close_reason
|
||||
|
||||
|
||||
Threading
|
||||
---------
|
||||
|
||||
A WebSocket handler must be thread-safe if the server (Apache or
|
||||
standalone.py) is configured to use threads.
|
||||
|
||||
|
||||
Configuring WebSocket Extension Processors
|
||||
------------------------------------------
|
||||
|
||||
See extensions.py for supported WebSocket extensions. Note that they are
|
||||
unstable and their APIs are subject to change substantially.
|
||||
|
||||
A request object has these extension processing related attributes.
|
||||
|
||||
- ws_requested_extensions:
|
||||
|
||||
A list of common.ExtensionParameter instances representing extension
|
||||
parameters received from the client in the client's opening handshake.
|
||||
You shouldn't modify it manually.
|
||||
|
||||
- ws_extensions:
|
||||
|
||||
A list of common.ExtensionParameter instances representing extension
|
||||
parameters to send back to the client in the server's opening handshake.
|
||||
You shouldn't touch it directly. Instead, call methods on extension
|
||||
processors.
|
||||
|
||||
- ws_extension_processors:
|
||||
|
||||
A list of loaded extension processors. Find the processor for the
|
||||
extension you want to configure from it, and call its methods.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -39,6 +39,8 @@
|
||||
# writing/reading.
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
@ -109,20 +111,34 @@ class StreamBase(object):
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
bytes = self._request.connection.read(length)
|
||||
if not bytes:
|
||||
try:
|
||||
read_bytes = self._request.connection.read(length)
|
||||
if not read_bytes:
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. Peer (%r) closed connection' %
|
||||
(length, (self._request.connection.remote_addr,)))
|
||||
return read_bytes
|
||||
except socket.error, e:
|
||||
# Catch a socket.error. Because it's not a child class of the
|
||||
# IOError prior to Python 2.6, we cannot omit this except clause.
|
||||
# Use %s rather than %r for the exception to use human friendly
|
||||
# format.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. Peer (%r) closed connection' %
|
||||
(length, (self._request.connection.remote_addr,)))
|
||||
return bytes
|
||||
'Receiving %d byte failed. socket.error (%s) occurred' %
|
||||
(length, e))
|
||||
except IOError, e:
|
||||
# Also catch an IOError because mod_python throws it.
|
||||
raise ConnectionTerminatedException(
|
||||
'Receiving %d byte failed. IOError (%s) occurred' %
|
||||
(length, e))
|
||||
|
||||
def _write(self, bytes):
|
||||
def _write(self, bytes_to_write):
|
||||
"""Writes given bytes to connection. In case we catch any exception,
|
||||
prepends remote address to the exception message and raise again.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._request.connection.write(bytes)
|
||||
self._request.connection.write(bytes_to_write)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
@ -138,12 +154,12 @@ class StreamBase(object):
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
bytes = []
|
||||
read_bytes = []
|
||||
while length > 0:
|
||||
new_bytes = self._read(length)
|
||||
bytes.append(new_bytes)
|
||||
length -= len(new_bytes)
|
||||
return ''.join(bytes)
|
||||
new_read_bytes = self._read(length)
|
||||
read_bytes.append(new_read_bytes)
|
||||
length -= len(new_read_bytes)
|
||||
return ''.join(read_bytes)
|
||||
|
||||
def _read_until(self, delim_char):
|
||||
"""Reads bytes until we encounter delim_char. The result will not
|
||||
@ -153,13 +169,13 @@ class StreamBase(object):
|
||||
ConnectionTerminatedException: when read returns empty string.
|
||||
"""
|
||||
|
||||
bytes = []
|
||||
read_bytes = []
|
||||
while True:
|
||||
ch = self._read(1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
bytes.append(ch)
|
||||
return ''.join(bytes)
|
||||
read_bytes.append(ch)
|
||||
return ''.join(read_bytes)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -32,7 +32,8 @@
|
||||
protocol version HyBi 00 and Hixie 75.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
|
||||
- HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
|
||||
- Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
|
||||
"""
|
||||
|
||||
|
||||
|
@ -37,8 +37,10 @@ http://tools.ietf.org/html/rfc6455
|
||||
|
||||
|
||||
from collections import deque
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
@ -161,14 +163,145 @@ def create_text_frame(
|
||||
frame_filters)
|
||||
|
||||
|
||||
def parse_frame(receive_bytes, logger=None,
|
||||
ws_version=common.VERSION_HYBI_LATEST,
|
||||
unmask_receive=True):
|
||||
"""Parses a frame. Returns a tuple containing each header field and
|
||||
payload.
|
||||
|
||||
Args:
|
||||
receive_bytes: a function that reads frame data from a stream or
|
||||
something similar. The function takes length of the bytes to be
|
||||
read. The function must raise ConnectionTerminatedException if
|
||||
there is not enough data to be read.
|
||||
logger: a logging object.
|
||||
ws_version: the version of WebSocket protocol.
|
||||
unmask_receive: unmask received frames. When received unmasked
|
||||
frame, raises InvalidFrameException.
|
||||
|
||||
Raises:
|
||||
ConnectionTerminatedException: when receive_bytes raises it.
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
|
||||
if not logger:
|
||||
logger = logging.getLogger()
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive the first 2 octets of a frame')
|
||||
|
||||
received = receive_bytes(2)
|
||||
|
||||
first_byte = ord(received[0])
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xf
|
||||
|
||||
second_byte = ord(received[1])
|
||||
mask = (second_byte >> 7) & 1
|
||||
payload_length = second_byte & 0x7f
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, '
|
||||
'Mask=%s, Payload_length=%s',
|
||||
fin, rsv1, rsv2, rsv3, opcode, mask, payload_length)
|
||||
|
||||
if (mask == 1) != unmask_receive:
|
||||
raise InvalidFrameException(
|
||||
'Mask bit on the received frame did\'nt match masking '
|
||||
'configuration for received frames')
|
||||
|
||||
# The HyBi and later specs disallow putting a value in 0x0-0xFFFF
|
||||
# into the 8-octet extended payload length field (or 0x0-0xFD in
|
||||
# 2-octet field).
|
||||
valid_length_encoding = True
|
||||
length_encoding_bytes = 1
|
||||
if payload_length == 127:
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Receive 8-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(8)
|
||||
payload_length = struct.unpack(
|
||||
'!Q', extended_payload_length)[0]
|
||||
if payload_length > 0x7FFFFFFFFFFFFFFF:
|
||||
raise InvalidFrameException(
|
||||
'Extended payload length >= 2^63')
|
||||
if ws_version >= 13 and payload_length < 0x10000:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 8
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Decoded_payload_length=%s', payload_length)
|
||||
elif payload_length == 126:
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Receive 2-octet extended payload length')
|
||||
|
||||
extended_payload_length = receive_bytes(2)
|
||||
payload_length = struct.unpack(
|
||||
'!H', extended_payload_length)[0]
|
||||
if ws_version >= 13 and payload_length < 126:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 2
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE,
|
||||
'Decoded_payload_length=%s', payload_length)
|
||||
|
||||
if not valid_length_encoding:
|
||||
logger.warning(
|
||||
'Payload length is not encoded using the minimal number of '
|
||||
'bytes (%d is encoded using %d bytes)',
|
||||
payload_length,
|
||||
length_encoding_bytes)
|
||||
|
||||
if mask == 1:
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive mask')
|
||||
|
||||
masking_nonce = receive_bytes(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce)
|
||||
else:
|
||||
masker = _NOOP_MASKER
|
||||
|
||||
logger.log(common.LOGLEVEL_FINE, 'Receive payload data')
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
receive_start = time.time()
|
||||
|
||||
raw_payload_bytes = receive_bytes(payload_length)
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE,
|
||||
'Done receiving payload data at %s MB/s',
|
||||
payload_length / (time.time() - receive_start) / 1000 / 1000)
|
||||
logger.log(common.LOGLEVEL_FINE, 'Unmask payload data')
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
unmask_start = time.time()
|
||||
|
||||
unmasked_bytes = masker.mask(raw_payload_bytes)
|
||||
|
||||
if logger.isEnabledFor(common.LOGLEVEL_FINE):
|
||||
logger.log(
|
||||
common.LOGLEVEL_FINE,
|
||||
'Done unmasking payload data at %s MB/s',
|
||||
payload_length / (time.time() - unmask_start) / 1000 / 1000)
|
||||
|
||||
return opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3
|
||||
|
||||
|
||||
class FragmentedFrameBuilder(object):
|
||||
"""A stateful class to send a message as fragments."""
|
||||
|
||||
def __init__(self, mask, frame_filters=[]):
|
||||
def __init__(self, mask, frame_filters=[], encode_utf8=True):
|
||||
"""Constructs an instance."""
|
||||
|
||||
self._mask = mask
|
||||
self._frame_filters = frame_filters
|
||||
# This is for skipping UTF-8 encoding when building text type frames
|
||||
# from compressed data.
|
||||
self._encode_utf8 = encode_utf8
|
||||
|
||||
self._started = False
|
||||
|
||||
@ -176,7 +309,7 @@ class FragmentedFrameBuilder(object):
|
||||
# frames in the message are all the same.
|
||||
self._opcode = common.OPCODE_TEXT
|
||||
|
||||
def build(self, message, end, binary):
|
||||
def build(self, payload_data, end, binary):
|
||||
if binary:
|
||||
frame_type = common.OPCODE_BINARY
|
||||
else:
|
||||
@ -197,18 +330,28 @@ class FragmentedFrameBuilder(object):
|
||||
self._started = True
|
||||
fin = 0
|
||||
|
||||
if binary:
|
||||
if binary or not self._encode_utf8:
|
||||
return create_binary_frame(
|
||||
message, opcode, fin, self._mask, self._frame_filters)
|
||||
payload_data, opcode, fin, self._mask, self._frame_filters)
|
||||
else:
|
||||
return create_text_frame(
|
||||
message, opcode, fin, self._mask, self._frame_filters)
|
||||
payload_data, opcode, fin, self._mask, self._frame_filters)
|
||||
|
||||
|
||||
def _create_control_frame(opcode, body, mask, frame_filters):
|
||||
frame = Frame(opcode=opcode, payload=body)
|
||||
|
||||
return _filter_and_format_frame_object(frame, mask, frame_filters)
|
||||
for frame_filter in frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
if len(frame.payload) > 125:
|
||||
raise BadOperationException(
|
||||
'Payload data size of control frames must be 125 bytes or less')
|
||||
|
||||
header = create_header(
|
||||
frame.opcode, len(frame.payload), frame.fin,
|
||||
frame.rsv1, frame.rsv2, frame.rsv3, mask)
|
||||
return _build_frame(header, frame.payload, mask)
|
||||
|
||||
|
||||
def create_ping_frame(body, mask=False, frame_filters=[]):
|
||||
@ -224,19 +367,37 @@ def create_close_frame(body, mask=False, frame_filters=[]):
|
||||
common.OPCODE_CLOSE, body, mask, frame_filters)
|
||||
|
||||
|
||||
def create_closing_handshake_body(code, reason):
|
||||
body = ''
|
||||
if code is not None:
|
||||
if (code > common.STATUS_USER_PRIVATE_MAX or
|
||||
code < common.STATUS_NORMAL_CLOSURE):
|
||||
raise BadOperationException('Status code is out of range')
|
||||
if (code == common.STATUS_NO_STATUS_RECEIVED or
|
||||
code == common.STATUS_ABNORMAL_CLOSURE or
|
||||
code == common.STATUS_TLS_HANDSHAKE):
|
||||
raise BadOperationException('Status code is reserved pseudo '
|
||||
'code')
|
||||
encoded_reason = reason.encode('utf-8')
|
||||
body = struct.pack('!H', code) + encoded_reason
|
||||
return body
|
||||
|
||||
|
||||
class StreamOptions(object):
|
||||
"""Holds option values to configure Stream objects."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs StreamOptions."""
|
||||
|
||||
# Enables deflate-stream extension.
|
||||
self.deflate_stream = False
|
||||
|
||||
# Filters applied to frames.
|
||||
self.outgoing_frame_filters = []
|
||||
self.incoming_frame_filters = []
|
||||
|
||||
# Filters applied to messages. Control frames are not affected by them.
|
||||
self.outgoing_message_filters = []
|
||||
self.incoming_message_filters = []
|
||||
|
||||
self.encode_text_message_to_utf8 = True
|
||||
self.mask_send = False
|
||||
self.unmask_receive = True
|
||||
|
||||
@ -259,10 +420,6 @@ class Stream(StreamBase):
|
||||
|
||||
self._options = options
|
||||
|
||||
if self._options.deflate_stream:
|
||||
self._logger.debug('Setup filter for deflate-stream')
|
||||
self._request = util.DeflateRequest(self._request)
|
||||
|
||||
self._request.client_terminated = False
|
||||
self._request.server_terminated = False
|
||||
|
||||
@ -272,7 +429,8 @@ class Stream(StreamBase):
|
||||
self._original_opcode = None
|
||||
|
||||
self._writer = FragmentedFrameBuilder(
|
||||
self._options.mask_send, self._options.outgoing_frame_filters)
|
||||
self._options.mask_send, self._options.outgoing_frame_filters,
|
||||
self._options.encode_text_message_to_utf8)
|
||||
|
||||
self._ping_queue = deque()
|
||||
|
||||
@ -286,69 +444,45 @@ class Stream(StreamBase):
|
||||
InvalidFrameException: when the frame contains invalid data.
|
||||
"""
|
||||
|
||||
received = self.receive_bytes(2)
|
||||
def _receive_bytes(length):
|
||||
return self.receive_bytes(length)
|
||||
|
||||
first_byte = ord(received[0])
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xf
|
||||
|
||||
second_byte = ord(received[1])
|
||||
mask = (second_byte >> 7) & 1
|
||||
payload_length = second_byte & 0x7f
|
||||
|
||||
if (mask == 1) != self._options.unmask_receive:
|
||||
raise InvalidFrameException(
|
||||
'Mask bit on the received frame did\'nt match masking '
|
||||
'configuration for received frames')
|
||||
|
||||
# The Hybi-13 and later specs disallow putting a value in 0x0-0xFFFF
|
||||
# into the 8-octet extended payload length field (or 0x0-0xFD in
|
||||
# 2-octet field).
|
||||
valid_length_encoding = True
|
||||
length_encoding_bytes = 1
|
||||
if payload_length == 127:
|
||||
extended_payload_length = self.receive_bytes(8)
|
||||
payload_length = struct.unpack(
|
||||
'!Q', extended_payload_length)[0]
|
||||
if payload_length > 0x7FFFFFFFFFFFFFFF:
|
||||
raise InvalidFrameException(
|
||||
'Extended payload length >= 2^63')
|
||||
if self._request.ws_version >= 13 and payload_length < 0x10000:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 8
|
||||
elif payload_length == 126:
|
||||
extended_payload_length = self.receive_bytes(2)
|
||||
payload_length = struct.unpack(
|
||||
'!H', extended_payload_length)[0]
|
||||
if self._request.ws_version >= 13 and payload_length < 126:
|
||||
valid_length_encoding = False
|
||||
length_encoding_bytes = 2
|
||||
|
||||
if not valid_length_encoding:
|
||||
self._logger.warning(
|
||||
'Payload length is not encoded using the minimal number of '
|
||||
'bytes (%d is encoded using %d bytes)',
|
||||
payload_length,
|
||||
length_encoding_bytes)
|
||||
|
||||
if mask == 1:
|
||||
masking_nonce = self.receive_bytes(4)
|
||||
masker = util.RepeatedXorMasker(masking_nonce)
|
||||
else:
|
||||
masker = _NOOP_MASKER
|
||||
|
||||
bytes = masker.mask(self.receive_bytes(payload_length))
|
||||
|
||||
return opcode, bytes, fin, rsv1, rsv2, rsv3
|
||||
return parse_frame(receive_bytes=_receive_bytes,
|
||||
logger=self._logger,
|
||||
ws_version=self._request.ws_version,
|
||||
unmask_receive=self._options.unmask_receive)
|
||||
|
||||
def _receive_frame_as_frame_object(self):
|
||||
opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
|
||||
opcode, unmasked_bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame()
|
||||
|
||||
return Frame(fin=fin, rsv1=rsv1, rsv2=rsv2, rsv3=rsv3,
|
||||
opcode=opcode, payload=bytes)
|
||||
opcode=opcode, payload=unmasked_bytes)
|
||||
|
||||
def receive_filtered_frame(self):
|
||||
"""Receives a frame and applies frame filters and message filters.
|
||||
The frame to be received must satisfy following conditions:
|
||||
- The frame is not fragmented.
|
||||
- The opcode of the frame is TEXT or BINARY.
|
||||
|
||||
DO NOT USE this method except for testing purpose.
|
||||
"""
|
||||
|
||||
frame = self._receive_frame_as_frame_object()
|
||||
if not frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Segmented frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
if (frame.opcode != common.OPCODE_TEXT and
|
||||
frame.opcode != common.OPCODE_BINARY):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be received via '
|
||||
'receive_filtered_frame()')
|
||||
|
||||
for frame_filter in self._options.incoming_frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
for message_filter in self._options.incoming_message_filters:
|
||||
frame.payload = message_filter.filter(frame.payload)
|
||||
return frame
|
||||
|
||||
def send_message(self, message, end=True, binary=False):
|
||||
"""Send message.
|
||||
@ -371,11 +505,219 @@ class Stream(StreamBase):
|
||||
raise BadOperationException(
|
||||
'Message for binary frame must be instance of str')
|
||||
|
||||
for message_filter in self._options.outgoing_message_filters:
|
||||
message = message_filter.filter(message, end, binary)
|
||||
|
||||
try:
|
||||
self._write(self._writer.build(message, end, binary))
|
||||
# Set this to any positive integer to limit maximum size of data in
|
||||
# payload data of each frame.
|
||||
MAX_PAYLOAD_DATA_SIZE = -1
|
||||
|
||||
if MAX_PAYLOAD_DATA_SIZE <= 0:
|
||||
self._write(self._writer.build(message, end, binary))
|
||||
return
|
||||
|
||||
bytes_written = 0
|
||||
while True:
|
||||
end_for_this_frame = end
|
||||
bytes_to_write = len(message) - bytes_written
|
||||
if (MAX_PAYLOAD_DATA_SIZE > 0 and
|
||||
bytes_to_write > MAX_PAYLOAD_DATA_SIZE):
|
||||
end_for_this_frame = False
|
||||
bytes_to_write = MAX_PAYLOAD_DATA_SIZE
|
||||
|
||||
frame = self._writer.build(
|
||||
message[bytes_written:bytes_written + bytes_to_write],
|
||||
end_for_this_frame,
|
||||
binary)
|
||||
self._write(frame)
|
||||
|
||||
bytes_written += bytes_to_write
|
||||
|
||||
# This if must be placed here (the end of while block) so that
|
||||
# at least one frame is sent.
|
||||
if len(message) <= bytes_written:
|
||||
break
|
||||
except ValueError, e:
|
||||
raise BadOperationException(e)
|
||||
|
||||
def _get_message_from_frame(self, frame):
|
||||
"""Gets a message from frame. If the message is composed of fragmented
|
||||
frames and the frame is not the last fragmented frame, this method
|
||||
returns None. The whole message will be returned when the last
|
||||
fragmented frame is passed to this method.
|
||||
|
||||
Raises:
|
||||
InvalidFrameException: when the frame doesn't match defragmentation
|
||||
context, or the frame contains invalid data.
|
||||
"""
|
||||
|
||||
if frame.opcode == common.OPCODE_CONTINUATION:
|
||||
if not self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received a termination frame but fragmentation '
|
||||
'not started')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'Received an intermediate frame but '
|
||||
'fragmentation not started')
|
||||
|
||||
if frame.fin:
|
||||
# End of fragmentation frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
message = ''.join(self._received_fragments)
|
||||
self._received_fragments = []
|
||||
return message
|
||||
else:
|
||||
# Intermediate frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
return None
|
||||
else:
|
||||
if self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received an unfragmented frame without '
|
||||
'terminating existing fragmentation')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'New fragmentation started without terminating '
|
||||
'existing fragmentation')
|
||||
|
||||
if frame.fin:
|
||||
# Unfragmented frame
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
return frame.payload
|
||||
else:
|
||||
# Start of fragmentation frame
|
||||
|
||||
if common.is_control_opcode(frame.opcode):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be fragmented')
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
self._received_fragments.append(frame.payload)
|
||||
return None
|
||||
|
||||
def _process_close_message(self, message):
|
||||
"""Processes close message.
|
||||
|
||||
Args:
|
||||
message: close message.
|
||||
|
||||
Raises:
|
||||
InvalidFrameException: when the message is invalid.
|
||||
"""
|
||||
|
||||
self._request.client_terminated = True
|
||||
|
||||
# Status code is optional. We can have status reason only if we
|
||||
# have status code. Status reason can be empty string. So,
|
||||
# allowed cases are
|
||||
# - no application data: no code no reason
|
||||
# - 2 octet of application data: has code but no reason
|
||||
# - 3 or more octet of application data: both code and reason
|
||||
if len(message) == 0:
|
||||
self._logger.debug('Received close frame (empty body)')
|
||||
self._request.ws_close_code = (
|
||||
common.STATUS_NO_STATUS_RECEIVED)
|
||||
elif len(message) == 1:
|
||||
raise InvalidFrameException(
|
||||
'If a close frame has status code, the length of '
|
||||
'status code must be 2 octet')
|
||||
elif len(message) >= 2:
|
||||
self._request.ws_close_code = struct.unpack(
|
||||
'!H', message[0:2])[0]
|
||||
self._request.ws_close_reason = message[2:].decode(
|
||||
'utf-8', 'replace')
|
||||
self._logger.debug(
|
||||
'Received close frame (code=%d, reason=%r)',
|
||||
self._request.ws_close_code,
|
||||
self._request.ws_close_reason)
|
||||
|
||||
# As we've received a close frame, no more data is coming over the
|
||||
# socket. We can now safely close the socket without worrying about
|
||||
# RST sending.
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing handshake')
|
||||
return
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
|
||||
code = common.STATUS_NORMAL_CLOSURE
|
||||
reason = ''
|
||||
if hasattr(self._request, '_dispatcher'):
|
||||
dispatcher = self._request._dispatcher
|
||||
code, reason = dispatcher.passive_closing_handshake(
|
||||
self._request)
|
||||
if code is None and reason is not None and len(reason) > 0:
|
||||
self._logger.warning(
|
||||
'Handler specified reason despite code being None')
|
||||
reason = ''
|
||||
if reason is None:
|
||||
reason = ''
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug(
|
||||
'Acknowledged closing handshake initiated by the peer '
|
||||
'(code=%r, reason=%r)', code, reason)
|
||||
|
||||
def _process_ping_message(self, message):
|
||||
"""Processes ping message.
|
||||
|
||||
Args:
|
||||
message: ping message.
|
||||
"""
|
||||
|
||||
try:
|
||||
handler = self._request.on_ping_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
return
|
||||
except AttributeError, e:
|
||||
pass
|
||||
self._send_pong(message)
|
||||
|
||||
def _process_pong_message(self, message):
|
||||
"""Processes pong message.
|
||||
|
||||
Args:
|
||||
message: pong message.
|
||||
"""
|
||||
|
||||
# TODO(tyoshino): Add ping timeout handling.
|
||||
|
||||
inflight_pings = deque()
|
||||
|
||||
while True:
|
||||
try:
|
||||
expected_body = self._ping_queue.popleft()
|
||||
if expected_body == message:
|
||||
# inflight_pings contains pings ignored by the
|
||||
# other peer. Just forget them.
|
||||
self._logger.debug(
|
||||
'Ping %r is acked (%d pings were ignored)',
|
||||
expected_body, len(inflight_pings))
|
||||
break
|
||||
else:
|
||||
inflight_pings.append(expected_body)
|
||||
except IndexError, e:
|
||||
# The received pong was unsolicited pong. Keep the
|
||||
# ping queue as is.
|
||||
self._ping_queue = inflight_pings
|
||||
self._logger.debug('Received a unsolicited pong')
|
||||
break
|
||||
|
||||
try:
|
||||
handler = self._request.on_pong_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
except AttributeError, e:
|
||||
pass
|
||||
|
||||
def receive_message(self):
|
||||
"""Receive a WebSocket frame and return its payload as a text in
|
||||
unicode or a binary in str.
|
||||
@ -408,6 +750,15 @@ class Stream(StreamBase):
|
||||
|
||||
frame = self._receive_frame_as_frame_object()
|
||||
|
||||
# Check the constraint on the payload size for control frames
|
||||
# before extension processes the frame.
|
||||
# See also http://tools.ietf.org/html/rfc6455#section-5.5
|
||||
if (common.is_control_opcode(frame.opcode) and
|
||||
len(frame.payload) > 125):
|
||||
raise InvalidFrameException(
|
||||
'Payload data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
|
||||
for frame_filter in self._options.incoming_frame_filters:
|
||||
frame_filter.filter(frame)
|
||||
|
||||
@ -416,58 +767,12 @@ class Stream(StreamBase):
|
||||
'Unsupported flag is set (rsv = %d%d%d)' %
|
||||
(frame.rsv1, frame.rsv2, frame.rsv3))
|
||||
|
||||
if frame.opcode == common.OPCODE_CONTINUATION:
|
||||
if not self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received a termination frame but fragmentation '
|
||||
'not started')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'Received an intermediate frame but '
|
||||
'fragmentation not started')
|
||||
message = self._get_message_from_frame(frame)
|
||||
if message is None:
|
||||
continue
|
||||
|
||||
if frame.fin:
|
||||
# End of fragmentation frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
message = ''.join(self._received_fragments)
|
||||
self._received_fragments = []
|
||||
else:
|
||||
# Intermediate frame
|
||||
self._received_fragments.append(frame.payload)
|
||||
continue
|
||||
else:
|
||||
if self._received_fragments:
|
||||
if frame.fin:
|
||||
raise InvalidFrameException(
|
||||
'Received an unfragmented frame without '
|
||||
'terminating existing fragmentation')
|
||||
else:
|
||||
raise InvalidFrameException(
|
||||
'New fragmentation started without terminating '
|
||||
'existing fragmentation')
|
||||
|
||||
if frame.fin:
|
||||
# Unfragmented frame
|
||||
|
||||
if (common.is_control_opcode(frame.opcode) and
|
||||
len(frame.payload) > 125):
|
||||
raise InvalidFrameException(
|
||||
'Application data size of control frames must be '
|
||||
'125 bytes or less')
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
message = frame.payload
|
||||
else:
|
||||
# Start of fragmentation frame
|
||||
|
||||
if common.is_control_opcode(frame.opcode):
|
||||
raise InvalidFrameException(
|
||||
'Control frames must not be fragmented')
|
||||
|
||||
self._original_opcode = frame.opcode
|
||||
self._received_fragments.append(frame.payload)
|
||||
continue
|
||||
for message_filter in self._options.incoming_message_filters:
|
||||
message = message_filter.filter(message)
|
||||
|
||||
if self._original_opcode == common.OPCODE_TEXT:
|
||||
# The WebSocket protocol section 4.4 specifies that invalid
|
||||
@ -480,134 +785,36 @@ class Stream(StreamBase):
|
||||
elif self._original_opcode == common.OPCODE_BINARY:
|
||||
return message
|
||||
elif self._original_opcode == common.OPCODE_CLOSE:
|
||||
self._request.client_terminated = True
|
||||
|
||||
# Status code is optional. We can have status reason only if we
|
||||
# have status code. Status reason can be empty string. So,
|
||||
# allowed cases are
|
||||
# - no application data: no code no reason
|
||||
# - 2 octet of application data: has code but no reason
|
||||
# - 3 or more octet of application data: both code and reason
|
||||
if len(message) == 0:
|
||||
self._logger.debug('Received close frame (empty body)')
|
||||
self._request.ws_close_code = (
|
||||
common.STATUS_NO_STATUS_RECEIVED)
|
||||
elif len(message) == 1:
|
||||
raise InvalidFrameException(
|
||||
'If a close frame has status code, the length of '
|
||||
'status code must be 2 octet')
|
||||
elif len(message) >= 2:
|
||||
self._request.ws_close_code = struct.unpack(
|
||||
'!H', message[0:2])[0]
|
||||
self._request.ws_close_reason = message[2:].decode(
|
||||
'utf-8', 'replace')
|
||||
self._logger.debug(
|
||||
'Received close frame (code=%d, reason=%r)',
|
||||
self._request.ws_close_code,
|
||||
self._request.ws_close_reason)
|
||||
|
||||
# Drain junk data after the close frame if necessary.
|
||||
self._drain_received_data()
|
||||
|
||||
if self._request.server_terminated:
|
||||
self._logger.debug(
|
||||
'Received ack for server-initiated closing handshake')
|
||||
return None
|
||||
|
||||
self._logger.debug(
|
||||
'Received client-initiated closing handshake')
|
||||
|
||||
code = common.STATUS_NORMAL_CLOSURE
|
||||
reason = ''
|
||||
if hasattr(self._request, '_dispatcher'):
|
||||
dispatcher = self._request._dispatcher
|
||||
code, reason = dispatcher.passive_closing_handshake(
|
||||
self._request)
|
||||
if code is None and reason is not None and len(reason) > 0:
|
||||
self._logger.warning(
|
||||
'Handler specified reason despite code being None')
|
||||
reason = ''
|
||||
if reason is None:
|
||||
reason = ''
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug(
|
||||
'Sent ack for client-initiated closing handshake '
|
||||
'(code=%r, reason=%r)', code, reason)
|
||||
self._process_close_message(message)
|
||||
return None
|
||||
elif self._original_opcode == common.OPCODE_PING:
|
||||
try:
|
||||
handler = self._request.on_ping_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
continue
|
||||
except AttributeError, e:
|
||||
pass
|
||||
self._send_pong(message)
|
||||
self._process_ping_message(message)
|
||||
elif self._original_opcode == common.OPCODE_PONG:
|
||||
# TODO(tyoshino): Add ping timeout handling.
|
||||
|
||||
inflight_pings = deque()
|
||||
|
||||
while True:
|
||||
try:
|
||||
expected_body = self._ping_queue.popleft()
|
||||
if expected_body == message:
|
||||
# inflight_pings contains pings ignored by the
|
||||
# other peer. Just forget them.
|
||||
self._logger.debug(
|
||||
'Ping %r is acked (%d pings were ignored)',
|
||||
expected_body, len(inflight_pings))
|
||||
break
|
||||
else:
|
||||
inflight_pings.append(expected_body)
|
||||
except IndexError, e:
|
||||
# The received pong was unsolicited pong. Keep the
|
||||
# ping queue as is.
|
||||
self._ping_queue = inflight_pings
|
||||
self._logger.debug('Received a unsolicited pong')
|
||||
break
|
||||
|
||||
try:
|
||||
handler = self._request.on_pong_handler
|
||||
if handler:
|
||||
handler(self._request, message)
|
||||
continue
|
||||
except AttributeError, e:
|
||||
pass
|
||||
|
||||
continue
|
||||
self._process_pong_message(message)
|
||||
else:
|
||||
raise UnsupportedFrameException(
|
||||
'Opcode %d is not supported' % self._original_opcode)
|
||||
|
||||
def _send_closing_handshake(self, code, reason):
|
||||
body = ''
|
||||
if code is not None:
|
||||
if code >= (1 << 16) or code < 0:
|
||||
raise BadOperationException('Status code is out of range')
|
||||
encoded_reason = reason.encode('utf-8')
|
||||
if len(encoded_reason) + 2 > 125:
|
||||
raise BadOperationException(
|
||||
'Application data size of close frames must be 125 bytes '
|
||||
'or less')
|
||||
body = struct.pack('!H', code) + encoded_reason
|
||||
|
||||
body = create_closing_handshake_body(code, reason)
|
||||
frame = create_close_frame(
|
||||
body,
|
||||
self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
body, mask=self._options.mask_send,
|
||||
frame_filters=self._options.outgoing_frame_filters)
|
||||
|
||||
self._request.server_terminated = True
|
||||
|
||||
self._write(frame)
|
||||
|
||||
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''):
|
||||
"""Closes a WebSocket connection.
|
||||
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='',
|
||||
wait_response=True):
|
||||
"""Closes a WebSocket connection. Note that this method blocks until
|
||||
it receives acknowledgement to the closing handshake.
|
||||
|
||||
Args:
|
||||
code: Status code for close frame. If code is None, a close
|
||||
frame with empty body will be sent.
|
||||
reason: string representing close reason.
|
||||
wait_response: True when caller want to wait the response.
|
||||
Raises:
|
||||
BadOperationException: when reason is specified with code None
|
||||
or reason is not an instance of both str and unicode.
|
||||
@ -618,6 +825,12 @@ class Stream(StreamBase):
|
||||
'Requested close_connection but server is already terminated')
|
||||
return
|
||||
|
||||
# When we receive a close frame, we call _process_close_message().
|
||||
# _process_close_message() immediately acknowledges to the
|
||||
# server-initiated closing handshake and sets server_terminated to
|
||||
# True. So, here we can assume that we haven't received any close
|
||||
# frame. We're initiating a closing handshake.
|
||||
|
||||
if code is None:
|
||||
if reason is not None and len(reason) > 0:
|
||||
raise BadOperationException(
|
||||
@ -630,11 +843,11 @@ class Stream(StreamBase):
|
||||
|
||||
self._send_closing_handshake(code, reason)
|
||||
self._logger.debug(
|
||||
'Sent server-initiated closing handshake (code=%r, reason=%r)',
|
||||
'Initiated closing handshake (code=%r, reason=%r)',
|
||||
code, reason)
|
||||
|
||||
if (code == common.STATUS_GOING_AWAY or
|
||||
code == common.STATUS_PROTOCOL_ERROR):
|
||||
code == common.STATUS_PROTOCOL_ERROR) or not wait_response:
|
||||
# It doesn't make sense to wait for a close frame if the reason is
|
||||
# protocol error or that the server is going away. For some of
|
||||
# other reasons, it might not make sense to wait for a close frame,
|
||||
@ -654,10 +867,6 @@ class Stream(StreamBase):
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
def send_ping(self, body=''):
|
||||
if len(body) > 125:
|
||||
raise ValueError(
|
||||
'Application data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
frame = create_ping_frame(
|
||||
body,
|
||||
self._options.mask_send,
|
||||
@ -667,35 +876,19 @@ class Stream(StreamBase):
|
||||
self._ping_queue.append(body)
|
||||
|
||||
def _send_pong(self, body):
|
||||
if len(body) > 125:
|
||||
raise ValueError(
|
||||
'Application data size of control frames must be 125 bytes or '
|
||||
'less')
|
||||
frame = create_pong_frame(
|
||||
body,
|
||||
self._options.mask_send,
|
||||
self._options.outgoing_frame_filters)
|
||||
self._write(frame)
|
||||
|
||||
def _drain_received_data(self):
|
||||
"""Drains unread data in the receive buffer to avoid sending out TCP
|
||||
RST packet. This is because when deflate-stream is enabled, some
|
||||
DEFLATE block for flushing data may follow a close frame. If any data
|
||||
remains in the receive buffer of a socket when the socket is closed,
|
||||
it sends out TCP RST packet to the other peer.
|
||||
|
||||
Since mod_python's mp_conn object doesn't support non-blocking read,
|
||||
we perform this only when pywebsocket is running in standalone mode.
|
||||
def get_last_received_opcode(self):
|
||||
"""Returns the opcode of the WebSocket message which the last received
|
||||
frame belongs to. The return value is valid iff immediately after
|
||||
receive_message call.
|
||||
"""
|
||||
|
||||
# If self._options.deflate_stream is true, self._request is
|
||||
# DeflateRequest, so we can get wrapped request object by
|
||||
# self._request._request.
|
||||
#
|
||||
# Only _StandaloneRequest has _drain_received_data method.
|
||||
if (self._options.deflate_stream and
|
||||
('_drain_received_data' in dir(self._request._request))):
|
||||
self._request._request._drain_received_data()
|
||||
return self._original_opcode
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -28,6 +28,16 @@
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""This file must not depend on any module specific to the WebSocket protocol.
|
||||
"""
|
||||
|
||||
|
||||
from mod_pywebsocket import http_header_util
|
||||
|
||||
|
||||
# Additional log level definitions.
|
||||
LOGLEVEL_FINE = 9
|
||||
|
||||
# Constants indicating WebSocket protocol version.
|
||||
VERSION_HIXIE75 = -1
|
||||
VERSION_HYBI00 = 0
|
||||
@ -91,9 +101,10 @@ SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
|
||||
SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
|
||||
|
||||
# Extensions
|
||||
DEFLATE_STREAM_EXTENSION = 'deflate-stream'
|
||||
DEFLATE_FRAME_EXTENSION = 'deflate-frame'
|
||||
PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
|
||||
X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
|
||||
MUX_EXTENSION = 'mux_DO_NOT_USE'
|
||||
|
||||
# Status codes
|
||||
# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
|
||||
@ -114,7 +125,7 @@ STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
|
||||
STATUS_POLICY_VIOLATION = 1008
|
||||
STATUS_MESSAGE_TOO_BIG = 1009
|
||||
STATUS_MANDATORY_EXTENSION = 1010
|
||||
STATUS_INTERNAL_SERVER_ERROR = 1011
|
||||
STATUS_INTERNAL_ENDPOINT_ERROR = 1011
|
||||
STATUS_TLS_HANDSHAKE = 1015
|
||||
STATUS_USER_REGISTERED_BASE = 3000
|
||||
STATUS_USER_REGISTERED_MAX = 3999
|
||||
@ -176,4 +187,115 @@ class ExtensionParameter(object):
|
||||
return param_value
|
||||
|
||||
|
||||
class ExtensionParsingException(Exception):
|
||||
def __init__(self, name):
|
||||
super(ExtensionParsingException, self).__init__(name)
|
||||
|
||||
|
||||
def _parse_extension_param(state, definition):
|
||||
param_name = http_header_util.consume_token(state)
|
||||
|
||||
if param_name is None:
|
||||
raise ExtensionParsingException('No valid parameter name found')
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, '='):
|
||||
definition.add_parameter(param_name, None)
|
||||
return
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
# TODO(tyoshino): Add code to validate that parsed param_value is token
|
||||
param_value = http_header_util.consume_token_or_quoted_string(state)
|
||||
if param_value is None:
|
||||
raise ExtensionParsingException(
|
||||
'No valid parameter value found on the right-hand side of '
|
||||
'parameter %r' % param_name)
|
||||
|
||||
definition.add_parameter(param_name, param_value)
|
||||
|
||||
|
||||
def _parse_extension(state):
|
||||
extension_token = http_header_util.consume_token(state)
|
||||
if extension_token is None:
|
||||
return None
|
||||
|
||||
extension = ExtensionParameter(extension_token)
|
||||
|
||||
while True:
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, ';'):
|
||||
break
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
try:
|
||||
_parse_extension_param(state, extension)
|
||||
except ExtensionParsingException, e:
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse parameter for %r (%r)' %
|
||||
(extension_token, e))
|
||||
|
||||
return extension
|
||||
|
||||
|
||||
def parse_extensions(data):
|
||||
"""Parses Sec-WebSocket-Extensions header value returns a list of
|
||||
ExtensionParameter objects.
|
||||
|
||||
Leading LWSes must be trimmed.
|
||||
"""
|
||||
|
||||
state = http_header_util.ParsingState(data)
|
||||
|
||||
extension_list = []
|
||||
while True:
|
||||
extension = _parse_extension(state)
|
||||
if extension is not None:
|
||||
extension_list.append(extension)
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if http_header_util.peek(state) is None:
|
||||
break
|
||||
|
||||
if not http_header_util.consume_string(state, ','):
|
||||
raise ExtensionParsingException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: '
|
||||
'Expected a comma but found %r' %
|
||||
http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if len(extension_list) == 0:
|
||||
raise ExtensionParsingException(
|
||||
'No valid extension entry found')
|
||||
|
||||
return extension_list
|
||||
|
||||
|
||||
def format_extension(extension):
|
||||
"""Formats an ExtensionParameter object."""
|
||||
|
||||
formatted_params = [extension.name()]
|
||||
for param_name, param_value in extension.get_parameters():
|
||||
if param_value is None:
|
||||
formatted_params.append(param_name)
|
||||
else:
|
||||
quoted_value = http_header_util.quote_if_necessary(param_value)
|
||||
formatted_params.append('%s=%s' % (param_name, quoted_value))
|
||||
return '; '.join(formatted_params)
|
||||
|
||||
|
||||
def format_extensions(extension_list):
|
||||
"""Formats a list of ExtensionParameter objects."""
|
||||
|
||||
formatted_extension_list = []
|
||||
for extension in extension_list:
|
||||
formatted_extension_list.append(format_extension(extension))
|
||||
return ', '.join(formatted_extension_list)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -39,6 +39,7 @@ import re
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import mux
|
||||
from mod_pywebsocket import stream
|
||||
from mod_pywebsocket import util
|
||||
|
||||
@ -254,6 +255,9 @@ class Dispatcher(object):
|
||||
try:
|
||||
do_extra_handshake_(request)
|
||||
except handshake.AbortedByUserException, e:
|
||||
# Re-raise to tell the caller of this function to finish this
|
||||
# connection without sending any error.
|
||||
self._logger.debug('%s', util.get_stack_trace())
|
||||
raise
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
@ -277,22 +281,28 @@ class Dispatcher(object):
|
||||
AbortedByUserException: when user handler abort connection
|
||||
"""
|
||||
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' % request.ws_resource)
|
||||
transfer_data_ = handler_suite.transfer_data
|
||||
# TODO(tyoshino): Terminate underlying TCP connection if possible.
|
||||
try:
|
||||
transfer_data_(request)
|
||||
if mux.use_mux(request):
|
||||
mux.start(request, self)
|
||||
else:
|
||||
handler_suite = self.get_handler_suite(request.ws_resource)
|
||||
if handler_suite is None:
|
||||
raise DispatchException('No handler for: %r' %
|
||||
request.ws_resource)
|
||||
transfer_data_ = handler_suite.transfer_data
|
||||
transfer_data_(request)
|
||||
|
||||
if not request.server_terminated:
|
||||
request.ws_stream.close_connection()
|
||||
# Catch non-critical exceptions the handler didn't handle.
|
||||
except handshake.AbortedByUserException, e:
|
||||
self._logger.debug('%s', e)
|
||||
self._logger.debug('%s', util.get_stack_trace())
|
||||
raise
|
||||
except msgutil.BadOperationException, e:
|
||||
self._logger.debug('%s', e)
|
||||
request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE)
|
||||
request.ws_stream.close_connection(
|
||||
common.STATUS_INTERNAL_ENDPOINT_ERROR)
|
||||
except msgutil.InvalidFrameException, e:
|
||||
# InvalidFrameException must be caught before
|
||||
# ConnectionTerminatedException that catches InvalidFrameException.
|
||||
@ -308,6 +318,8 @@ class Dispatcher(object):
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
self._logger.debug('%s', e)
|
||||
except Exception, e:
|
||||
# Any other exceptions are forwarded to the caller of this
|
||||
# function.
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2011, Google Inc.
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
@ -30,76 +30,164 @@
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.http_header_util import quote_if_necessary
|
||||
|
||||
|
||||
# The list of available server side extension processor classes.
|
||||
_available_processors = {}
|
||||
_compression_extension_names = []
|
||||
|
||||
|
||||
class ExtensionProcessorInterface(object):
|
||||
|
||||
def get_extension_response(self):
|
||||
return None
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
pass
|
||||
|
||||
|
||||
class DeflateStreamExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""WebSocket DEFLATE stream extension processor."""
|
||||
|
||||
def __init__(self, request):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._active = True
|
||||
|
||||
def request(self):
|
||||
return self._request
|
||||
|
||||
def name(self):
|
||||
return None
|
||||
|
||||
def check_consistency_with_other_processors(self, processors):
|
||||
pass
|
||||
|
||||
def set_active(self, active):
|
||||
self._active = active
|
||||
|
||||
def is_active(self):
|
||||
return self._active
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
return None
|
||||
|
||||
def get_extension_response(self):
|
||||
if len(self._request.get_parameter_names()) != 0:
|
||||
if not self._active:
|
||||
self._logger.debug('Extension %s is deactivated', self.name())
|
||||
return None
|
||||
|
||||
self._logger.debug(
|
||||
'Enable %s extension', common.DEFLATE_STREAM_EXTENSION)
|
||||
response = self._get_extension_response_internal()
|
||||
if response is None:
|
||||
self._active = False
|
||||
return response
|
||||
|
||||
return common.ExtensionParameter(common.DEFLATE_STREAM_EXTENSION)
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
pass
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
stream_options.deflate_stream = True
|
||||
if self._active:
|
||||
self._setup_stream_options_internal(stream_options)
|
||||
|
||||
|
||||
_available_processors[common.DEFLATE_STREAM_EXTENSION] = (
|
||||
DeflateStreamExtensionProcessor)
|
||||
def _log_outgoing_compression_ratio(
|
||||
logger, original_bytes, filtered_bytes, average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if original_bytes != 0:
|
||||
ratio = float(filtered_bytes) / original_bytes
|
||||
|
||||
logger.debug('Outgoing compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _log_incoming_compression_ratio(
|
||||
logger, received_bytes, filtered_bytes, average_ratio):
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
if filtered_bytes != 0:
|
||||
ratio = float(received_bytes) / filtered_bytes
|
||||
|
||||
logger.debug('Incoming compression ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
|
||||
|
||||
def _parse_window_bits(bits):
|
||||
"""Return parsed integer value iff the given string conforms to the
|
||||
grammar of the window bits extension parameters.
|
||||
"""
|
||||
|
||||
if bits is None:
|
||||
raise ValueError('Value is required')
|
||||
|
||||
# For non integer values such as "10.0", ValueError will be raised.
|
||||
int_bits = int(bits)
|
||||
|
||||
# First condition is to drop leading zero case e.g. "08".
|
||||
if bits != str(int_bits) or int_bits < 8 or int_bits > 15:
|
||||
raise ValueError('Invalid value: %r' % bits)
|
||||
|
||||
return int_bits
|
||||
|
||||
|
||||
class _AverageRatioCalculator(object):
|
||||
"""Stores total bytes of original and result data, and calculates average
|
||||
result / original ratio.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._total_original_bytes = 0
|
||||
self._total_result_bytes = 0
|
||||
|
||||
def add_original_bytes(self, value):
|
||||
self._total_original_bytes += value
|
||||
|
||||
def add_result_bytes(self, value):
|
||||
self._total_result_bytes += value
|
||||
|
||||
def get_average_ratio(self):
|
||||
if self._total_original_bytes != 0:
|
||||
return (float(self._total_result_bytes) /
|
||||
self._total_original_bytes)
|
||||
else:
|
||||
return float('inf')
|
||||
|
||||
|
||||
class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""WebSocket Per-frame DEFLATE extension processor."""
|
||||
"""deflate-frame extension processor.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate
|
||||
"""
|
||||
|
||||
_WINDOW_BITS_PARAM = 'max_window_bits'
|
||||
_NO_CONTEXT_TAKEOVER_PARAM = 'no_context_takeover'
|
||||
|
||||
def __init__(self, request):
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
|
||||
self._response_window_bits = None
|
||||
self._response_no_context_takeover = False
|
||||
self._bfinal = False
|
||||
|
||||
# Counters for statistics.
|
||||
# Calculates
|
||||
# (Total outgoing bytes supplied to this filter) /
|
||||
# (Total bytes sent to the network after applying this filter)
|
||||
self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
# Total number of outgoing bytes supplied to this filter.
|
||||
self._total_outgoing_payload_bytes = 0
|
||||
# Total number of bytes sent to the network after applying this filter.
|
||||
self._total_filtered_outgoing_payload_bytes = 0
|
||||
# Calculates
|
||||
# (Total bytes received from the network) /
|
||||
# (Total incoming bytes obtained after applying this filter)
|
||||
self._incoming_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
# Total number of bytes received from the network.
|
||||
self._total_incoming_payload_bytes = 0
|
||||
# Total number of incoming bytes obtained after applying this filter.
|
||||
self._total_filtered_incoming_payload_bytes = 0
|
||||
def name(self):
|
||||
return common.DEFLATE_FRAME_EXTENSION
|
||||
|
||||
def get_extension_response(self):
|
||||
def _get_extension_response_internal(self):
|
||||
# Any unknown parameter will be just ignored.
|
||||
|
||||
window_bits = self._request.get_parameter_value(
|
||||
self._WINDOW_BITS_PARAM)
|
||||
window_bits = None
|
||||
if self._request.has_parameter(self._WINDOW_BITS_PARAM):
|
||||
window_bits = self._request.get_parameter_value(
|
||||
self._WINDOW_BITS_PARAM)
|
||||
try:
|
||||
window_bits = _parse_window_bits(window_bits)
|
||||
except ValueError, e:
|
||||
return None
|
||||
|
||||
no_context_takeover = self._request.has_parameter(
|
||||
self._NO_CONTEXT_TAKEOVER_PARAM)
|
||||
if (no_context_takeover and
|
||||
@ -107,18 +195,10 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
self._NO_CONTEXT_TAKEOVER_PARAM) is not None):
|
||||
return None
|
||||
|
||||
if window_bits is not None:
|
||||
try:
|
||||
window_bits = int(window_bits)
|
||||
except ValueError, e:
|
||||
return None
|
||||
if window_bits < 8 or window_bits > 15:
|
||||
return None
|
||||
|
||||
self._deflater = util._RFC1979Deflater(
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
window_bits, no_context_takeover)
|
||||
|
||||
self._inflater = util._RFC1979Inflater()
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._compress_outgoing = True
|
||||
|
||||
@ -143,7 +223,7 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
|
||||
return response
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
|
||||
class _OutgoingFilter(object):
|
||||
|
||||
@ -172,6 +252,9 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
def set_response_no_context_takeover(self, value):
|
||||
self._response_no_context_takeover = value
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._bfinal = value
|
||||
|
||||
def enable_outgoing_compression(self):
|
||||
self._compress_outgoing = True
|
||||
|
||||
@ -184,32 +267,28 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""
|
||||
|
||||
original_payload_size = len(frame.payload)
|
||||
self._total_outgoing_payload_bytes += original_payload_size
|
||||
self._outgoing_average_ratio_calculator.add_original_bytes(
|
||||
original_payload_size)
|
||||
|
||||
if (not self._compress_outgoing or
|
||||
common.is_control_opcode(frame.opcode)):
|
||||
self._total_filtered_outgoing_payload_bytes += (
|
||||
original_payload_size)
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
original_payload_size)
|
||||
return
|
||||
|
||||
frame.payload = self._deflater.filter(frame.payload)
|
||||
frame.payload = self._rfc1979_deflater.filter(
|
||||
frame.payload, bfinal=self._bfinal)
|
||||
frame.rsv1 = 1
|
||||
|
||||
filtered_payload_size = len(frame.payload)
|
||||
self._total_filtered_outgoing_payload_bytes += filtered_payload_size
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
average_ratio = float('inf')
|
||||
if original_payload_size != 0:
|
||||
ratio = float(filtered_payload_size) / original_payload_size
|
||||
if self._total_outgoing_payload_bytes != 0:
|
||||
average_ratio = (
|
||||
float(self._total_filtered_outgoing_payload_bytes) /
|
||||
self._total_outgoing_payload_bytes)
|
||||
self._logger.debug(
|
||||
'Outgoing compress ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
_log_outgoing_compression_ratio(
|
||||
self._logger,
|
||||
original_payload_size,
|
||||
filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
def _incoming_filter(self, frame):
|
||||
"""Transform incoming frames. This method is called only by
|
||||
@ -217,49 +296,469 @@ class DeflateFrameExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""
|
||||
|
||||
received_payload_size = len(frame.payload)
|
||||
self._total_incoming_payload_bytes += received_payload_size
|
||||
self._incoming_average_ratio_calculator.add_result_bytes(
|
||||
received_payload_size)
|
||||
|
||||
if frame.rsv1 != 1 or common.is_control_opcode(frame.opcode):
|
||||
self._total_filtered_incoming_payload_bytes += (
|
||||
received_payload_size)
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
received_payload_size)
|
||||
return
|
||||
|
||||
frame.payload = self._inflater.filter(frame.payload)
|
||||
frame.payload = self._rfc1979_inflater.filter(frame.payload)
|
||||
frame.rsv1 = 0
|
||||
|
||||
filtered_payload_size = len(frame.payload)
|
||||
self._total_filtered_incoming_payload_bytes += filtered_payload_size
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
# Print inf when ratio is not available.
|
||||
ratio = float('inf')
|
||||
average_ratio = float('inf')
|
||||
if received_payload_size != 0:
|
||||
ratio = float(received_payload_size) / filtered_payload_size
|
||||
if self._total_filtered_incoming_payload_bytes != 0:
|
||||
average_ratio = (
|
||||
float(self._total_incoming_payload_bytes) /
|
||||
self._total_filtered_incoming_payload_bytes)
|
||||
self._logger.debug(
|
||||
'Incoming compress ratio: %f (average: %f)' %
|
||||
(ratio, average_ratio))
|
||||
_log_incoming_compression_ratio(
|
||||
self._logger,
|
||||
received_payload_size,
|
||||
filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
|
||||
_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
|
||||
DeflateFrameExtensionProcessor)
|
||||
_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION)
|
||||
|
||||
|
||||
# Adding vendor-prefixed deflate-frame extension.
|
||||
# TODO(bashi): Remove this after WebKit stops using vender prefix.
|
||||
_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
|
||||
DeflateFrameExtensionProcessor)
|
||||
_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION)
|
||||
|
||||
|
||||
class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""permessage-deflate extension processor.
|
||||
|
||||
Specification:
|
||||
http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08
|
||||
"""
|
||||
|
||||
_SERVER_MAX_WINDOW_BITS_PARAM = 'server_max_window_bits'
|
||||
_SERVER_NO_CONTEXT_TAKEOVER_PARAM = 'server_no_context_takeover'
|
||||
_CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
|
||||
_CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
|
||||
|
||||
def __init__(self, request, draft08=True):
|
||||
"""Construct PerMessageDeflateExtensionProcessor
|
||||
|
||||
Args:
|
||||
draft08: Follow the constraints on the parameters that were not
|
||||
specified for permessage-compress but are specified for
|
||||
permessage-deflate as on
|
||||
draft-ietf-hybi-permessage-compression-08.
|
||||
"""
|
||||
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._preferred_client_max_window_bits = None
|
||||
self._client_no_context_takeover = False
|
||||
|
||||
self._draft08 = draft08
|
||||
|
||||
def name(self):
|
||||
# This method returns "deflate" (not "permessage-deflate") for
|
||||
# compatibility.
|
||||
return 'deflate'
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
if self._draft08:
|
||||
for name in self._request.get_parameter_names():
|
||||
if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM]:
|
||||
self._logger.debug('Unknown parameter: %r', name)
|
||||
return None
|
||||
else:
|
||||
# Any unknown parameter will be just ignored.
|
||||
pass
|
||||
|
||||
server_max_window_bits = None
|
||||
if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
|
||||
server_max_window_bits = self._request.get_parameter_value(
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM)
|
||||
try:
|
||||
server_max_window_bits = _parse_window_bits(
|
||||
server_max_window_bits)
|
||||
except ValueError, e:
|
||||
self._logger.debug('Bad %s parameter: %r',
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
e)
|
||||
return None
|
||||
|
||||
server_no_context_takeover = self._request.has_parameter(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM)
|
||||
if (server_no_context_takeover and
|
||||
self._request.get_parameter_value(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM) is not None):
|
||||
self._logger.debug('%s parameter must not have a value: %r',
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
|
||||
server_no_context_takeover)
|
||||
return None
|
||||
|
||||
# client_max_window_bits from a client indicates whether the client can
|
||||
# accept client_max_window_bits from a server or not.
|
||||
client_client_max_window_bits = self._request.has_parameter(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
if (self._draft08 and
|
||||
client_client_max_window_bits and
|
||||
self._request.get_parameter_value(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
|
||||
self._logger.debug('%s parameter must not have a value in a '
|
||||
'client\'s opening handshake: %r',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
client_client_max_window_bits)
|
||||
return None
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
server_max_window_bits, server_no_context_takeover)
|
||||
|
||||
# Note that we prepare for incoming messages compressed with window
|
||||
# bits upto 15 regardless of the client_max_window_bits value to be
|
||||
# sent to the client.
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._framer = _PerMessageDeflateFramer(
|
||||
server_max_window_bits, server_no_context_takeover)
|
||||
self._framer.set_bfinal(False)
|
||||
self._framer.set_compress_outgoing_enabled(True)
|
||||
|
||||
response = common.ExtensionParameter(self._request.name())
|
||||
|
||||
if server_max_window_bits is not None:
|
||||
response.add_parameter(
|
||||
self._SERVER_MAX_WINDOW_BITS_PARAM,
|
||||
str(server_max_window_bits))
|
||||
|
||||
if server_no_context_takeover:
|
||||
response.add_parameter(
|
||||
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None)
|
||||
|
||||
if self._preferred_client_max_window_bits is not None:
|
||||
if self._draft08 and not client_client_max_window_bits:
|
||||
self._logger.debug('Processor is configured to use %s but '
|
||||
'the client cannot accept it',
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM)
|
||||
return None
|
||||
response.add_parameter(
|
||||
self._CLIENT_MAX_WINDOW_BITS_PARAM,
|
||||
str(self._preferred_client_max_window_bits))
|
||||
|
||||
if self._client_no_context_takeover:
|
||||
response.add_parameter(
|
||||
self._CLIENT_NO_CONTEXT_TAKEOVER_PARAM, None)
|
||||
|
||||
self._logger.debug(
|
||||
'Enable %s extension ('
|
||||
'request: server_max_window_bits=%s; '
|
||||
'server_no_context_takeover=%r, '
|
||||
'response: client_max_window_bits=%s; '
|
||||
'client_no_context_takeover=%r)' %
|
||||
(self._request.name(),
|
||||
server_max_window_bits,
|
||||
server_no_context_takeover,
|
||||
self._preferred_client_max_window_bits,
|
||||
self._client_no_context_takeover))
|
||||
|
||||
return response
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
self._framer.setup_stream_options(stream_options)
|
||||
|
||||
def set_client_max_window_bits(self, value):
|
||||
"""If this option is specified, this class adds the
|
||||
client_max_window_bits extension parameter to the handshake response,
|
||||
but doesn't reduce the LZ77 sliding window size of its inflater.
|
||||
I.e., you can use this for testing client implementation but cannot
|
||||
reduce memory usage of this class.
|
||||
|
||||
If this method has been called with True and an offer without the
|
||||
client_max_window_bits extension parameter is received,
|
||||
- (When processing the permessage-deflate extension) this processor
|
||||
declines the request.
|
||||
- (When processing the permessage-compress extension) this processor
|
||||
accepts the request.
|
||||
"""
|
||||
|
||||
self._preferred_client_max_window_bits = value
|
||||
|
||||
def set_client_no_context_takeover(self, value):
|
||||
"""If this option is specified, this class adds the
|
||||
client_no_context_takeover extension parameter to the handshake
|
||||
response, but doesn't reset inflater for each message. I.e., you can
|
||||
use this for testing client implementation but cannot reduce memory
|
||||
usage of this class.
|
||||
"""
|
||||
|
||||
self._client_no_context_takeover = value
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._framer.set_bfinal(value)
|
||||
|
||||
def enable_outgoing_compression(self):
|
||||
self._framer.set_compress_outgoing_enabled(True)
|
||||
|
||||
def disable_outgoing_compression(self):
|
||||
self._framer.set_compress_outgoing_enabled(False)
|
||||
|
||||
|
||||
class _PerMessageDeflateFramer(object):
|
||||
"""A framer for extensions with per-message DEFLATE feature."""
|
||||
|
||||
def __init__(self, deflate_max_window_bits, deflate_no_context_takeover):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._rfc1979_deflater = util._RFC1979Deflater(
|
||||
deflate_max_window_bits, deflate_no_context_takeover)
|
||||
|
||||
self._rfc1979_inflater = util._RFC1979Inflater()
|
||||
|
||||
self._bfinal = False
|
||||
|
||||
self._compress_outgoing_enabled = False
|
||||
|
||||
# True if a message is fragmented and compression is ongoing.
|
||||
self._compress_ongoing = False
|
||||
|
||||
# Calculates
|
||||
# (Total outgoing bytes supplied to this filter) /
|
||||
# (Total bytes sent to the network after applying this filter)
|
||||
self._outgoing_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
# Calculates
|
||||
# (Total bytes received from the network) /
|
||||
# (Total incoming bytes obtained after applying this filter)
|
||||
self._incoming_average_ratio_calculator = _AverageRatioCalculator()
|
||||
|
||||
def set_bfinal(self, value):
|
||||
self._bfinal = value
|
||||
|
||||
def set_compress_outgoing_enabled(self, value):
|
||||
self._compress_outgoing_enabled = value
|
||||
|
||||
def _process_incoming_message(self, message, decompress):
|
||||
if not decompress:
|
||||
return message
|
||||
|
||||
received_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_result_bytes(
|
||||
received_payload_size)
|
||||
|
||||
message = self._rfc1979_inflater.filter(message)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._incoming_average_ratio_calculator.add_original_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_incoming_compression_ratio(
|
||||
self._logger,
|
||||
received_payload_size,
|
||||
filtered_payload_size,
|
||||
self._incoming_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
return message
|
||||
|
||||
def _process_outgoing_message(self, message, end, binary):
|
||||
if not binary:
|
||||
message = message.encode('utf-8')
|
||||
|
||||
if not self._compress_outgoing_enabled:
|
||||
return message
|
||||
|
||||
original_payload_size = len(message)
|
||||
self._outgoing_average_ratio_calculator.add_original_bytes(
|
||||
original_payload_size)
|
||||
|
||||
message = self._rfc1979_deflater.filter(
|
||||
message, end=end, bfinal=self._bfinal)
|
||||
|
||||
filtered_payload_size = len(message)
|
||||
self._outgoing_average_ratio_calculator.add_result_bytes(
|
||||
filtered_payload_size)
|
||||
|
||||
_log_outgoing_compression_ratio(
|
||||
self._logger,
|
||||
original_payload_size,
|
||||
filtered_payload_size,
|
||||
self._outgoing_average_ratio_calculator.get_average_ratio())
|
||||
|
||||
if not self._compress_ongoing:
|
||||
self._outgoing_frame_filter.set_compression_bit()
|
||||
self._compress_ongoing = not end
|
||||
return message
|
||||
|
||||
def _process_incoming_frame(self, frame):
|
||||
if frame.rsv1 == 1 and not common.is_control_opcode(frame.opcode):
|
||||
self._incoming_message_filter.decompress_next_message()
|
||||
frame.rsv1 = 0
|
||||
|
||||
def _process_outgoing_frame(self, frame, compression_bit):
|
||||
if (not compression_bit or
|
||||
common.is_control_opcode(frame.opcode)):
|
||||
return
|
||||
|
||||
frame.rsv1 = 1
|
||||
|
||||
def setup_stream_options(self, stream_options):
|
||||
"""Creates filters and sets them to the StreamOptions."""
|
||||
|
||||
class _OutgoingMessageFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, message, end=True, binary=False):
|
||||
return self._parent._process_outgoing_message(
|
||||
message, end, binary)
|
||||
|
||||
class _IncomingMessageFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._decompress_next_message = False
|
||||
|
||||
def decompress_next_message(self):
|
||||
self._decompress_next_message = True
|
||||
|
||||
def filter(self, message):
|
||||
message = self._parent._process_incoming_message(
|
||||
message, self._decompress_next_message)
|
||||
self._decompress_next_message = False
|
||||
return message
|
||||
|
||||
self._outgoing_message_filter = _OutgoingMessageFilter(self)
|
||||
self._incoming_message_filter = _IncomingMessageFilter(self)
|
||||
stream_options.outgoing_message_filters.append(
|
||||
self._outgoing_message_filter)
|
||||
stream_options.incoming_message_filters.append(
|
||||
self._incoming_message_filter)
|
||||
|
||||
class _OutgoingFrameFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
self._set_compression_bit = False
|
||||
|
||||
def set_compression_bit(self):
|
||||
self._set_compression_bit = True
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._process_outgoing_frame(
|
||||
frame, self._set_compression_bit)
|
||||
self._set_compression_bit = False
|
||||
|
||||
class _IncomingFrameFilter(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
def filter(self, frame):
|
||||
self._parent._process_incoming_frame(frame)
|
||||
|
||||
self._outgoing_frame_filter = _OutgoingFrameFilter(self)
|
||||
self._incoming_frame_filter = _IncomingFrameFilter(self)
|
||||
stream_options.outgoing_frame_filters.append(
|
||||
self._outgoing_frame_filter)
|
||||
stream_options.incoming_frame_filters.append(
|
||||
self._incoming_frame_filter)
|
||||
|
||||
stream_options.encode_text_message_to_utf8 = False
|
||||
|
||||
|
||||
_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
|
||||
PerMessageDeflateExtensionProcessor)
|
||||
# TODO(tyoshino): Reorganize class names.
|
||||
_compression_extension_names.append('deflate')
|
||||
|
||||
|
||||
class MuxExtensionProcessor(ExtensionProcessorInterface):
|
||||
"""WebSocket multiplexing extension processor."""
|
||||
|
||||
_QUOTA_PARAM = 'quota'
|
||||
|
||||
def __init__(self, request):
|
||||
ExtensionProcessorInterface.__init__(self, request)
|
||||
self._quota = 0
|
||||
self._extensions = []
|
||||
|
||||
def name(self):
|
||||
return common.MUX_EXTENSION
|
||||
|
||||
def check_consistency_with_other_processors(self, processors):
|
||||
before_mux = True
|
||||
for processor in processors:
|
||||
name = processor.name()
|
||||
if name == self.name():
|
||||
before_mux = False
|
||||
continue
|
||||
if not processor.is_active():
|
||||
continue
|
||||
if before_mux:
|
||||
# Mux extension cannot be used after extensions
|
||||
# that depend on frame boundary, extension data field, or any
|
||||
# reserved bits which are attributed to each frame.
|
||||
if (name == common.DEFLATE_FRAME_EXTENSION or
|
||||
name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
|
||||
self.set_active(False)
|
||||
return
|
||||
else:
|
||||
# Mux extension should not be applied before any history-based
|
||||
# compression extension.
|
||||
if (name == 'deflate' or
|
||||
name == common.DEFLATE_FRAME_EXTENSION or
|
||||
name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
|
||||
self.set_active(False)
|
||||
return
|
||||
|
||||
def _get_extension_response_internal(self):
|
||||
self._active = False
|
||||
quota = self._request.get_parameter_value(self._QUOTA_PARAM)
|
||||
if quota is not None:
|
||||
try:
|
||||
quota = int(quota)
|
||||
except ValueError, e:
|
||||
return None
|
||||
if quota < 0 or quota >= 2 ** 32:
|
||||
return None
|
||||
self._quota = quota
|
||||
|
||||
self._active = True
|
||||
return common.ExtensionParameter(common.MUX_EXTENSION)
|
||||
|
||||
def _setup_stream_options_internal(self, stream_options):
|
||||
pass
|
||||
|
||||
def set_quota(self, quota):
|
||||
self._quota = quota
|
||||
|
||||
def quota(self):
|
||||
return self._quota
|
||||
|
||||
def set_extensions(self, extensions):
|
||||
self._extensions = extensions
|
||||
|
||||
def extensions(self):
|
||||
return self._extensions
|
||||
|
||||
|
||||
_available_processors[common.MUX_EXTENSION] = MuxExtensionProcessor
|
||||
|
||||
|
||||
def get_extension_processor(extension_request):
|
||||
global _available_processors
|
||||
"""Given an ExtensionParameter representing an extension offer received
|
||||
from a client, configures and returns an instance of the corresponding
|
||||
extension processor class.
|
||||
"""
|
||||
|
||||
processor_class = _available_processors.get(extension_request.name())
|
||||
if processor_class is None:
|
||||
return None
|
||||
return processor_class(extension_request)
|
||||
|
||||
|
||||
def is_compression_extension(extension_name):
|
||||
return extension_name in _compression_extension_names
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
98
testing/mochitest/pywebsocket/mod_pywebsocket/fast_masking.i
Normal file
98
testing/mochitest/pywebsocket/mod_pywebsocket/fast_masking.i
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright 2013, Google Inc.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
%module fast_masking
|
||||
|
||||
%include "cstring.i"
|
||||
|
||||
%{
|
||||
#include <cstring>
|
||||
|
||||
#ifdef __SSE2__
|
||||
#include <emmintrin.h>
|
||||
#endif
|
||||
%}
|
||||
|
||||
%apply (char *STRING, int LENGTH) {
|
||||
(const char* payload, int payload_length),
|
||||
(const char* masking_key, int masking_key_length) };
|
||||
%cstring_output_allocate_size(
|
||||
char** result, int* result_length, delete [] *$1);
|
||||
|
||||
%inline %{
|
||||
|
||||
void mask(
|
||||
const char* payload, int payload_length,
|
||||
const char* masking_key, int masking_key_length,
|
||||
int masking_key_index,
|
||||
char** result, int* result_length) {
|
||||
*result = new char[payload_length];
|
||||
*result_length = payload_length;
|
||||
memcpy(*result, payload, payload_length);
|
||||
|
||||
char* cursor = *result;
|
||||
char* cursor_end = *result + *result_length;
|
||||
|
||||
#ifdef __SSE2__
|
||||
while ((cursor < cursor_end) &&
|
||||
(reinterpret_cast<size_t>(cursor) & 0xf)) {
|
||||
*cursor ^= masking_key[masking_key_index];
|
||||
++cursor;
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_length;
|
||||
}
|
||||
if (cursor == cursor_end) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int kBlockSize = 16;
|
||||
__m128i masking_key_block;
|
||||
for (int i = 0; i < kBlockSize; ++i) {
|
||||
*(reinterpret_cast<char*>(&masking_key_block) + i) =
|
||||
masking_key[masking_key_index];
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_length;
|
||||
}
|
||||
|
||||
while (cursor + kBlockSize <= cursor_end) {
|
||||
__m128i payload_block =
|
||||
_mm_load_si128(reinterpret_cast<__m128i*>(cursor));
|
||||
_mm_stream_si128(reinterpret_cast<__m128i*>(cursor),
|
||||
_mm_xor_si128(payload_block, masking_key_block));
|
||||
cursor += kBlockSize;
|
||||
}
|
||||
#endif
|
||||
|
||||
while (cursor < cursor_end) {
|
||||
*cursor ^= masking_key[masking_key_index];
|
||||
++cursor;
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_length;
|
||||
}
|
||||
}
|
||||
|
||||
%}
|
@ -37,7 +37,6 @@ successfully established.
|
||||
import logging
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.handshake import draft75
|
||||
from mod_pywebsocket.handshake import hybi00
|
||||
from mod_pywebsocket.handshake import hybi
|
||||
# Export AbortedByUserException, HandshakeException, and VersionException
|
||||
@ -56,10 +55,8 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
allowDraft75: allow draft 75 handshake protocol.
|
||||
strict: Strictly check handshake request in draft 75.
|
||||
Default: False. If True, request.connection must provide
|
||||
get_memorized_lines method.
|
||||
allowDraft75: obsolete argument. ignored.
|
||||
strict: obsolete argument. ignored.
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
@ -86,9 +83,6 @@ def do_handshake(request, dispatcher, allowDraft75=False, strict=False):
|
||||
('RFC 6455', hybi.Handshaker(request, dispatcher)))
|
||||
handshakers.append(
|
||||
('HyBi 00', hybi00.Handshaker(request, dispatcher)))
|
||||
if allowDraft75:
|
||||
handshakers.append(
|
||||
('Hixie 75', draft75.Handshaker(request, dispatcher, strict)))
|
||||
|
||||
for name, handshaker in handshakers:
|
||||
_LOGGER.debug('Trying protocol version %s', name)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2011, Google Inc.
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
@ -84,39 +84,29 @@ def get_default_port(is_secure):
|
||||
return common.DEFAULT_WEB_SOCKET_PORT
|
||||
|
||||
|
||||
def validate_subprotocol(subprotocol, hixie):
|
||||
"""Validate a value in subprotocol fields such as WebSocket-Protocol,
|
||||
Sec-WebSocket-Protocol.
|
||||
def validate_subprotocol(subprotocol):
|
||||
"""Validate a value in the Sec-WebSocket-Protocol field.
|
||||
|
||||
See
|
||||
- RFC 6455: Section 4.1., 4.2.2., and 4.3.
|
||||
- HyBi 00: Section 4.1. Opening handshake
|
||||
- Hixie 75: Section 4.1. Handshake
|
||||
See the Section 4.1., 4.2.2., and 4.3. of RFC 6455.
|
||||
"""
|
||||
|
||||
if not subprotocol:
|
||||
raise HandshakeException('Invalid subprotocol name: empty')
|
||||
if hixie:
|
||||
# Parameter should be in the range U+0020 to U+007E.
|
||||
for c in subprotocol:
|
||||
if not 0x20 <= ord(c) <= 0x7e:
|
||||
raise HandshakeException(
|
||||
'Illegal character in subprotocol name: %r' % c)
|
||||
else:
|
||||
# Parameter should be encoded HTTP token.
|
||||
state = http_header_util.ParsingState(subprotocol)
|
||||
token = http_header_util.consume_token(state)
|
||||
rest = http_header_util.peek(state)
|
||||
# If |rest| is not None, |subprotocol| is not one token or invalid. If
|
||||
# |rest| is None, |token| must not be None because |subprotocol| is
|
||||
# concatenation of |token| and |rest| and is not None.
|
||||
if rest is not None:
|
||||
raise HandshakeException('Invalid non-token string in subprotocol '
|
||||
'name: %r' % rest)
|
||||
|
||||
# Parameter should be encoded HTTP token.
|
||||
state = http_header_util.ParsingState(subprotocol)
|
||||
token = http_header_util.consume_token(state)
|
||||
rest = http_header_util.peek(state)
|
||||
# If |rest| is not None, |subprotocol| is not one token or invalid. If
|
||||
# |rest| is None, |token| must not be None because |subprotocol| is
|
||||
# concatenation of |token| and |rest| and is not None.
|
||||
if rest is not None:
|
||||
raise HandshakeException('Invalid non-token string in subprotocol '
|
||||
'name: %r' % rest)
|
||||
|
||||
|
||||
def parse_host_header(request):
|
||||
fields = request.headers_in['Host'].split(':', 1)
|
||||
fields = request.headers_in[common.HOST_HEADER].split(':', 1)
|
||||
if len(fields) == 1:
|
||||
return fields[0], get_default_port(request.is_https())
|
||||
try:
|
||||
@ -129,27 +119,6 @@ def format_header(name, value):
|
||||
return '%s: %s\r\n' % (name, value)
|
||||
|
||||
|
||||
def build_location(request):
|
||||
"""Build WebSocket location for request."""
|
||||
location_parts = []
|
||||
if request.is_https():
|
||||
location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
|
||||
else:
|
||||
location_parts.append(common.WEB_SOCKET_SCHEME)
|
||||
location_parts.append('://')
|
||||
host, port = parse_host_header(request)
|
||||
connection_port = request.connection.local_addr[1]
|
||||
if port != connection_port:
|
||||
raise HandshakeException('Header/connection port mismatch: %d/%d' %
|
||||
(port, connection_port))
|
||||
location_parts.append(host)
|
||||
if (port != get_default_port(request.is_https())):
|
||||
location_parts.append(':')
|
||||
location_parts.append(str(port))
|
||||
location_parts.append(request.uri)
|
||||
return ''.join(location_parts)
|
||||
|
||||
|
||||
def get_mandatory_header(request, key):
|
||||
value = request.headers_in.get(key)
|
||||
if value is None:
|
||||
@ -170,17 +139,11 @@ def check_request_line(request):
|
||||
# 5.1 1. The three character UTF-8 string "GET".
|
||||
# 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
|
||||
if request.method != 'GET':
|
||||
raise HandshakeException('Method is not GET')
|
||||
raise HandshakeException('Method is not GET: %r' % request.method)
|
||||
|
||||
|
||||
def check_header_lines(request, mandatory_headers):
|
||||
check_request_line(request)
|
||||
|
||||
# The expected field names, and the meaning of their corresponding
|
||||
# values, are as follows.
|
||||
# |Upgrade| and |Connection|
|
||||
for key, expected_value in mandatory_headers:
|
||||
validate_mandatory_header(request, key, expected_value)
|
||||
if request.protocol != 'HTTP/1.1':
|
||||
raise HandshakeException('Version is not HTTP/1.1: %r' %
|
||||
request.protocol)
|
||||
|
||||
|
||||
def parse_token_list(data):
|
||||
@ -216,108 +179,4 @@ def parse_token_list(data):
|
||||
return token_list
|
||||
|
||||
|
||||
def _parse_extension_param(state, definition, allow_quoted_string):
|
||||
param_name = http_header_util.consume_token(state)
|
||||
|
||||
if param_name is None:
|
||||
raise HandshakeException('No valid parameter name found')
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, '='):
|
||||
definition.add_parameter(param_name, None)
|
||||
return
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if allow_quoted_string:
|
||||
# TODO(toyoshim): Add code to validate that parsed param_value is token
|
||||
param_value = http_header_util.consume_token_or_quoted_string(state)
|
||||
else:
|
||||
param_value = http_header_util.consume_token(state)
|
||||
if param_value is None:
|
||||
raise HandshakeException(
|
||||
'No valid parameter value found on the right-hand side of '
|
||||
'parameter %r' % param_name)
|
||||
|
||||
definition.add_parameter(param_name, param_value)
|
||||
|
||||
|
||||
def _parse_extension(state, allow_quoted_string):
|
||||
extension_token = http_header_util.consume_token(state)
|
||||
if extension_token is None:
|
||||
return None
|
||||
|
||||
extension = common.ExtensionParameter(extension_token)
|
||||
|
||||
while True:
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if not http_header_util.consume_string(state, ';'):
|
||||
break
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
try:
|
||||
_parse_extension_param(state, extension, allow_quoted_string)
|
||||
except HandshakeException, e:
|
||||
raise HandshakeException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: '
|
||||
'Failed to parse parameter for %r (%r)' %
|
||||
(extension_token, e))
|
||||
|
||||
return extension
|
||||
|
||||
|
||||
def parse_extensions(data, allow_quoted_string=False):
|
||||
"""Parses Sec-WebSocket-Extensions header value returns a list of
|
||||
common.ExtensionParameter objects.
|
||||
|
||||
Leading LWSes must be trimmed.
|
||||
"""
|
||||
|
||||
state = http_header_util.ParsingState(data)
|
||||
|
||||
extension_list = []
|
||||
while True:
|
||||
extension = _parse_extension(state, allow_quoted_string)
|
||||
if extension is not None:
|
||||
extension_list.append(extension)
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if http_header_util.peek(state) is None:
|
||||
break
|
||||
|
||||
if not http_header_util.consume_string(state, ','):
|
||||
raise HandshakeException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: '
|
||||
'Expected a comma but found %r' %
|
||||
http_header_util.peek(state))
|
||||
|
||||
http_header_util.consume_lwses(state)
|
||||
|
||||
if len(extension_list) == 0:
|
||||
raise HandshakeException(
|
||||
'Sec-WebSocket-Extensions header contains no valid extension')
|
||||
|
||||
return extension_list
|
||||
|
||||
|
||||
def format_extensions(extension_list):
|
||||
formatted_extension_list = []
|
||||
for extension in extension_list:
|
||||
formatted_params = [extension.name()]
|
||||
for param_name, param_value in extension.get_parameters():
|
||||
if param_value is None:
|
||||
formatted_params.append(param_name)
|
||||
else:
|
||||
quoted_value = http_header_util.quote_if_necessary(param_value)
|
||||
formatted_params.append('%s=%s' % (param_name, quoted_value))
|
||||
|
||||
formatted_extension_list.append('; '.join(formatted_params))
|
||||
|
||||
return ', '.join(formatted_extension_list)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -1,190 +0,0 @@
|
||||
# Copyright 2011, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75."""
|
||||
|
||||
|
||||
# Note: request.connection.write is used in this module, even though mod_python
|
||||
# document says that it should be used only in connection handlers.
|
||||
# Unfortunately, we have no other options. For example, request.write is not
|
||||
# suitable because it doesn't allow direct raw bytes writing.
|
||||
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.stream import StreamHixie75
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake._base import HandshakeException
|
||||
from mod_pywebsocket.handshake._base import build_location
|
||||
from mod_pywebsocket.handshake._base import validate_subprotocol
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
# key, expected value or None
|
||||
['Upgrade', 'WebSocket'],
|
||||
['Connection', 'Upgrade'],
|
||||
['Host', None],
|
||||
['Origin', None],
|
||||
]
|
||||
|
||||
_FIRST_FIVE_LINES = map(re.compile, [
|
||||
r'^GET /[\S]* HTTP/1.1\r\n$',
|
||||
r'^Upgrade: WebSocket\r\n$',
|
||||
r'^Connection: Upgrade\r\n$',
|
||||
r'^Host: [\S]+\r\n$',
|
||||
r'^Origin: [\S]+\r\n$',
|
||||
])
|
||||
|
||||
_SIXTH_AND_LATER = re.compile(
|
||||
r'^'
|
||||
r'(WebSocket-Protocol: [\x20-\x7e]+\r\n)?'
|
||||
r'(Cookie: [^\r]*\r\n)*'
|
||||
r'(Cookie2: [^\r]*\r\n)?'
|
||||
r'(Cookie: [^\r]*\r\n)*'
|
||||
r'\r\n')
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""This class performs WebSocket handshake."""
|
||||
|
||||
def __init__(self, request, dispatcher, strict=False):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
dispatcher: Dispatcher (dispatch.Dispatcher).
|
||||
strict: Strictly check handshake request. Default: False.
|
||||
If True, request.connection must provide get_memorized_lines
|
||||
method.
|
||||
|
||||
Handshaker will add attributes such as ws_resource in performing
|
||||
handshake.
|
||||
"""
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self._request = request
|
||||
self._dispatcher = dispatcher
|
||||
self._strict = strict
|
||||
|
||||
def do_handshake(self):
|
||||
"""Perform WebSocket Handshake.
|
||||
|
||||
On _request, we set
|
||||
ws_resource, ws_origin, ws_location, ws_protocol
|
||||
ws_challenge_md5: WebSocket handshake information.
|
||||
ws_stream: Frame generation/parsing class.
|
||||
ws_version: Protocol version.
|
||||
"""
|
||||
|
||||
self._check_header_lines()
|
||||
self._set_resource()
|
||||
self._set_origin()
|
||||
self._set_location()
|
||||
self._set_subprotocol()
|
||||
self._set_protocol_version()
|
||||
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
|
||||
self._send_handshake()
|
||||
|
||||
self._logger.debug('Sent opening handshake response')
|
||||
|
||||
def _set_resource(self):
|
||||
self._request.ws_resource = self._request.uri
|
||||
|
||||
def _set_origin(self):
|
||||
self._request.ws_origin = self._request.headers_in['Origin']
|
||||
|
||||
def _set_location(self):
|
||||
self._request.ws_location = build_location(self._request)
|
||||
|
||||
def _set_subprotocol(self):
|
||||
subprotocol = self._request.headers_in.get('WebSocket-Protocol')
|
||||
if subprotocol is not None:
|
||||
validate_subprotocol(subprotocol, hixie=True)
|
||||
self._request.ws_protocol = subprotocol
|
||||
|
||||
def _set_protocol_version(self):
|
||||
self._logger.debug('IETF Hixie 75 protocol')
|
||||
self._request.ws_version = common.VERSION_HIXIE75
|
||||
self._request.ws_stream = StreamHixie75(self._request)
|
||||
|
||||
def _sendall(self, data):
|
||||
self._request.connection.write(data)
|
||||
|
||||
def _send_handshake(self):
|
||||
self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
|
||||
self._sendall('Upgrade: WebSocket\r\n')
|
||||
self._sendall('Connection: Upgrade\r\n')
|
||||
self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin)
|
||||
self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location)
|
||||
if self._request.ws_protocol:
|
||||
self._sendall(
|
||||
'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol)
|
||||
self._sendall('\r\n')
|
||||
|
||||
def _check_header_lines(self):
|
||||
for key, expected_value in _MANDATORY_HEADERS:
|
||||
actual_value = self._request.headers_in.get(key)
|
||||
if not actual_value:
|
||||
raise HandshakeException('Header %s is not defined' % key)
|
||||
if expected_value:
|
||||
if actual_value != expected_value:
|
||||
raise HandshakeException(
|
||||
'Expected %r for header %s but found %r' %
|
||||
(expected_value, key, actual_value))
|
||||
if self._strict:
|
||||
try:
|
||||
lines = self._request.connection.get_memorized_lines()
|
||||
except AttributeError, e:
|
||||
raise AttributeError(
|
||||
'Strict handshake is specified but the connection '
|
||||
'doesn\'t provide get_memorized_lines()')
|
||||
self._check_first_lines(lines)
|
||||
|
||||
def _check_first_lines(self, lines):
|
||||
if len(lines) < len(_FIRST_FIVE_LINES):
|
||||
raise HandshakeException('Too few header lines: %d' % len(lines))
|
||||
for line, regexp in zip(lines, _FIRST_FIVE_LINES):
|
||||
if not regexp.search(line):
|
||||
raise HandshakeException(
|
||||
'Unexpected header: %r doesn\'t match %r'
|
||||
% (line, regexp.pattern))
|
||||
sixth_and_later = ''.join(lines[5:])
|
||||
if not _SIXTH_AND_LATER.search(sixth_and_later):
|
||||
raise HandshakeException(
|
||||
'Unexpected header: %r doesn\'t match %r'
|
||||
% (sixth_and_later, _SIXTH_AND_LATER.pattern))
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2011, Google Inc.
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
@ -49,12 +49,11 @@ import re
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket.extensions import get_extension_processor
|
||||
from mod_pywebsocket.extensions import is_compression_extension
|
||||
from mod_pywebsocket.handshake._base import check_request_line
|
||||
from mod_pywebsocket.handshake._base import format_extensions
|
||||
from mod_pywebsocket.handshake._base import format_header
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
from mod_pywebsocket.handshake._base import HandshakeException
|
||||
from mod_pywebsocket.handshake._base import parse_extensions
|
||||
from mod_pywebsocket.handshake._base import parse_token_list
|
||||
from mod_pywebsocket.handshake._base import validate_mandatory_header
|
||||
from mod_pywebsocket.handshake._base import validate_subprotocol
|
||||
@ -70,13 +69,10 @@ from mod_pywebsocket import util
|
||||
_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$')
|
||||
|
||||
# Defining aliases for values used frequently.
|
||||
_VERSION_HYBI08 = common.VERSION_HYBI08
|
||||
_VERSION_HYBI08_STRING = str(_VERSION_HYBI08)
|
||||
_VERSION_LATEST = common.VERSION_HYBI_LATEST
|
||||
_VERSION_LATEST_STRING = str(_VERSION_LATEST)
|
||||
_SUPPORTED_VERSIONS = [
|
||||
_VERSION_LATEST,
|
||||
_VERSION_HYBI08,
|
||||
]
|
||||
|
||||
|
||||
@ -151,9 +147,6 @@ class Handshaker(object):
|
||||
|
||||
self._request.ws_version = self._check_version()
|
||||
|
||||
# This handshake must be based on latest hybi. We are responsible to
|
||||
# fallback to HTTP on handshake failure as latest hybi handshake
|
||||
# specifies.
|
||||
try:
|
||||
self._get_origin()
|
||||
self._set_protocol()
|
||||
@ -182,16 +175,58 @@ class Handshaker(object):
|
||||
processors.append(processor)
|
||||
self._request.ws_extension_processors = processors
|
||||
|
||||
# List of extra headers. The extra handshake handler may add header
|
||||
# data as name/value pairs to this list and pywebsocket appends
|
||||
# them to the WebSocket handshake.
|
||||
self._request.extra_headers = []
|
||||
|
||||
# Extra handshake handler may modify/remove processors.
|
||||
self._dispatcher.do_extra_handshake(self._request)
|
||||
processors = filter(lambda processor: processor is not None,
|
||||
self._request.ws_extension_processors)
|
||||
|
||||
# Ask each processor if there are extensions on the request which
|
||||
# cannot co-exist. When processor decided other processors cannot
|
||||
# co-exist with it, the processor marks them (or itself) as
|
||||
# "inactive". The first extension processor has the right to
|
||||
# make the final call.
|
||||
for processor in reversed(processors):
|
||||
if processor.is_active():
|
||||
processor.check_consistency_with_other_processors(
|
||||
processors)
|
||||
processors = filter(lambda processor: processor.is_active(),
|
||||
processors)
|
||||
|
||||
accepted_extensions = []
|
||||
|
||||
# We need to take into account of mux extension here.
|
||||
# If mux extension exists:
|
||||
# - Remove processors of extensions for logical channel,
|
||||
# which are processors located before the mux processor
|
||||
# - Pass extension requests for logical channel to mux processor
|
||||
# - Attach the mux processor to the request. It will be referred
|
||||
# by dispatcher to see whether the dispatcher should use mux
|
||||
# handler or not.
|
||||
mux_index = -1
|
||||
for i, processor in enumerate(processors):
|
||||
if processor.name() == common.MUX_EXTENSION:
|
||||
mux_index = i
|
||||
break
|
||||
if mux_index >= 0:
|
||||
logical_channel_extensions = []
|
||||
for processor in processors[:mux_index]:
|
||||
logical_channel_extensions.append(processor.request())
|
||||
processor.set_active(False)
|
||||
self._request.mux_processor = processors[mux_index]
|
||||
self._request.mux_processor.set_extensions(
|
||||
logical_channel_extensions)
|
||||
processors = filter(lambda processor: processor.is_active(),
|
||||
processors)
|
||||
|
||||
stream_options = StreamOptions()
|
||||
|
||||
self._request.ws_extensions = None
|
||||
for processor in self._request.ws_extension_processors:
|
||||
if processor is None:
|
||||
# Some processors may be removed by extra handshake
|
||||
# handler.
|
||||
for index, processor in enumerate(processors):
|
||||
if not processor.is_active():
|
||||
continue
|
||||
|
||||
extension_response = processor.get_extension_response()
|
||||
@ -199,26 +234,34 @@ class Handshaker(object):
|
||||
# Rejected.
|
||||
continue
|
||||
|
||||
if self._request.ws_extensions is None:
|
||||
self._request.ws_extensions = []
|
||||
self._request.ws_extensions.append(extension_response)
|
||||
accepted_extensions.append(extension_response)
|
||||
|
||||
processor.setup_stream_options(stream_options)
|
||||
|
||||
if self._request.ws_extensions is not None:
|
||||
if not is_compression_extension(processor.name()):
|
||||
continue
|
||||
|
||||
# Inactivate all of the following compression extensions.
|
||||
for j in xrange(index + 1, len(processors)):
|
||||
if is_compression_extension(processors[j].name()):
|
||||
processors[j].set_active(False)
|
||||
|
||||
if len(accepted_extensions) > 0:
|
||||
self._request.ws_extensions = accepted_extensions
|
||||
self._logger.debug(
|
||||
'Extensions accepted: %r',
|
||||
map(common.ExtensionParameter.name,
|
||||
self._request.ws_extensions))
|
||||
map(common.ExtensionParameter.name, accepted_extensions))
|
||||
else:
|
||||
self._request.ws_extensions = None
|
||||
|
||||
self._request.ws_stream = Stream(self._request, stream_options)
|
||||
self._request.ws_stream = self._create_stream(stream_options)
|
||||
|
||||
if self._request.ws_requested_protocols is not None:
|
||||
if self._request.ws_protocol is None:
|
||||
raise HandshakeException(
|
||||
'do_extra_handshake must choose one subprotocol from '
|
||||
'ws_requested_protocols and set it to ws_protocol')
|
||||
validate_subprotocol(self._request.ws_protocol, hixie=False)
|
||||
validate_subprotocol(self._request.ws_protocol)
|
||||
|
||||
self._logger.debug(
|
||||
'Subprotocol accepted: %r',
|
||||
@ -237,10 +280,7 @@ class Handshaker(object):
|
||||
raise e
|
||||
|
||||
def _get_origin(self):
|
||||
if self._request.ws_version is _VERSION_HYBI08:
|
||||
origin_header = common.SEC_WEBSOCKET_ORIGIN_HEADER
|
||||
else:
|
||||
origin_header = common.ORIGIN_HEADER
|
||||
origin_header = common.ORIGIN_HEADER
|
||||
origin = self._request.headers_in.get(origin_header)
|
||||
if origin is None:
|
||||
self._logger.debug('Client request does not have origin header')
|
||||
@ -249,8 +289,6 @@ class Handshaker(object):
|
||||
def _check_version(self):
|
||||
version = get_mandatory_header(self._request,
|
||||
common.SEC_WEBSOCKET_VERSION_HEADER)
|
||||
if version == _VERSION_HYBI08_STRING:
|
||||
return _VERSION_HYBI08
|
||||
if version == _VERSION_LATEST_STRING:
|
||||
return _VERSION_LATEST
|
||||
|
||||
@ -273,7 +311,7 @@ class Handshaker(object):
|
||||
protocol_header = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
|
||||
if not protocol_header:
|
||||
if protocol_header is None:
|
||||
self._request.ws_requested_protocols = None
|
||||
return
|
||||
|
||||
@ -289,12 +327,12 @@ class Handshaker(object):
|
||||
self._request.ws_requested_extensions = None
|
||||
return
|
||||
|
||||
if self._request.ws_version is common.VERSION_HYBI08:
|
||||
allow_quoted_string=False
|
||||
else:
|
||||
allow_quoted_string=True
|
||||
self._request.ws_requested_extensions = parse_extensions(
|
||||
extensions_header, allow_quoted_string=allow_quoted_string)
|
||||
try:
|
||||
self._request.ws_requested_extensions = common.parse_extensions(
|
||||
extensions_header)
|
||||
except common.ExtensionParsingException, e:
|
||||
raise HandshakeException(
|
||||
'Failed to parse Sec-WebSocket-Extensions header: %r' % e)
|
||||
|
||||
self._logger.debug(
|
||||
'Extensions requested: %r',
|
||||
@ -342,11 +380,15 @@ class Handshaker(object):
|
||||
|
||||
return key
|
||||
|
||||
def _send_handshake(self, accept):
|
||||
def _create_stream(self, stream_options):
|
||||
return Stream(self._request, stream_options)
|
||||
|
||||
def _create_handshake_response(self, accept):
|
||||
response = []
|
||||
|
||||
response.append('HTTP/1.1 101 Switching Protocols\r\n')
|
||||
|
||||
# WebSocket headers
|
||||
response.append(format_header(
|
||||
common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE))
|
||||
response.append(format_header(
|
||||
@ -361,15 +403,23 @@ class Handshaker(object):
|
||||
len(self._request.ws_extensions) != 0):
|
||||
response.append(format_header(
|
||||
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
|
||||
format_extensions(self._request.ws_extensions)))
|
||||
common.format_extensions(self._request.ws_extensions)))
|
||||
# MOZILLA: Add HSTS header if requested to
|
||||
if self._request.sts is not None:
|
||||
response.append(format_header("Strict-Transport-Security",
|
||||
self._request.sts))
|
||||
# /MOZILLA
|
||||
|
||||
# Headers not specific for WebSocket
|
||||
for name, value in self._request.extra_headers:
|
||||
response.append(format_header(name, value))
|
||||
|
||||
response.append('\r\n')
|
||||
|
||||
raw_response = ''.join(response)
|
||||
return ''.join(response)
|
||||
|
||||
def _send_handshake(self, accept):
|
||||
raw_response = self._create_handshake_response(accept)
|
||||
self._request.connection.write(raw_response)
|
||||
self._logger.debug('Sent server\'s opening handshake: %r',
|
||||
raw_response)
|
||||
|
@ -51,11 +51,12 @@ from mod_pywebsocket import common
|
||||
from mod_pywebsocket.stream import StreamHixie75
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.handshake._base import HandshakeException
|
||||
from mod_pywebsocket.handshake._base import build_location
|
||||
from mod_pywebsocket.handshake._base import check_header_lines
|
||||
from mod_pywebsocket.handshake._base import check_request_line
|
||||
from mod_pywebsocket.handshake._base import format_header
|
||||
from mod_pywebsocket.handshake._base import get_default_port
|
||||
from mod_pywebsocket.handshake._base import get_mandatory_header
|
||||
from mod_pywebsocket.handshake._base import validate_subprotocol
|
||||
from mod_pywebsocket.handshake._base import parse_host_header
|
||||
from mod_pywebsocket.handshake._base import validate_mandatory_header
|
||||
|
||||
|
||||
_MANDATORY_HEADERS = [
|
||||
@ -65,6 +66,56 @@ _MANDATORY_HEADERS = [
|
||||
]
|
||||
|
||||
|
||||
def _validate_subprotocol(subprotocol):
|
||||
"""Checks if characters in subprotocol are in range between U+0020 and
|
||||
U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this
|
||||
requirement.
|
||||
|
||||
See the Section 4.1. Opening handshake of the spec.
|
||||
"""
|
||||
|
||||
if not subprotocol:
|
||||
raise HandshakeException('Invalid subprotocol name: empty')
|
||||
|
||||
# Parameter should be in the range U+0020 to U+007E.
|
||||
for c in subprotocol:
|
||||
if not 0x20 <= ord(c) <= 0x7e:
|
||||
raise HandshakeException(
|
||||
'Illegal character in subprotocol name: %r' % c)
|
||||
|
||||
|
||||
def _check_header_lines(request, mandatory_headers):
|
||||
check_request_line(request)
|
||||
|
||||
# The expected field names, and the meaning of their corresponding
|
||||
# values, are as follows.
|
||||
# |Upgrade| and |Connection|
|
||||
for key, expected_value in mandatory_headers:
|
||||
validate_mandatory_header(request, key, expected_value)
|
||||
|
||||
|
||||
def _build_location(request):
|
||||
"""Build WebSocket location for request."""
|
||||
|
||||
location_parts = []
|
||||
if request.is_https():
|
||||
location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
|
||||
else:
|
||||
location_parts.append(common.WEB_SOCKET_SCHEME)
|
||||
location_parts.append('://')
|
||||
host, port = parse_host_header(request)
|
||||
connection_port = request.connection.local_addr[1]
|
||||
if port != connection_port:
|
||||
raise HandshakeException('Header/connection port mismatch: %d/%d' %
|
||||
(port, connection_port))
|
||||
location_parts.append(host)
|
||||
if (port != get_default_port(request.is_https())):
|
||||
location_parts.append(':')
|
||||
location_parts.append(str(port))
|
||||
location_parts.append(request.unparsed_uri)
|
||||
return ''.join(location_parts)
|
||||
|
||||
|
||||
class Handshaker(object):
|
||||
"""Opening handshake processor for the WebSocket protocol version HyBi 00.
|
||||
"""
|
||||
@ -101,7 +152,7 @@ class Handshaker(object):
|
||||
|
||||
# 5.1 Reading the client's opening handshake.
|
||||
# dispatcher sets it in self._request.
|
||||
check_header_lines(self._request, _MANDATORY_HEADERS)
|
||||
_check_header_lines(self._request, _MANDATORY_HEADERS)
|
||||
self._set_resource()
|
||||
self._set_subprotocol()
|
||||
self._set_location()
|
||||
@ -121,14 +172,14 @@ class Handshaker(object):
|
||||
subprotocol = self._request.headers_in.get(
|
||||
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
|
||||
if subprotocol is not None:
|
||||
validate_subprotocol(subprotocol, hixie=True)
|
||||
_validate_subprotocol(subprotocol)
|
||||
self._request.ws_protocol = subprotocol
|
||||
|
||||
def _set_location(self):
|
||||
# |Host|
|
||||
host = self._request.headers_in.get(common.HOST_HEADER)
|
||||
if host is not None:
|
||||
self._request.ws_location = build_location(self._request)
|
||||
self._request.ws_location = _build_location(self._request)
|
||||
# TODO(ukai): check host is this host.
|
||||
|
||||
def _set_origin(self):
|
||||
|
@ -63,8 +63,9 @@ _PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT = (
|
||||
_PYOPT_ALLOW_HANDLERS_OUTSIDE_ROOT_DEFINITION = {
|
||||
'off': False, 'no': False, 'on': True, 'yes': True}
|
||||
|
||||
# PythonOption to specify to allow draft75 handshake.
|
||||
# The default is None (Off)
|
||||
# (Obsolete option. Ignored.)
|
||||
# PythonOption to specify to allow handshake defined in Hixie 75 version
|
||||
# protocol. The default is None (Off)
|
||||
_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
|
||||
# Map from values to their meanings.
|
||||
_PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True}
|
||||
@ -166,7 +167,9 @@ def _create_dispatcher():
|
||||
handler_root, handler_scan, allow_handlers_outside_root)
|
||||
|
||||
for warning in dispatcher.source_warnings():
|
||||
apache.log_error('mod_pywebsocket: %s' % warning, apache.APLOG_WARNING)
|
||||
apache.log_error(
|
||||
'mod_pywebsocket: Warning in source loading: %s' % warning,
|
||||
apache.APLOG_WARNING)
|
||||
|
||||
return dispatcher
|
||||
|
||||
@ -190,12 +193,16 @@ def headerparserhandler(request):
|
||||
# Fallback to default http handler for request paths for which
|
||||
# we don't have request handlers.
|
||||
if not _dispatcher.get_handler_suite(request.uri):
|
||||
request.log_error('No handler for resource: %r' % request.uri,
|
||||
apache.APLOG_INFO)
|
||||
request.log_error('Fallback to Apache', apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: No handler for resource: %r' % request.uri,
|
||||
apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Fallback to Apache', apache.APLOG_INFO)
|
||||
return apache.DECLINED
|
||||
except dispatch.DispatchException, e:
|
||||
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Dispatch failed for error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
if not handshake_is_done:
|
||||
return e.status
|
||||
|
||||
@ -209,26 +216,30 @@ def headerparserhandler(request):
|
||||
handshake.do_handshake(
|
||||
request, _dispatcher, allowDraft75=allow_draft75)
|
||||
except handshake.VersionException, e:
|
||||
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Handshake failed for version error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
request.err_headers_out.add(common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
e.supported_versions)
|
||||
return apache.HTTP_BAD_REQUEST
|
||||
except handshake.HandshakeException, e:
|
||||
# Handshake for ws/wss failed.
|
||||
# Send http response with error status.
|
||||
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
|
||||
request.log_error(
|
||||
'mod_pywebsocket: Handshake failed for error: %s' % e,
|
||||
apache.APLOG_INFO)
|
||||
return e.status
|
||||
|
||||
handshake_is_done = True
|
||||
request._dispatcher = _dispatcher
|
||||
_dispatcher.transfer_data(request)
|
||||
except handshake.AbortedByUserException, e:
|
||||
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
|
||||
request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO)
|
||||
except Exception, e:
|
||||
# DispatchException can also be thrown if something is wrong in
|
||||
# pywebsocket code. It's caught here, then.
|
||||
|
||||
request.log_error('mod_pywebsocket: %s\n%s' %
|
||||
request.log_error('mod_pywebsocket: Exception occurred: %s\n%s' %
|
||||
(e, util.get_stack_trace()),
|
||||
apache.APLOG_ERR)
|
||||
# Unknown exceptions before handshake mean Apache must handle its
|
||||
|
@ -59,20 +59,20 @@ def close_connection(request):
|
||||
request.ws_stream.close_connection()
|
||||
|
||||
|
||||
def send_message(request, message, end=True, binary=False):
|
||||
"""Send message.
|
||||
def send_message(request, payload_data, end=True, binary=False):
|
||||
"""Send a message (or part of a message).
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
message: unicode text or str binary to send.
|
||||
end: False to send message as a fragment. All messages until the
|
||||
first call with end=True (inclusive) will be delivered to the
|
||||
client in separate frames but as one WebSocket message.
|
||||
binary: send message as binary frame.
|
||||
payload_data: unicode text or str binary to send.
|
||||
end: True to terminate a message.
|
||||
False to send payload_data as part of a message that is to be
|
||||
terminated by next or later send_message call with end=True.
|
||||
binary: send payload_data as binary frame(s).
|
||||
Raises:
|
||||
BadOperationException: when server already terminated.
|
||||
"""
|
||||
request.ws_stream.send_message(message, end, binary)
|
||||
request.ws_stream.send_message(payload_data, end, binary)
|
||||
|
||||
|
||||
def receive_message(request):
|
||||
|
1889
testing/mochitest/pywebsocket/mod_pywebsocket/mux.py
Normal file
1889
testing/mochitest/pywebsocket/mod_pywebsocket/mux.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,7 @@ from mod_pywebsocket._stream_hybi import create_ping_frame
|
||||
from mod_pywebsocket._stream_hybi import create_pong_frame
|
||||
from mod_pywebsocket._stream_hybi import create_binary_frame
|
||||
from mod_pywebsocket._stream_hybi import create_text_frame
|
||||
from mod_pywebsocket._stream_hybi import create_closing_handshake_body
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -56,6 +56,11 @@ import socket
|
||||
import traceback
|
||||
import zlib
|
||||
|
||||
try:
|
||||
from mod_pywebsocket import fast_masking
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def get_stack_trace():
|
||||
"""Get the current stack trace as string.
|
||||
@ -169,45 +174,39 @@ class RepeatedXorMasker(object):
|
||||
ended and resumes from that point on the next mask method call.
|
||||
"""
|
||||
|
||||
def __init__(self, mask):
|
||||
self._mask = map(ord, mask)
|
||||
self._mask_size = len(self._mask)
|
||||
self._count = 0
|
||||
def __init__(self, masking_key):
|
||||
self._masking_key = masking_key
|
||||
self._masking_key_index = 0
|
||||
|
||||
def mask(self, s):
|
||||
def _mask_using_swig(self, s):
|
||||
masked_data = fast_masking.mask(
|
||||
s, self._masking_key, self._masking_key_index)
|
||||
self._masking_key_index = (
|
||||
(self._masking_key_index + len(s)) % len(self._masking_key))
|
||||
return masked_data
|
||||
|
||||
def _mask_using_array(self, s):
|
||||
result = array.array('B')
|
||||
result.fromstring(s)
|
||||
|
||||
# Use temporary local variables to eliminate the cost to access
|
||||
# attributes
|
||||
count = self._count
|
||||
mask = self._mask
|
||||
mask_size = self._mask_size
|
||||
masking_key = map(ord, self._masking_key)
|
||||
masking_key_size = len(masking_key)
|
||||
masking_key_index = self._masking_key_index
|
||||
|
||||
for i in xrange(len(result)):
|
||||
result[i] ^= mask[count]
|
||||
count = (count + 1) % mask_size
|
||||
self._count = count
|
||||
result[i] ^= masking_key[masking_key_index]
|
||||
masking_key_index = (masking_key_index + 1) % masking_key_size
|
||||
|
||||
self._masking_key_index = masking_key_index
|
||||
|
||||
return result.tostring()
|
||||
|
||||
|
||||
class DeflateRequest(object):
|
||||
"""A wrapper class for request object to intercept send and recv to perform
|
||||
deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
self._request = request
|
||||
self.connection = DeflateConnection(request.connection)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in ('_request', 'connection'):
|
||||
return object.__getattribute__(self, name)
|
||||
return self._request.__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ('_request', 'connection'):
|
||||
return object.__setattr__(self, name, value)
|
||||
return self._request.__setattr__(name, value)
|
||||
if 'fast_masking' in globals():
|
||||
mask = _mask_using_swig
|
||||
else:
|
||||
mask = _mask_using_array
|
||||
|
||||
|
||||
# By making wbits option negative, we can suppress CMF/FLG (2 octet) and
|
||||
@ -232,6 +231,12 @@ class _Deflater(object):
|
||||
self._compress = zlib.compressobj(
|
||||
zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -window_bits)
|
||||
|
||||
def compress(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
self._logger.debug('Compress input %r', bytes)
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
def compress_and_flush(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH)
|
||||
@ -239,11 +244,19 @@ class _Deflater(object):
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
def compress_and_finish(self, bytes):
|
||||
compressed_bytes = self._compress.compress(bytes)
|
||||
compressed_bytes += self._compress.flush(zlib.Z_FINISH)
|
||||
self._logger.debug('Compress input %r', bytes)
|
||||
self._logger.debug('Compress result %r', compressed_bytes)
|
||||
return compressed_bytes
|
||||
|
||||
|
||||
class _Inflater(object):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, window_bits):
|
||||
self._logger = get_class_logger(self)
|
||||
self._window_bits = window_bits
|
||||
|
||||
self._unconsumed = ''
|
||||
|
||||
@ -300,7 +313,7 @@ class _Inflater(object):
|
||||
|
||||
def reset(self):
|
||||
self._logger.debug('Reset')
|
||||
self._decompress = zlib.decompressobj(-zlib.MAX_WBITS)
|
||||
self._decompress = zlib.decompressobj(-self._window_bits)
|
||||
|
||||
|
||||
# Compresses/decompresses given octets using the method introduced in RFC1979.
|
||||
@ -318,13 +331,27 @@ class _RFC1979Deflater(object):
|
||||
self._window_bits = window_bits
|
||||
self._no_context_takeover = no_context_takeover
|
||||
|
||||
def filter(self, bytes):
|
||||
if self._deflater is None or self._no_context_takeover:
|
||||
def filter(self, bytes, end=True, bfinal=False):
|
||||
if self._deflater is None:
|
||||
self._deflater = _Deflater(self._window_bits)
|
||||
|
||||
# Strip last 4 octets which is LEN and NLEN field of a non-compressed
|
||||
# block added for Z_SYNC_FLUSH.
|
||||
return self._deflater.compress_and_flush(bytes)[:-4]
|
||||
if bfinal:
|
||||
result = self._deflater.compress_and_finish(bytes)
|
||||
# Add a padding block with BFINAL = 0 and BTYPE = 0.
|
||||
result = result + chr(0)
|
||||
self._deflater = None
|
||||
return result
|
||||
|
||||
result = self._deflater.compress_and_flush(bytes)
|
||||
if end:
|
||||
# Strip last 4 octets which is LEN and NLEN field of a
|
||||
# non-compressed block added for Z_SYNC_FLUSH.
|
||||
result = result[:-4]
|
||||
|
||||
if self._no_context_takeover and end:
|
||||
self._deflater = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _RFC1979Inflater(object):
|
||||
@ -332,8 +359,8 @@ class _RFC1979Inflater(object):
|
||||
the algorithm described in the RFC1979 section 2.1.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._inflater = _Inflater()
|
||||
def __init__(self, window_bits=zlib.MAX_WBITS):
|
||||
self._inflater = _Inflater(window_bits)
|
||||
|
||||
def filter(self, bytes):
|
||||
# Restore stripped LEN and NLEN field of a non-compressed block added
|
||||
@ -356,7 +383,7 @@ class DeflateSocket(object):
|
||||
self._logger = get_class_logger(self)
|
||||
|
||||
self._deflater = _Deflater(zlib.MAX_WBITS)
|
||||
self._inflater = _Inflater()
|
||||
self._inflater = _Inflater(zlib.MAX_WBITS)
|
||||
|
||||
def recv(self, size):
|
||||
"""Receives data from the socket specified on the construction up
|
||||
@ -386,111 +413,4 @@ class DeflateSocket(object):
|
||||
return len(bytes)
|
||||
|
||||
|
||||
class DeflateConnection(object):
|
||||
"""A wrapper class for request object to intercept write and read to
|
||||
perform deflate compression and decompression transparently.
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
|
||||
self._logger = get_class_logger(self)
|
||||
|
||||
self._deflater = _Deflater(zlib.MAX_WBITS)
|
||||
self._inflater = _Inflater()
|
||||
|
||||
def get_remote_addr(self):
|
||||
return self._connection.remote_addr
|
||||
remote_addr = property(get_remote_addr)
|
||||
|
||||
def put_bytes(self, bytes):
|
||||
self.write(bytes)
|
||||
|
||||
def read(self, size=-1):
|
||||
"""Reads at most size bytes. Blocks until there's at least one byte
|
||||
available.
|
||||
"""
|
||||
|
||||
# TODO(tyoshino): Allow call with size=0.
|
||||
if not (size == -1 or size > 0):
|
||||
raise Exception('size must be -1 or positive')
|
||||
|
||||
data = ''
|
||||
while True:
|
||||
if size == -1:
|
||||
data += self._inflater.decompress(-1)
|
||||
else:
|
||||
data += self._inflater.decompress(size - len(data))
|
||||
|
||||
if size >= 0 and len(data) != 0:
|
||||
break
|
||||
|
||||
# TODO(tyoshino): Make this read efficient by some workaround.
|
||||
#
|
||||
# In 3.0.3 and prior of mod_python, read blocks until length bytes
|
||||
# was read. We don't know the exact size to read while using
|
||||
# deflate, so read byte-by-byte.
|
||||
#
|
||||
# _StandaloneRequest.read that ultimately performs
|
||||
# socket._fileobject.read also blocks until length bytes was read
|
||||
read_data = self._connection.read(1)
|
||||
if not read_data:
|
||||
break
|
||||
self._inflater.append(read_data)
|
||||
return data
|
||||
|
||||
def write(self, bytes):
|
||||
self._connection.write(self._deflater.compress_and_flush(bytes))
|
||||
|
||||
|
||||
def _is_ewouldblock_errno(error_number):
|
||||
"""Returns True iff error_number indicates that receive operation would
|
||||
block. To make this portable, we check availability of errno and then
|
||||
compare them.
|
||||
"""
|
||||
|
||||
for error_name in ['WSAEWOULDBLOCK', 'EWOULDBLOCK', 'EAGAIN']:
|
||||
if (error_name in dir(errno) and
|
||||
error_number == getattr(errno, error_name)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def drain_received_data(raw_socket):
|
||||
# Set the socket non-blocking.
|
||||
original_timeout = raw_socket.gettimeout()
|
||||
raw_socket.settimeout(0.0)
|
||||
|
||||
drained_data = []
|
||||
|
||||
# Drain until the socket is closed or no data is immediately
|
||||
# available for read.
|
||||
while True:
|
||||
try:
|
||||
data = raw_socket.recv(1)
|
||||
if not data:
|
||||
break
|
||||
drained_data.append(data)
|
||||
except socket.error, e:
|
||||
# e can be either a pair (errno, string) or just a string (or
|
||||
# something else) telling what went wrong. We suppress only
|
||||
# the errors that indicates that the socket blocks. Those
|
||||
# exceptions can be parsed as a pair (errno, string).
|
||||
try:
|
||||
error_number, message = e
|
||||
except:
|
||||
# Failed to parse socket.error.
|
||||
raise e
|
||||
|
||||
if _is_ewouldblock_errno(error_number):
|
||||
break
|
||||
else:
|
||||
raise e
|
||||
|
||||
# Rollback timeout value.
|
||||
raw_socket.settimeout(original_timeout)
|
||||
|
||||
return ''.join(drained_data)
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
||||
|
@ -0,0 +1,109 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the COPYING file or at
|
||||
# https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
class XHRBenchmarkHandler(object):
|
||||
def __init__(self, headers, rfile, wfile):
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self.headers = headers
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
|
||||
def do_send(self):
|
||||
content_length = int(self.headers.getheader('Content-Length'))
|
||||
|
||||
self._logger.debug('Requested to receive %s bytes', content_length)
|
||||
|
||||
RECEIVE_BLOCK_SIZE = 1024 * 1024
|
||||
|
||||
bytes_to_receive = content_length
|
||||
while bytes_to_receive > 0:
|
||||
bytes_to_receive_in_this_loop = bytes_to_receive
|
||||
if bytes_to_receive_in_this_loop > RECEIVE_BLOCK_SIZE:
|
||||
bytes_to_receive_in_this_loop = RECEIVE_BLOCK_SIZE
|
||||
received_data = self.rfile.read(bytes_to_receive_in_this_loop)
|
||||
if received_data != ('a' * bytes_to_receive_in_this_loop):
|
||||
self._logger.debug('Request body verification failed')
|
||||
return
|
||||
bytes_to_receive -= len(received_data)
|
||||
if bytes_to_receive < 0:
|
||||
self._logger.debug('Received %d more bytes than expected' %
|
||||
(-bytes_to_receive))
|
||||
return
|
||||
|
||||
# Return the number of received bytes back to the client.
|
||||
response_body = '%d' % content_length
|
||||
self.wfile.write(
|
||||
'HTTP/1.1 200 OK\r\n'
|
||||
'Content-Type: text/html\r\n'
|
||||
'Content-Length: %d\r\n'
|
||||
'\r\n%s' % (len(response_body), response_body))
|
||||
self.wfile.flush()
|
||||
|
||||
def do_receive(self):
|
||||
content_length = int(self.headers.getheader('Content-Length'))
|
||||
request_body = self.rfile.read(content_length)
|
||||
|
||||
request_array = request_body.split(' ')
|
||||
if len(request_array) < 2:
|
||||
self._logger.debug('Malformed request body: %r', request_body)
|
||||
return
|
||||
|
||||
# Parse the size parameter.
|
||||
bytes_to_send = request_array[0]
|
||||
try:
|
||||
bytes_to_send = int(bytes_to_send)
|
||||
except ValueError, e:
|
||||
self._logger.debug('Malformed size parameter: %r', bytes_to_send)
|
||||
return
|
||||
self._logger.debug('Requested to send %s bytes', bytes_to_send)
|
||||
|
||||
# Parse the transfer encoding parameter.
|
||||
chunked_mode = False
|
||||
mode_parameter = request_array[1]
|
||||
if mode_parameter == 'chunked':
|
||||
self._logger.debug('Requested chunked transfer encoding')
|
||||
chunked_mode = True
|
||||
elif mode_parameter != 'none':
|
||||
self._logger.debug('Invalid mode parameter: %r', mode_parameter)
|
||||
return
|
||||
|
||||
# Write a header
|
||||
response_header = (
|
||||
'HTTP/1.1 200 OK\r\n'
|
||||
'Content-Type: application/octet-stream\r\n')
|
||||
if chunked_mode:
|
||||
response_header += 'Transfer-Encoding: chunked\r\n\r\n'
|
||||
else:
|
||||
response_header += (
|
||||
'Content-Length: %d\r\n\r\n' % bytes_to_send)
|
||||
self.wfile.write(response_header)
|
||||
self.wfile.flush()
|
||||
|
||||
# Write a body
|
||||
SEND_BLOCK_SIZE = 1024 * 1024
|
||||
|
||||
while bytes_to_send > 0:
|
||||
bytes_to_send_in_this_loop = bytes_to_send
|
||||
if bytes_to_send_in_this_loop > SEND_BLOCK_SIZE:
|
||||
bytes_to_send_in_this_loop = SEND_BLOCK_SIZE
|
||||
|
||||
if chunked_mode:
|
||||
self.wfile.write('%x\r\n' % bytes_to_send_in_this_loop)
|
||||
self.wfile.write('a' * bytes_to_send_in_this_loop)
|
||||
if chunked_mode:
|
||||
self.wfile.write('\r\n')
|
||||
self.wfile.flush()
|
||||
|
||||
bytes_to_send -= bytes_to_send_in_this_loop
|
||||
|
||||
if chunked_mode:
|
||||
self.wfile.write('0\r\n\r\n')
|
||||
self.wfile.flush()
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011, Google Inc.
|
||||
# Copyright 2012, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
@ -32,30 +32,92 @@
|
||||
|
||||
"""Standalone WebSocket server.
|
||||
|
||||
Use this file to launch pywebsocket without Apache HTTP Server.
|
||||
|
||||
|
||||
BASIC USAGE
|
||||
===========
|
||||
|
||||
Use this server to run mod_pywebsocket without Apache HTTP Server.
|
||||
Go to the src directory and run
|
||||
|
||||
Usage:
|
||||
python standalone.py [-p <ws_port>] [-w <websock_handlers>]
|
||||
[-s <scan_dir>]
|
||||
[-d <document_root>]
|
||||
[-m <websock_handlers_map_file>]
|
||||
... for other options, see _main below ...
|
||||
$ python mod_pywebsocket/standalone.py [-p <ws_port>]
|
||||
[-w <websock_handlers>]
|
||||
[-d <document_root>]
|
||||
|
||||
<ws_port> is the port number to use for ws:// connection.
|
||||
|
||||
<document_root> is the path to the root directory of HTML files.
|
||||
|
||||
<websock_handlers> is the path to the root directory of WebSocket handlers.
|
||||
See __init__.py for details of <websock_handlers> and how to write WebSocket
|
||||
handlers. If this path is relative, <document_root> is used as the base.
|
||||
If not specified, <document_root> will be used. See __init__.py (or
|
||||
run $ pydoc mod_pywebsocket) for how to write WebSocket handlers.
|
||||
|
||||
<scan_dir> is a path under the root directory. If specified, only the
|
||||
handlers under scan_dir are scanned. This is useful in saving scan time.
|
||||
For more detail and other options, run
|
||||
|
||||
$ python mod_pywebsocket/standalone.py --help
|
||||
|
||||
or see _build_option_parser method below.
|
||||
|
||||
For trouble shooting, adding "--log_level debug" might help you.
|
||||
|
||||
|
||||
TRY DEMO
|
||||
========
|
||||
|
||||
Go to the src directory and run standalone.py with -d option to set the
|
||||
document root to the directory containing example HTMLs and handlers like this:
|
||||
|
||||
$ cd src
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example
|
||||
|
||||
to launch pywebsocket with the sample handler and html on port 80. Open
|
||||
http://localhost/console.html, click the connect button, type something into
|
||||
the text box next to the send button and click the send button. If everything
|
||||
is working, you'll see the message you typed echoed by the server.
|
||||
|
||||
|
||||
USING TLS
|
||||
=========
|
||||
|
||||
To run the standalone server with TLS support, run it with -t, -k, and -c
|
||||
options. When TLS is enabled, the standalone server accepts only TLS connection.
|
||||
|
||||
Note that when ssl module is used and the key/cert location is incorrect,
|
||||
TLS connection silently fails while pyOpenSSL fails on startup.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py \
|
||||
-d example \
|
||||
-p 10443 \
|
||||
-t \
|
||||
-c ../test/cert/cert.pem \
|
||||
-k ../test/cert/key.pem \
|
||||
|
||||
Note that when passing a relative path to -c and -k option, it will be resolved
|
||||
using the document root directory as the base.
|
||||
|
||||
|
||||
USING CLIENT AUTHENTICATION
|
||||
===========================
|
||||
|
||||
To run the standalone server with TLS client authentication support, run it with
|
||||
--tls-client-auth and --tls-client-ca options in addition to ones required for
|
||||
TLS support.
|
||||
|
||||
Example:
|
||||
|
||||
$ PYTHONPATH=. python mod_pywebsocket/standalone.py -d example -p 10443 -t \
|
||||
-c ../test/cert/cert.pem -k ../test/cert/key.pem \
|
||||
--tls-client-auth \
|
||||
--tls-client-ca=../test/cert/cacert.pem
|
||||
|
||||
Note that when passing a relative path to --tls-client-ca option, it will be
|
||||
resolved using the document root directory as the base.
|
||||
|
||||
|
||||
CONFIGURATION FILE
|
||||
==================
|
||||
|
||||
You can also write a configuration file and use it by specifying the path to
|
||||
the configuration file by --config option. Please write a configuration file
|
||||
@ -79,12 +141,14 @@ configuration file.
|
||||
|
||||
|
||||
THREADING
|
||||
=========
|
||||
|
||||
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
||||
used for each request.
|
||||
|
||||
|
||||
SECURITY WARNING
|
||||
================
|
||||
|
||||
This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
||||
It may execute arbitrary Python code or external programs. It should not be
|
||||
@ -96,6 +160,7 @@ import CGIHTTPServer
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import ConfigParser
|
||||
import base64
|
||||
import httplib
|
||||
import logging
|
||||
import logging.handlers
|
||||
@ -108,24 +173,13 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
_HAS_SSL = False
|
||||
_HAS_OPEN_SSL = False
|
||||
try:
|
||||
import ssl
|
||||
_HAS_SSL = True
|
||||
except ImportError:
|
||||
try:
|
||||
import OpenSSL.SSL
|
||||
_HAS_OPEN_SSL = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from mod_pywebsocket import common
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import http_header_util
|
||||
from mod_pywebsocket import memorizingfile
|
||||
from mod_pywebsocket import util
|
||||
from mod_pywebsocket.xhr_benchmark_handler import XHRBenchmarkHandler
|
||||
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
@ -136,6 +190,10 @@ _DEFAULT_REQUEST_QUEUE_SIZE = 128
|
||||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
# Constants for the --tls_module flag.
|
||||
_TLS_BY_STANDARD_MODULE = 'ssl'
|
||||
_TLS_BY_PYOPENSSL = 'pyopenssl'
|
||||
|
||||
|
||||
class _StandaloneConnection(object):
|
||||
"""Mimic mod_python mp_conn."""
|
||||
@ -199,55 +257,128 @@ class _StandaloneRequest(object):
|
||||
self.headers_in = request_handler.headers
|
||||
|
||||
def get_uri(self):
|
||||
"""Getter to mimic request.uri."""
|
||||
"""Getter to mimic request.uri.
|
||||
|
||||
This method returns the raw data at the Request-URI part of the
|
||||
Request-Line, while the uri method on the request object of mod_python
|
||||
returns the path portion after parsing the raw data. This behavior is
|
||||
kept for compatibility.
|
||||
"""
|
||||
|
||||
return self._request_handler.path
|
||||
uri = property(get_uri)
|
||||
|
||||
def get_unparsed_uri(self):
|
||||
"""Getter to mimic request.unparsed_uri."""
|
||||
|
||||
return self._request_handler.path
|
||||
unparsed_uri = property(get_unparsed_uri)
|
||||
|
||||
def get_method(self):
|
||||
"""Getter to mimic request.method."""
|
||||
|
||||
return self._request_handler.command
|
||||
method = property(get_method)
|
||||
|
||||
def get_protocol(self):
|
||||
"""Getter to mimic request.protocol."""
|
||||
|
||||
return self._request_handler.request_version
|
||||
protocol = property(get_protocol)
|
||||
|
||||
def is_https(self):
|
||||
"""Mimic request.is_https()."""
|
||||
|
||||
return self._use_tls
|
||||
|
||||
def _drain_received_data(self):
|
||||
"""Don't use this method from WebSocket handler. Drains unread data
|
||||
in the receive buffer.
|
||||
"""
|
||||
|
||||
raw_socket = self._request_handler.connection
|
||||
drained_data = util.drain_received_data(raw_socket)
|
||||
def _import_ssl():
|
||||
global ssl
|
||||
try:
|
||||
import ssl
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
if drained_data:
|
||||
self._logger.debug(
|
||||
'Drained data following close frame: %r', drained_data)
|
||||
|
||||
def _import_pyopenssl():
|
||||
global OpenSSL
|
||||
try:
|
||||
import OpenSSL.SSL
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
class _StandaloneSSLConnection(object):
|
||||
"""A wrapper class for OpenSSL.SSL.Connection to provide makefile method
|
||||
which is not supported by the class.
|
||||
"""A wrapper class for OpenSSL.SSL.Connection to
|
||||
- provide makefile method which is not supported by the class
|
||||
- tweak shutdown method since OpenSSL.SSL.Connection.shutdown doesn't
|
||||
accept the "how" argument.
|
||||
- convert SysCallError exceptions that its recv method may raise into a
|
||||
return value of '', meaning EOF. We cannot overwrite the recv method on
|
||||
self._connection since it's immutable.
|
||||
"""
|
||||
|
||||
_OVERRIDDEN_ATTRIBUTES = ['_connection', 'makefile', 'shutdown', 'recv']
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name in ('_connection', 'makefile'):
|
||||
if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
|
||||
return object.__getattribute__(self, name)
|
||||
return self._connection.__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ('_connection', 'makefile'):
|
||||
if name in _StandaloneSSLConnection._OVERRIDDEN_ATTRIBUTES:
|
||||
return object.__setattr__(self, name, value)
|
||||
return self._connection.__setattr__(name, value)
|
||||
|
||||
def makefile(self, mode='r', bufsize=-1):
|
||||
return socket._fileobject(self._connection, mode, bufsize)
|
||||
return socket._fileobject(self, mode, bufsize)
|
||||
|
||||
def shutdown(self, unused_how):
|
||||
self._connection.shutdown()
|
||||
|
||||
def recv(self, bufsize, flags=0):
|
||||
if flags != 0:
|
||||
raise ValueError('Non-zero flags not allowed')
|
||||
|
||||
try:
|
||||
return self._connection.recv(bufsize)
|
||||
except OpenSSL.SSL.SysCallError, (err, message):
|
||||
if err == -1:
|
||||
# Suppress "unexpected EOF" exception. See the OpenSSL document
|
||||
# for SSL_get_error.
|
||||
return ''
|
||||
raise
|
||||
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
Args:
|
||||
dispatcher: dispatch.Dispatcher instance
|
||||
websock_handler_map_file: alias map file
|
||||
"""
|
||||
|
||||
fp = open(websock_handlers_map_file)
|
||||
try:
|
||||
for line in fp:
|
||||
if line[0] == '#' or line.isspace():
|
||||
continue
|
||||
m = re.match('(\S+)\s+(\S+)', line)
|
||||
if not m:
|
||||
logging.warning('Wrong format in map file:' + line)
|
||||
continue
|
||||
try:
|
||||
dispatcher.add_resource_path_alias(
|
||||
m.group(1), m.group(2))
|
||||
except dispatch.DispatchException, e:
|
||||
logging.error(str(e))
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
@ -264,6 +395,20 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
if necessary.
|
||||
"""
|
||||
|
||||
# Share a Dispatcher among request handlers to save time for
|
||||
# instantiation. Dispatcher can be shared because it is thread-safe.
|
||||
options.dispatcher = dispatch.Dispatcher(
|
||||
options.websock_handlers,
|
||||
options.scan_dir,
|
||||
options.allow_handlers_outside_root_dir)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
options.websock_handlers_map_file)
|
||||
warnings = options.dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('Warning in source loading: %s' % warning)
|
||||
|
||||
self._logger = util.get_class_logger(self)
|
||||
|
||||
self.request_queue_size = options.request_queue_size
|
||||
@ -309,19 +454,25 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
except Exception, e:
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
continue
|
||||
if self.websocket_server_options.use_tls:
|
||||
if _HAS_SSL:
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
# For the case of _HAS_OPEN_SSL, we do wrapper setup after
|
||||
# accept.
|
||||
if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
|
||||
if server_options.tls_client_auth:
|
||||
if server_options.tls_client_cert_optional:
|
||||
client_cert_ = ssl.CERT_OPTIONAL
|
||||
else:
|
||||
client_cert_ = ssl.CERT_REQUIRED
|
||||
else:
|
||||
client_cert_ = ssl.CERT_NONE
|
||||
socket_ = ssl.wrap_socket(socket_,
|
||||
keyfile=self.websocket_server_options.private_key,
|
||||
certfile=self.websocket_server_options.certificate,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||
if _HAS_OPEN_SSL:
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||
ctx.use_privatekey_file(
|
||||
self.websocket_server_options.private_key)
|
||||
ctx.use_certificate_file(
|
||||
self.websocket_server_options.certificate)
|
||||
socket_ = OpenSSL.SSL.Connection(ctx, socket_)
|
||||
keyfile=server_options.private_key,
|
||||
certfile=server_options.certificate,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23,
|
||||
ca_certs=server_options.tls_client_ca,
|
||||
cert_reqs=client_cert_,
|
||||
do_handshake_on_connect=False)
|
||||
self._sockets.append((socket_, addrinfo))
|
||||
|
||||
def server_bind(self):
|
||||
@ -342,6 +493,15 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
self._logger.info('Skip by failure: %r', e)
|
||||
socket_.close()
|
||||
failed_sockets.append(socketinfo)
|
||||
if self.server_address[1] == 0:
|
||||
# The operating system assigns the actual port number for port
|
||||
# number 0. This case, the second and later sockets should use
|
||||
# the same port number. Also self.server_port is rewritten
|
||||
# because it is exported, and will be used by external code.
|
||||
self.server_address = (
|
||||
self.server_name, socket_.getsockname()[1])
|
||||
self.server_port = self.server_address[1]
|
||||
self._logger.info('Port %r is assigned', self.server_port)
|
||||
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
@ -366,6 +526,10 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
for socketinfo in failed_sockets:
|
||||
self._sockets.remove(socketinfo)
|
||||
|
||||
if len(self._sockets) == 0:
|
||||
self._logger.critical(
|
||||
'No sockets activated. Use info log level to see the reason.')
|
||||
|
||||
def server_close(self):
|
||||
"""Override SocketServer.TCPServer.server_close to enable multiple
|
||||
sockets close.
|
||||
@ -382,7 +546,7 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
self._logger.critical('Not supported: fileno')
|
||||
return self._sockets[0][0].fileno()
|
||||
|
||||
def handle_error(self, rquest, client_address):
|
||||
def handle_error(self, request, client_address):
|
||||
"""Override SocketServer.handle_error."""
|
||||
|
||||
self._logger.error(
|
||||
@ -399,8 +563,64 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""
|
||||
|
||||
accepted_socket, client_address = self.socket.accept()
|
||||
if self.websocket_server_options.use_tls and _HAS_OPEN_SSL:
|
||||
accepted_socket = _StandaloneSSLConnection(accepted_socket)
|
||||
|
||||
server_options = self.websocket_server_options
|
||||
if server_options.use_tls:
|
||||
if server_options.tls_module == _TLS_BY_STANDARD_MODULE:
|
||||
try:
|
||||
accepted_socket.do_handshake()
|
||||
except ssl.SSLError, e:
|
||||
self._logger.debug('%r', e)
|
||||
raise
|
||||
|
||||
# Print cipher in use. Handshake is done on accept.
|
||||
self._logger.debug('Cipher: %s', accepted_socket.cipher())
|
||||
self._logger.debug('Client cert: %r',
|
||||
accepted_socket.getpeercert())
|
||||
elif server_options.tls_module == _TLS_BY_PYOPENSSL:
|
||||
# We cannot print the cipher in use. pyOpenSSL doesn't provide
|
||||
# any method to fetch that.
|
||||
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||
ctx.use_privatekey_file(server_options.private_key)
|
||||
ctx.use_certificate_file(server_options.certificate)
|
||||
|
||||
def default_callback(conn, cert, errnum, errdepth, ok):
|
||||
return ok == 1
|
||||
|
||||
# See the OpenSSL document for SSL_CTX_set_verify.
|
||||
if server_options.tls_client_auth:
|
||||
verify_mode = OpenSSL.SSL.VERIFY_PEER
|
||||
if not server_options.tls_client_cert_optional:
|
||||
verify_mode |= OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT
|
||||
ctx.set_verify(verify_mode, default_callback)
|
||||
ctx.load_verify_locations(server_options.tls_client_ca,
|
||||
None)
|
||||
else:
|
||||
ctx.set_verify(OpenSSL.SSL.VERIFY_NONE, default_callback)
|
||||
|
||||
accepted_socket = OpenSSL.SSL.Connection(ctx, accepted_socket)
|
||||
accepted_socket.set_accept_state()
|
||||
|
||||
# Convert SSL related error into socket.error so that
|
||||
# SocketServer ignores them and keeps running.
|
||||
#
|
||||
# TODO(tyoshino): Convert all kinds of errors.
|
||||
try:
|
||||
accepted_socket.do_handshake()
|
||||
except OpenSSL.SSL.Error, e:
|
||||
# Set errno part to 1 (SSL_ERROR_SSL) like the ssl module
|
||||
# does.
|
||||
self._logger.debug('%r', e)
|
||||
raise socket.error(1, '%r' % e)
|
||||
cert = accepted_socket.get_peer_certificate()
|
||||
if cert is not None:
|
||||
self._logger.debug('Client cert subject: %r',
|
||||
cert.get_subject().get_components())
|
||||
accepted_socket = _StandaloneSSLConnection(accepted_socket)
|
||||
else:
|
||||
raise ValueError('No TLS support module is available')
|
||||
|
||||
return accepted_socket, client_address
|
||||
|
||||
def serve_forever(self, poll_interval=0.5):
|
||||
@ -493,7 +713,32 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
# attributes).
|
||||
if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
|
||||
return False
|
||||
|
||||
if self._options.use_basic_auth:
|
||||
auth = self.headers.getheader('Authorization')
|
||||
if auth != self._options.basic_auth_credential:
|
||||
self.send_response(401)
|
||||
self.send_header('WWW-Authenticate',
|
||||
'Basic realm="Pywebsocket"')
|
||||
self.end_headers()
|
||||
self._logger.info('Request basic authentication')
|
||||
return False
|
||||
|
||||
host, port, resource = http_header_util.parse_uri(self.path)
|
||||
|
||||
# Special paths for XMLHttpRequest benchmark
|
||||
xhr_benchmark_helper_prefix = '/073be001e10950692ccbf3a2ad21c245'
|
||||
if resource == (xhr_benchmark_helper_prefix + '_send'):
|
||||
xhr_benchmark_handler = XHRBenchmarkHandler(
|
||||
self.headers, self.rfile, self.wfile)
|
||||
xhr_benchmark_handler.do_send()
|
||||
return False
|
||||
if resource == (xhr_benchmark_helper_prefix + '_receive'):
|
||||
xhr_benchmark_handler = XHRBenchmarkHandler(
|
||||
self.headers, self.rfile, self.wfile)
|
||||
xhr_benchmark_handler.do_receive()
|
||||
return False
|
||||
|
||||
if resource is None:
|
||||
self._logger.info('Invalid URI: %r', self.path)
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
@ -528,7 +773,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
self._logger.info('Fallback to CGIHTTPRequestHandler')
|
||||
return True
|
||||
except dispatch.DispatchException, e:
|
||||
self._logger.info('%s', e)
|
||||
self._logger.info('Dispatch failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
@ -544,7 +789,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
allowDraft75=self._options.allow_draft75,
|
||||
strict=self._options.strict)
|
||||
except handshake.VersionException, e:
|
||||
self._logger.info('%s', e)
|
||||
self._logger.info('Handshake failed for version error: %s', e)
|
||||
self.send_response(common.HTTP_STATUS_BAD_REQUEST)
|
||||
self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
|
||||
e.supported_versions)
|
||||
@ -552,14 +797,14 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
return False
|
||||
except handshake.HandshakeException, e:
|
||||
# Handshake for ws(s) failed.
|
||||
self._logger.info('%s', e)
|
||||
self._logger.info('Handshake failed for error: %s', e)
|
||||
self.send_error(e.status)
|
||||
return False
|
||||
|
||||
request._dispatcher = self._options.dispatcher
|
||||
self._options.dispatcher.transfer_data(request)
|
||||
except handshake.AbortedByUserException, e:
|
||||
self._logger.info('%s', e)
|
||||
self._logger.info('Aborted: %s', e)
|
||||
return False
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
@ -601,7 +846,13 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
return False
|
||||
|
||||
|
||||
def _get_logger_from_class(c):
|
||||
return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
|
||||
|
||||
|
||||
def _configure_logging(options):
|
||||
logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.getLevelName(options.log_level.upper()))
|
||||
if options.log_file:
|
||||
@ -614,31 +865,12 @@ def _configure_logging(options):
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
Args:
|
||||
dispatcher: dispatch.Dispatcher instance
|
||||
websock_handler_map_file: alias map file
|
||||
"""
|
||||
|
||||
fp = open(websock_handlers_map_file)
|
||||
try:
|
||||
for line in fp:
|
||||
if line[0] == '#' or line.isspace():
|
||||
continue
|
||||
m = re.match('(\S+)\s+(\S+)', line)
|
||||
if not m:
|
||||
logging.warning('Wrong format in map file:' + line)
|
||||
continue
|
||||
try:
|
||||
dispatcher.add_resource_path_alias(
|
||||
m.group(1), m.group(2))
|
||||
except dispatch.DispatchException, e:
|
||||
logging.error(str(e))
|
||||
finally:
|
||||
fp.close()
|
||||
deflate_log_level_name = logging.getLevelName(
|
||||
options.deflate_log_level.upper())
|
||||
_get_logger_from_class(util._Deflater).setLevel(
|
||||
deflate_log_level_name)
|
||||
_get_logger_from_class(util._Inflater).setLevel(
|
||||
deflate_log_level_name)
|
||||
|
||||
|
||||
def _build_option_parser():
|
||||
@ -667,7 +899,9 @@ def _build_option_parser():
|
||||
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help='WebSocket handlers root directory.')
|
||||
help=('The root directory of WebSocket handler files. '
|
||||
'If the path is relative, --document-root is used '
|
||||
'as the base.'))
|
||||
parser.add_option('-m', '--websock-handlers-map-file',
|
||||
'--websock_handlers_map_file',
|
||||
dest='websock_handlers_map_file',
|
||||
@ -677,15 +911,20 @@ def _build_option_parser():
|
||||
'existing_resource_path, separated by spaces.'))
|
||||
parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
|
||||
default=None,
|
||||
help=('WebSocket handlers scan directory. '
|
||||
'Must be a directory under websock_handlers.'))
|
||||
help=('Must be a directory under --websock-handlers. '
|
||||
'Only handlers under this directory are scanned '
|
||||
'and registered to the server. '
|
||||
'Useful for saving scan time when the handler '
|
||||
'root directory contains lots of files that are '
|
||||
'not handler file or are handler files but you '
|
||||
'don\'t want them to be registered. '))
|
||||
parser.add_option('--allow-handlers-outside-root-dir',
|
||||
'--allow_handlers_outside_root_dir',
|
||||
dest='allow_handlers_outside_root_dir',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=('Scans WebSocket handlers even if their canonical '
|
||||
'path is not under websock_handlers.'))
|
||||
'path is not under --websock-handlers.'))
|
||||
parser.add_option('-d', '--document-root', '--document_root',
|
||||
dest='document_root', default='.',
|
||||
help='Document root directory.')
|
||||
@ -697,18 +936,52 @@ def _build_option_parser():
|
||||
'as CGI programs. Must be executable.'))
|
||||
parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
|
||||
default=False, help='use TLS (wss://)')
|
||||
parser.add_option('--tls-module', '--tls_module', dest='tls_module',
|
||||
type='choice',
|
||||
choices = [_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL],
|
||||
help='Use ssl module if "%s" is specified. '
|
||||
'Use pyOpenSSL module if "%s" is specified' %
|
||||
(_TLS_BY_STANDARD_MODULE, _TLS_BY_PYOPENSSL))
|
||||
parser.add_option('-k', '--private-key', '--private_key',
|
||||
dest='private_key',
|
||||
default='', help='TLS private key file.')
|
||||
parser.add_option('-c', '--certificate', dest='certificate',
|
||||
default='', help='TLS certificate file.')
|
||||
parser.add_option('--tls-client-auth', dest='tls_client_auth',
|
||||
action='store_true', default=False,
|
||||
help='Requests TLS client auth on every connection.')
|
||||
parser.add_option('--tls-client-cert-optional',
|
||||
dest='tls_client_cert_optional',
|
||||
action='store_true', default=False,
|
||||
help=('Makes client certificate optional even though '
|
||||
'TLS client auth is enabled.'))
|
||||
parser.add_option('--tls-client-ca', dest='tls_client_ca', default='',
|
||||
help=('Specifies a pem file which contains a set of '
|
||||
'concatenated CA certificates which are used to '
|
||||
'validate certificates passed from clients'))
|
||||
parser.add_option('--basic-auth', dest='use_basic_auth',
|
||||
action='store_true', default=False,
|
||||
help='Requires Basic authentication.')
|
||||
parser.add_option('--basic-auth-credential',
|
||||
dest='basic_auth_credential', default='test:test',
|
||||
help='Specifies the credential of basic authentication '
|
||||
'by username:password pair (e.g. test:test).')
|
||||
parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
|
||||
default='', help='Log file.')
|
||||
# Custom log level:
|
||||
# - FINE: Prints status of each frame processing step
|
||||
parser.add_option('--log-level', '--log_level', type='choice',
|
||||
dest='log_level', default='warn',
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error',
|
||||
choices=['fine',
|
||||
'debug', 'info', 'warning', 'warn', 'error',
|
||||
'critical'],
|
||||
help='Log level.')
|
||||
parser.add_option('--deflate-log-level', '--deflate_log_level',
|
||||
type='choice',
|
||||
dest='deflate_log_level', default='warn',
|
||||
choices=['debug', 'info', 'warning', 'warn', 'error',
|
||||
'critical'],
|
||||
help='Log level for _Deflater and _Inflater.')
|
||||
parser.add_option('--thread-monitor-interval-in-sec',
|
||||
'--thread_monitor_interval_in_sec',
|
||||
dest='thread_monitor_interval_in_sec',
|
||||
@ -726,9 +999,9 @@ def _build_option_parser():
|
||||
help='Log backup count')
|
||||
parser.add_option('--allow-draft75', dest='allow_draft75',
|
||||
action='store_true', default=False,
|
||||
help='Allow draft 75 handshake')
|
||||
help='Obsolete option. Ignored.')
|
||||
parser.add_option('--strict', dest='strict', action='store_true',
|
||||
default=False, help='Strictly check handshake request')
|
||||
default=False, help='Obsolete option. Ignored.')
|
||||
parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
|
||||
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
||||
help='request queue size')
|
||||
@ -796,12 +1069,24 @@ def _parse_args_and_config(args):
|
||||
|
||||
|
||||
def _main(args=None):
|
||||
"""You can call this function from your own program, but please note that
|
||||
this function has some side-effects that might affect your program. For
|
||||
example, util.wrap_popen3_for_win use in this method replaces implementation
|
||||
of os.popen3.
|
||||
"""
|
||||
|
||||
options, args = _parse_args_and_config(args=args)
|
||||
|
||||
os.chdir(options.document_root)
|
||||
|
||||
_configure_logging(options)
|
||||
|
||||
if options.allow_draft75:
|
||||
logging.warning('--allow_draft75 option is obsolete.')
|
||||
|
||||
if options.strict:
|
||||
logging.warning('--strict option is obsolete.')
|
||||
|
||||
# TODO(tyoshino): Clean up initialization of CGI related values. Move some
|
||||
# of code here to WebSocketRequestHandler class if it's better.
|
||||
options.cgi_directories = []
|
||||
@ -824,37 +1109,67 @@ def _main(args=None):
|
||||
options.is_executable_method = __check_script
|
||||
|
||||
if options.use_tls:
|
||||
if not (_HAS_SSL or _HAS_OPEN_SSL):
|
||||
logging.critical('TLS support requires ssl or pyOpenSSL.')
|
||||
if options.tls_module is None:
|
||||
if _import_ssl():
|
||||
options.tls_module = _TLS_BY_STANDARD_MODULE
|
||||
logging.debug('Using ssl module')
|
||||
elif _import_pyopenssl():
|
||||
options.tls_module = _TLS_BY_PYOPENSSL
|
||||
logging.debug('Using pyOpenSSL module')
|
||||
else:
|
||||
logging.critical(
|
||||
'TLS support requires ssl or pyOpenSSL module.')
|
||||
sys.exit(1)
|
||||
elif options.tls_module == _TLS_BY_STANDARD_MODULE:
|
||||
if not _import_ssl():
|
||||
logging.critical('ssl module is not available')
|
||||
sys.exit(1)
|
||||
elif options.tls_module == _TLS_BY_PYOPENSSL:
|
||||
if not _import_pyopenssl():
|
||||
logging.critical('pyOpenSSL module is not available')
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.critical('Invalid --tls-module option: %r',
|
||||
options.tls_module)
|
||||
sys.exit(1)
|
||||
|
||||
if not options.private_key or not options.certificate:
|
||||
logging.critical(
|
||||
'To use TLS, specify private_key and certificate.')
|
||||
sys.exit(1)
|
||||
|
||||
if (options.tls_client_cert_optional and
|
||||
not options.tls_client_auth):
|
||||
logging.critical('Client authentication must be enabled to '
|
||||
'specify tls_client_cert_optional')
|
||||
sys.exit(1)
|
||||
else:
|
||||
if options.tls_module is not None:
|
||||
logging.critical('Use --tls-module option only together with '
|
||||
'--use-tls option.')
|
||||
sys.exit(1)
|
||||
|
||||
if options.tls_client_auth:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if options.tls_client_cert_optional:
|
||||
logging.critical('TLS must be enabled for client authentication.')
|
||||
sys.exit(1)
|
||||
|
||||
if not options.scan_dir:
|
||||
options.scan_dir = options.websock_handlers
|
||||
|
||||
if options.use_basic_auth:
|
||||
options.basic_auth_credential = 'Basic ' + base64.b64encode(
|
||||
options.basic_auth_credential)
|
||||
|
||||
try:
|
||||
if options.thread_monitor_interval_in_sec > 0:
|
||||
# Run a thread monitor to show the status of server threads for
|
||||
# debugging.
|
||||
ThreadMonitor(options.thread_monitor_interval_in_sec).start()
|
||||
|
||||
# Share a Dispatcher among request handlers to save time for
|
||||
# instantiation. Dispatcher can be shared because it is thread-safe.
|
||||
options.dispatcher = dispatch.Dispatcher(
|
||||
options.websock_handlers,
|
||||
options.scan_dir,
|
||||
options.allow_handlers_outside_root_dir)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
options.websock_handlers_map_file)
|
||||
warnings = options.dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('mod_pywebsocket: %s' % warning)
|
||||
|
||||
server = WebSocketServer(options)
|
||||
server.serve_forever()
|
||||
except Exception, e:
|
||||
|
Loading…
Reference in New Issue
Block a user