Bug 792831 - WebSocket permessage compression extension, r=jduell

This commit is contained in:
Michal Novotny 2014-12-29 12:43:40 +01:00
parent 4c280fb16f
commit 2296e00d57
32 changed files with 4631 additions and 1381 deletions

View File

@ -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)

View File

@ -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)

View 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)

View File

@ -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]

View 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>

View File

@ -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

View File

@ -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));

View File

@ -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;

View File

@ -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',
]

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.
"""

View File

@ -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

View File

@ -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
"""

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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

View 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;
}
}
%}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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):

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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: