📝 Added article explaining websockets protocol
This commit is contained in:
parent
5c1415752e
commit
9182063b39
1 changed files with 637 additions and 0 deletions
637
websockets_explanation.html
Normal file
637
websockets_explanation.html
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8>
|
||||
|
||||
|
||||
<title>Websockets 101 | Armin Ronacher's Thoughts and Writings</title>
|
||||
<link rel="stylesheet" href="/static/new-style.css" type="text/css">
|
||||
<link href="/feed.atom" rel="alternate" title="Armin Ronacher's Thoughts and Writings" type="application/atom+xml">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/_pygments.css" type="text/css">
|
||||
<meta property="title" content="Websockets 101">
|
||||
<meta property="og:title" content="Websockets 101">
|
||||
<meta name="twitter:site" content="@mitsuhiko">
|
||||
<meta name="twitter:title" content="Websockets 101">
|
||||
<link rel="me" href="https://hachyderm.io/@mitsuhiko">
|
||||
|
||||
<meta name="description" content="Quick introduction into the final specification of websockets (RFC 6455).">
|
||||
<meta name="og:description" content="Quick introduction into the final specification of websockets (RFC 6455).">
|
||||
<meta name="twitter:description" content="Quick introduction into the final specification of websockets (RFC 6455).">
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class=container>
|
||||
<div class=header>
|
||||
<a href="/about/">Armin Ronacher</a>'s Thoughts and Writings
|
||||
</div>
|
||||
<div class=navigation>
|
||||
<ul>
|
||||
<li><a href="/">blog</a>
|
||||
<li><a href="/archive/">archive</a>
|
||||
<li><a href="/tags/">tags</a>
|
||||
<li><a href="/projects/">projects</a>
|
||||
<li><a href="/talks/">talks</a>
|
||||
<li><a href="/about/">about</a>
|
||||
</ul>
|
||||
</div>
|
||||
<div class=body>
|
||||
|
||||
<h1 class="title">Websockets 101</h1>
|
||||
|
||||
|
||||
<p class=date>written on Monday, September 24, 2012
|
||||
|
||||
|
||||
<p>Out of curiosity I taught the <a class="reference external" href="http://fireteam.net/">Fireteam</a> presence
|
||||
server websockets as a protocol in addition to the proprietary protocol it
|
||||
speaks out of the box. I don't really have a usecase for websocket per-se
|
||||
with the server, but I thought it might make it more useful for
|
||||
customers that want to use the event based parts of the API with HTML5
|
||||
games. This post focuses on implementing websockets on the server, not so
|
||||
much about how you would use them from the client and basically collects
|
||||
all thing I wish I would have known before.</p>
|
||||
<div class="section" id="what-are-websockets">
|
||||
<h2>What are Websockets</h2>
|
||||
<p>So let's start with that part first. What are websockets and why would
|
||||
you use websockets? Basically only if you want to have a bidirectional
|
||||
communication between an actual web browser and some server. Websocket is
|
||||
not necessarily a good protocol if neither of the endpoints is an actual
|
||||
browser. Websockets suffer a lot under the restrictions and
|
||||
implementation details that were forced upon the protocol to make it work
|
||||
with existing HTTP infrastructure.</p>
|
||||
<p>Websockets in the current iteration as specified by <a class="reference external" href="http://tools.ietf.org/html/rfc6455.html">RFC 6455</a> do a bunch
|
||||
of things differently to what a raw TCP connections does. The name
|
||||
websocket gives the impression that it's a traditional socket. In
|
||||
practice it combines the parts of UDP and TCP: it's message based like
|
||||
UDP, but it's reliable like TCP. So assuming you know what TCP is, here
|
||||
is what websocket adds on top of that:</p>
|
||||
<ul class="simple">
|
||||
<li>it upgrades from HTTP 1.1 through a system previously defined as part
|
||||
of the HTTP specification (upgrade header).</li>
|
||||
<li>it sends the origin domain in the upgrade so that connections can be
|
||||
restricted to prevent CSRF attacks. This works similar to CORS for
|
||||
XMLHttpRequest.</li>
|
||||
<li>Through the HTTP handshake it becomes feasible to proxy the connection
|
||||
without losing information (a proxy could inject an <tt class="docutils literal"><span class="pre">X-Real-IP</span></tt> or
|
||||
<tt class="docutils literal"><span class="pre">X-Forwarded-For</span></tt> header).</li>
|
||||
<li>It sends messages. Each message can also carry control information
|
||||
and is wrapped in a frame.</li>
|
||||
<li>Messages sent from the client to the server are obfuscated with a
|
||||
basic transmission mask of 4 bytes which is sent within each websocket
|
||||
package. Messages can be of utf-8 text type or binary nature.</li>
|
||||
<li>It adds an agreed way for the two peers to signal that the connection
|
||||
is about to be terminated that does not rely on the underlying TCP
|
||||
connection.</li>
|
||||
<li>Builtin heartbeating support. That's actually pretty nifty because
|
||||
the application code in the browser does not have to deal with
|
||||
keeping the connection alive and can just assume that either the
|
||||
server or the browser will make sure that they exchange ping/pongs.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="when-to-use-them">
|
||||
<h2>When to use them?</h2>
|
||||
<p>Websockets make you sad. There, I said it. What started out as a really
|
||||
small simple thing ended up as an abomination of (what feels like) needles
|
||||
complexity. Now the complexity comes for a reason. The protocol went
|
||||
through many iterations and basically had to be changed multiple times
|
||||
because of unforeseen security problems that came up with misbehaving
|
||||
proxies. The protocol I created for the internal communication of our
|
||||
server is upgrading from HTTP just like websockets do, but without the
|
||||
“secure” parts. And here is why it does not matter:</p>
|
||||
<p>Everybody knows about HTTP proxies. We have proxies that do load
|
||||
balancing on the application side, we have proxies that do SSL offloading,
|
||||
we have proxies for all kinds of things. Unfortunately outside our
|
||||
internal infrastructure everyone of us also has to deal with HTTP proxies
|
||||
in corporate networks, and worse, on mobile network connections. The
|
||||
amount of shitty HTTP middleware installed all around the world is just
|
||||
staggering. And this pretty much has shown me that the only way you can
|
||||
do anything useful these days is by putting TLS on top of everything and
|
||||
just force people to stop with their shenanigans. On O2's mobile networks
|
||||
you cannot use websockets unless they are encrypted. You cannot get
|
||||
websocket connections through Amazon's ELB (Their load HTTP/TCP balancer).
|
||||
Heck, you can't even get PATCH as an HTTP method through the ELB.</p>
|
||||
<p>Everything that Fireteam will be doing will most likely always be behind
|
||||
an encrypted connection. It guarantees me that nothing can do funny
|
||||
things with the data I'm sending around. And as a positive side effect I
|
||||
don't have to mask my communication like websocket does because I know the
|
||||
software stack on my side until it hits the internet. The communication
|
||||
gets encrypted and I know nobody is going to change my data on the way to
|
||||
the client.</p>
|
||||
<p>In fact, I would also recommend to always use websockets through TLS.
|
||||
Even if you don't care about the security side of things you will still
|
||||
benefit from the fact that your websocket connections succeed in many more
|
||||
cases. Not encrypting your connection is definitely something you will
|
||||
regret sooner or later.</p>
|
||||
</div>
|
||||
<div class="section" id="the-handshake">
|
||||
<h2>The Handshake</h2>
|
||||
<p>Alright. After this fairly long disclaimer you're still there, which
|
||||
probably means you still want to do websockets. Fair enough. Now let's
|
||||
start with the basics, the handshake. This is where everything starts.
|
||||
It upgrades your connection from HTTP to something else. For the internal
|
||||
protocol we recommend to customers we upgrade our HTTP connection
|
||||
basically to a raw TCP connection. Websockets are not an upgrade to TCP,
|
||||
it's an upgrade to a message based communication.</p>
|
||||
<p>To begin: why would you upgrade from HTTP instead of directly starting
|
||||
with TCP as a protocol? The reasons for why Fireteam's protocol starts
|
||||
with HTTP not all that different from why websockets upgrade from HTTP.</p>
|
||||
<p>Websockets upgrade from HTTP because it was believed that people would
|
||||
develop servers that serve both websocket connections as well as HTTP
|
||||
ones. I don't believe for a second that this will be what people do at
|
||||
the end of the day however. A server that handles stateless requests to
|
||||
answer with a reply has a very different behavior than a server that keeps
|
||||
TCP connections open. However the advantage is that websockets use the
|
||||
same ports as HTTP and HTTPS do and that is a huge win. It's a win
|
||||
because these are privileged ports (< 1024) and they are traditionally
|
||||
handled differently than non privileged ports. For instance on a linux
|
||||
system only root can open such ports. Even more important: ELB only lets
|
||||
you open a handful of these privileged ports (25, 80 and 443 to be exact).
|
||||
Since ELB also does socket level load balancing you can still do
|
||||
websockets on Amazon, just not through their HTTP local balancer.</p>
|
||||
<p>We're handling our persistent presence protocol very differently than our
|
||||
HTTP webservice but we still benefit from the HTTP upgrade in some edge
|
||||
cases. That's mainly where we have to tunnel our communication through a
|
||||
HTTP library because arbitrary socket connections are not possible for
|
||||
security or scalability reasons. If you have ever used Google Appengine
|
||||
or early Windows Phone you will have noticed that HTTP connections are
|
||||
possible where regular socket connections are not.</p>
|
||||
<p>You will also see that many corporate networks only allow certain ports
|
||||
outgoing. The fact that websockets use the same port as HTTP/HTTPS make
|
||||
this much more interesting. If anyone has ever used the flash socket
|
||||
policy system will know that pain. Currently it's entirely impossible to
|
||||
use flash sockets behind Amazon's ELB because the Flash VM will attempt to
|
||||
connect to port 843 to get authorization information. That's a port you
|
||||
can't open on the ELB. So the idea of starting with HTTP is pretty solid.</p>
|
||||
<p>HTTP always supported upgrades, but unfortunately many proxies seem to
|
||||
have ignored that part of the specification. The main reason for that
|
||||
probably is that until websockets came around nobody was actually using
|
||||
the <cite>Upgrade</cite> flag. There was an SSL upgrade RFC that used the same
|
||||
mechanism but I don't think anyone is using that.</p>
|
||||
<p>Alright. So what does the handshake look like? The upgrade is initiated
|
||||
by the client, not by the server. The way Fireteam upgrades the
|
||||
connection is by following the old SSL RFC and looks like this:</p>
|
||||
<pre class="literal-block">
|
||||
OPTIONS / HTTP/1.1
|
||||
Host: example.com
|
||||
Upgrade: firepresence
|
||||
X-Auth-Token: auth-info-here
|
||||
</pre>
|
||||
<p>The server then replies by upgrading:</p>
|
||||
<pre class="literal-block">
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
Upgrade: firepresence/1.0
|
||||
Connection: Upgrade
|
||||
</pre>
|
||||
<p>If the upgrade header was missing, the server instead answers with 426
|
||||
Upgrade Required:</p>
|
||||
<pre class="literal-block">
|
||||
HTTP/1.1 426 Upgrade Required
|
||||
</pre>
|
||||
<p>What's interesting about this is that the upgrade require status code is
|
||||
defined, but it does not show up in the HTTP/1.1 RFC. Instead if does
|
||||
come from that SSL RFC.</p>
|
||||
<p>Websockets upgrade very similar, but they are using <tt class="docutils literal">400 Bad Request</tt> to
|
||||
signal a missing upgrade. They also transmit a special key with the
|
||||
upgrade request which the server has to process and send back. This is
|
||||
done so that a websocket connection cannot be established with an endpoint
|
||||
that is not aware of websockets. Here is what the handshake looks like
|
||||
for the client:</p>
|
||||
<pre class="literal-block">
|
||||
GET / HTTP/1.1
|
||||
Host: example.com
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
||||
Origin: http://example.com
|
||||
</pre>
|
||||
<p>The websocket key here are random bytes. The server takes these bytes and
|
||||
appends the special string <tt class="docutils literal"><span class="pre">258EAFA5-E914-47DA-95CA-C5AB0DC85B11</span></tt> to it,
|
||||
then creates the SHA1 hash from it and base64 encodes the result (the
|
||||
bytes, not the hexadecimal representation). The magic string looks like a
|
||||
UUID and also is one, but that's completely irrelevant because the exact
|
||||
string needs to be used. A lowercase representation or braces around the
|
||||
string would obviously fail. That value is then put into the
|
||||
<tt class="docutils literal"><span class="pre">Sec-WebSocket-Accept</span></tt> header. When the server has computed the value it
|
||||
can send an upgrade response back:</p>
|
||||
<pre class="literal-block">
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
||||
</pre>
|
||||
<p>The handshake can also include a protocol request and the websocket
|
||||
version information but you can't include arbitrary other headers. If you
|
||||
compare the websocket upgrade with our own upgrade you will notice that we
|
||||
can't transmit the authorization information. There are two ways around
|
||||
that. You can either transmit the authorization information as the first
|
||||
request or put it into the URL as query parameter.</p>
|
||||
<p>Also notice that the <tt class="docutils literal"><span class="pre">Sec-WebSocket-Accept</span></tt> header brings it's own
|
||||
grammar for the value. Normally you would expect you can quote the value
|
||||
but the specification specifically requires a base64 value there:</p>
|
||||
<pre class="literal-block">
|
||||
Sec-WebSocket-Accept = base64-value-non-empty
|
||||
base64-value-non-empty = (1*base64-data [ base64-padding ]) |
|
||||
base64-padding
|
||||
base64-data = 4base64-character
|
||||
base64-padding = (2base64-character "==") |
|
||||
(3base64-character "=")
|
||||
base64-character = ALPHA | DIGIT | "+" | "/"
|
||||
</pre>
|
||||
</div>
|
||||
<div class="section" id="websocket-urls">
|
||||
<h2>Websocket URLs</h2>
|
||||
<p>Alright. As if websockets were not painful enough as they are, someone
|
||||
had the amazing idea to also introduce a new URL scheme. Two in fact.
|
||||
<tt class="docutils literal"><span class="pre">ws://</span></tt> and <tt class="docutils literal"><span class="pre">wss://</span></tt>. Sounds like a tiny change from <tt class="docutils literal">http</tt> to
|
||||
<tt class="docutils literal">https</tt> but unfortunately that's not the case. URLs have scheme
|
||||
specific grammar. For instance FTP URLs can have authorization
|
||||
information in the netloc part (<tt class="docutils literal"><span class="pre">ftp://username@server/</span></tt>) whereas HTTP
|
||||
can't. <tt class="docutils literal">mailto</tt> URLs don't have the leading slashes etc. Websocket
|
||||
URLs are special in that they do not support anchors (<tt class="docutils literal">#foo</tt>). Now why
|
||||
would that matter? It matters because whoever created the URL parsing
|
||||
module in Python also decided that they should stick as closely as
|
||||
possible to the strict standard that you cannot throw arbitrary URLs at
|
||||
the module. For instance if you would try to parse websocket URLs you
|
||||
quickly realize that the results are just wrong:</p>
|
||||
<div class="highlight"><pre><span></span><span class="gp">>>> </span><span class="kn">import</span> <span class="nn">urlparse</span>
|
||||
<span class="gp">>>> </span><span class="n">urlparse</span><span class="o">.</span><span class="n">urlparse</span><span class="p">(</span><span class="s1">'wss://foo/?bar=baz'</span><span class="p">)</span>
|
||||
<span class="go">ParseResult(scheme='wss', netloc='foo', path='/?bar=baz',</span>
|
||||
<span class="go"> params='', query='', fragment='')</span>
|
||||
</pre></div>
|
||||
<p>The reason why websockets have a separate URL is beyond me. I suspect it
|
||||
stems from the fact that the RFC hints towards eventually dropping the
|
||||
upgrade from HTTP so the HTTP URL would not make much sense. In any case
|
||||
it's just a very annoying example of where we now have to things now that
|
||||
were previously unnecessary.</p>
|
||||
<p>Also since it's a different protocol, protocol relative links will
|
||||
obviously not work. You will have to switch between <tt class="docutils literal">wss</tt> and <tt class="docutils literal">ws</tt> by
|
||||
hand.</p>
|
||||
<p>Otherwise the same rules as for HTTP style URLs apply. Namely that <tt class="docutils literal">ws</tt>
|
||||
is unencrypted and has port 80 as default port and <tt class="docutils literal">wss</tt> requires TLS
|
||||
encryption and port 443 as default.</p>
|
||||
</div>
|
||||
<div class="section" id="authorization-and-ips">
|
||||
<h2>Authorization and IPs</h2>
|
||||
<p>Before we dive further into websockets let me throw yet another problem at
|
||||
you. Since proxying is a word that comes up with websockets so very often
|
||||
we should probably talk about how proxies work there. HTTP in the
|
||||
original RFC does not really talk much about TCP and IP addresses don't
|
||||
show up in there either. However it has become clear over the last couple
|
||||
of years that there is a lot of value behind knowing which IP address
|
||||
connects to your server. Traditionally this is doing by looking at the IP
|
||||
address that is stored in the individual IP packets. Unfortunately this
|
||||
falls short the moment you start proxying HTTP through another server.</p>
|
||||
<p>There are two headers that have become popular to remedy that situation.
|
||||
The first one is <tt class="docutils literal"><span class="pre">X-Forwarded-For</span></tt> which can contain one or multiple IPs
|
||||
of servers that took part in the request. Each proxy server would add the
|
||||
remote address of the request to the header. So if you have a user agent
|
||||
at 192.168.0.100 connect to 192.168.0.200 which acts as a proxy for
|
||||
192.168.0.201 you end up with an <tt class="docutils literal"><span class="pre">X-Forwarded-For</span></tt> of <tt class="docutils literal">192.168.0.100</tt>.
|
||||
Bring in yet another proxy server that one would append it's own IP
|
||||
address to that list.</p>
|
||||
<p>Now there has to be a limit to this. Usually what you do is you configure
|
||||
the first HTTP server to reject any already set <tt class="docutils literal"><span class="pre">X-Forwarded-For</span></tt>
|
||||
headers or users can just use a browser extension or a proxy to inject a
|
||||
forged value in there. And people do that. I remember that people did
|
||||
exactly that to get American streaming content available in Europe by just
|
||||
injecting an American IP into that header.</p>
|
||||
<p>With websockets it's worse. Odds are that your frontend proxy is doing
|
||||
TCP level proxying. If it would be doing HTTP level load balancing and
|
||||
it would understand the <tt class="docutils literal">Upgrade</tt> header it you could make it inject an
|
||||
<tt class="docutils literal"><span class="pre">X-Forwarded-For</span></tt> header and then read that on the server. However
|
||||
Amazon's ELB as mentioned only works with websockets if you set it to TCP
|
||||
level proxying for instance. And with that you lose your remote address.
|
||||
Maybe not a problem for you, it definitely is a problem for us. Now I
|
||||
know that protocols exists to inform a peer about the IP address on the
|
||||
other side of a proxy connection, but ELB does not speak it so it's a
|
||||
rather uninteresting thing for me.</p>
|
||||
<p>The way our server works is pretty similar to how the node.js juggernaut
|
||||
server used to work. You connect to a presence server that holds your
|
||||
socket connection open and acts as a central hub that uses other means to
|
||||
communicate with backend services. We have a pretty lightweight server
|
||||
and we don't want to make it too complicated to authorize requests against
|
||||
it. Namely we don't want to implement OAuth a second time for that server
|
||||
when we already use it for the strictly request/response based webservice
|
||||
infrastructure.</p>
|
||||
<p>The trick we use for that is that we let a user authorize against the
|
||||
webservice infrastructure through OAuth and then call an endpoint that
|
||||
gives you a ticket that is valid for a few seconds. That ticket is stored
|
||||
in redis. It contains your account ID and a few important internal
|
||||
attributes, but we also store the IP on it which you used at the time the
|
||||
ticket was generated.</p>
|
||||
<p>With that ticket you go to the presence server and redeem it. The
|
||||
presence server only needs to connect to redis and check if such a ticket
|
||||
exist and delete it. Since we already use redis anyways in that server
|
||||
it's a pretty simple undertaking. We obviously assume here that nobody
|
||||
takes the ticket between IP addresses. We can't gurantee that this does
|
||||
not happen but we don't care about the implications either. Someone could
|
||||
obviously create the ticket through a VPN and then disable the VPN
|
||||
connection when it's time to connect to the presence server. But to be
|
||||
honest: I don't care. It's as far as I am concerned, no security problem.</p>
|
||||
<p>In theory the spec says you can also use any of the HTTP authorization
|
||||
systems (basic auth, cookies etc.) but since you can't customize the
|
||||
headers with the JavaScript API that are being sent with the handshake you
|
||||
are basically very limited to implicit auth or auth through one of the
|
||||
first requests / URL based.</p>
|
||||
</div>
|
||||
<div class="section" id="framing">
|
||||
<h2>Framing</h2>
|
||||
<p>Now that we know how we can connect to the websocket server, how to
|
||||
upgrade to the websocket protocol and how authorization can be handled
|
||||
without losing the IP address information even if we do TCP level load
|
||||
balancing. The next thing you have to know is how websocket transfer
|
||||
works. As mentioned earlier websocket is not a stream based protocol like
|
||||
TCP, it's message based. What's the difference? With TCP you send bytes
|
||||
around and have to make sure (for the most part) that you can figure out
|
||||
the end of a message. Our own protocol makes this very easy because we
|
||||
send full JSON objects around which are self terminating. For naive JSON
|
||||
parsers (like the one in the Python standard library) that cannot parse of
|
||||
a stream we also add a newline at the end and ensure that all newlines in
|
||||
JSON strings are escaped. So you can just read to the newline and then
|
||||
hand that line to the JSON parser.</p>
|
||||
<p>Websockets makes this easier because it puts a frame around everything.
|
||||
Upside: easier to handle from the JavaScript side, downside: much harder
|
||||
to handle on the server side because you now need to wrap everything in
|
||||
frames.</p>
|
||||
<p>So let's have a look first how the frames are defined. This is what the
|
||||
RFC provides us with:</p>
|
||||
<pre class="literal-block">
|
||||
+-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-------+-+-------------+-------------------------------+
|
||||
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||
|I|S|S|S| (4) |A| (7) | (16/64) |
|
||||
|N|V|V|V| |S| | (if payload len==126/127) |
|
||||
| |1|2|3| |K| | |
|
||||
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
||||
| Extended payload length continued, if payload len == 127 |
|
||||
+ - - - - - - - - - - - - - - - +-------------------------------+
|
||||
| | Masking-key, if MASK set to 1 |
|
||||
+-------------------------------+-------------------------------+
|
||||
| Masking-key (continued) | Payload Data |
|
||||
+-------------------------------- - - - - - - - - - - - - - - - +
|
||||
: Payload Data continued ... :
|
||||
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
||||
| Payload Data continued ... |
|
||||
+---------------------------------------------------------------+
|
||||
</pre>
|
||||
<p>Good news first: as of the websocket version specified by the RFC it's
|
||||
only a header in front of each packet. The bad news is that it's a rather
|
||||
complex header and it has the frighting word “mask” in it. Here are the
|
||||
individual parts explained:</p>
|
||||
<ul>
|
||||
<li><p class="first"><tt class="docutils literal">fin</tt> (<em>1 bit</em>): indicates if this frame is the final frame that
|
||||
makes up the message. Most of the time the message fits into a
|
||||
single frame and this bit will always be set. Experiments show that
|
||||
Firefox makes a second frame after 32K however.</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal">rsv1</tt>, <tt class="docutils literal">rsv2</tt>, <tt class="docutils literal">rsv3</tt> (<em>1 bit each</em>): it wouldn't be a proper
|
||||
protocol if it did not include reserved bits. As of right now, they
|
||||
are unused.</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal">opcode</tt> (<em>4 bits</em>): the opcode. Mainly says what the frame
|
||||
represents. The following values are currently in use:</p>
|
||||
<div class="line-block">
|
||||
<div class="line"><tt class="docutils literal">0x00</tt>: this frame continues the payload from the last.</div>
|
||||
<div class="line"><tt class="docutils literal">0x01</tt>: this frame includes utf-8 text data.</div>
|
||||
<div class="line"><tt class="docutils literal">0x02</tt>: this frame includes binary data.</div>
|
||||
<div class="line"><tt class="docutils literal">0x08</tt>: this frame terminates the connection.</div>
|
||||
<div class="line"><tt class="docutils literal">0x09</tt>: this frame is a ping.</div>
|
||||
<div class="line"><tt class="docutils literal">0x0a</tt>: this frame is a pong.</div>
|
||||
</div>
|
||||
<p>(As you can see, there are enough values unused, they are reserved for
|
||||
future usage).</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal">mask</tt> (<em>1 bit</em>): indicates if the connection is masked. As it
|
||||
stands right now, every message from client to server <em>must be masked</em>
|
||||
and the spec wants to to terminate the connection if it's unmasked.</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal">payload_len</tt> (<em>7 bits</em>): the length of the payload. 7 bits is not
|
||||
enough? Of course not. Websocket frames come in the following length
|
||||
brackets:</p>
|
||||
<p>0-125 mean the payload is that long. 126 means that the following two
|
||||
bytes indicate the length, 127 means the next 8 bytes indicate the
|
||||
length. So it comes in ~7bit, 16bit and 64bit. I don't even have
|
||||
words for this. My browser fragments off after 32K of payload
|
||||
anyways, when would I ever send a package of 64bit size (oh well, the
|
||||
most significant bit must be null at least)? 32bit would have been
|
||||
plenty but oh well. This also means there is more than one way to
|
||||
represent the length but the spec is very clear about only using the
|
||||
shortest possible way to define the length of a frame.</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal"><span class="pre">masking-key</span></tt> (<em>32 bits</em>): if the mask bit is set (and trust me, it
|
||||
is if you write for the server side) you can read for unsigned bytes
|
||||
here which are used to xor the payload with. It's used to ensure that
|
||||
shitty proxies cannot be abused by attackers from the client side.</p>
|
||||
</li>
|
||||
<li><p class="first"><tt class="docutils literal">payload</tt>: the actual data and most likely masked. The length of
|
||||
this is the length of the <tt class="docutils literal">payload_len</tt>.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Why websockets are frame based and not stream based? Probably because
|
||||
JavaScript lacks the tools to deal with stream data. Personally I take a
|
||||
stream based socket over a frame based one every day because if I really
|
||||
need frames, I can express them on top of a stream ones easily.</p>
|
||||
</div>
|
||||
<div class="section" id="data-frames">
|
||||
<h2>Data Frames</h2>
|
||||
<p>Let's start talking about the data on the frames. As mentioned before the
|
||||
data can be fragmented into multiple frames. The first frame that
|
||||
transmits the data has a opcode on it that indicates what sort of data is
|
||||
being transmitted. This is only really necessary because JavaScript has
|
||||
pretty much nonexisting support for binary data at the time the
|
||||
specification was started and now we have to live with it. <tt class="docutils literal">0x01</tt>
|
||||
indicates utf-8 encoded text data, <tt class="docutils literal">0x02</tt> is binary data. Most people
|
||||
will transmit JSON over that thing anyways in which case you probably want
|
||||
to chose the text opcode. When you emit binary data it will be
|
||||
represented in a browser specific <tt class="docutils literal">Blob</tt> object.</p>
|
||||
</div>
|
||||
<div class="section" id="fragmentation-and-masking">
|
||||
<h2>Fragmentation and Masking</h2>
|
||||
<p>Ah, the best part. Payload data can be split up into multiple individual
|
||||
frames. The receiving end is supposed to buffer them up until the <tt class="docutils literal">fin</tt>
|
||||
but is set. So you can transmit the string <tt class="docutils literal">Hello World</tt> in 11 packages
|
||||
of 6 (header length) + 1 byte each if that's what floats your boat.
|
||||
However fragmentation is not allowed for control packages. However
|
||||
the specification wants you to be able to handle interleaved control
|
||||
frames. You know in case TCP packages arrive in arbitrary order :-/.</p>
|
||||
<p>The logic for joining frames is roughly this: receive first frame,
|
||||
remember opcode, concatenate frame payload together until the <tt class="docutils literal">fin</tt> bit
|
||||
is set. Assert that the opcode for each package is zero.</p>
|
||||
<p>The reason for fragmentation supposedly that you can generate bits and
|
||||
pieces of information on the server and send them to the client to buffer
|
||||
up instead of the server and vice versa. It's annoying to handle on both
|
||||
client and server but I can see how it makes it easier for a JavaScript
|
||||
programmer to handle. The dirty bits of the transport protocol are
|
||||
entirely hidden away. Since the server's native protocol was entirely
|
||||
based on the concept of streaming JSON there is zero value in the
|
||||
messages. I suppose that will be a common thing for people adding
|
||||
websocket support to servers that previously spoke some other protocol on
|
||||
top of TCP.</p>
|
||||
<p>But when dealing with the payload we not only have to concatenate frames
|
||||
together, we also have to unmask them. The unmasking is pretty simple
|
||||
once you have the mask key:</p>
|
||||
<div class="highlight"><pre><span></span><span class="kt">uint8_t</span><span class="w"> </span><span class="n">payload</span><span class="p">[</span><span class="n">payload_len</span><span class="p">];</span><span class="w"></span>
|
||||
<span class="n">read_bytes</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span><span class="w"> </span><span class="n">payload_len</span><span class="p">);</span><span class="w"></span>
|
||||
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="n">payload_len</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"></span>
|
||||
<span class="w"> </span><span class="n">payload</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">^=</span><span class="w"> </span><span class="n">mask</span><span class="p">[</span><span class="n">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">4</span><span class="p">];</span><span class="w"></span>
|
||||
</pre></div>
|
||||
<p>Masking is the best part because it makes debugging so incredible fun.</p>
|
||||
<p>Why is there masking at all? Because apparently there is enough broken
|
||||
infrastructure out there that lets the upgrade header go through and then
|
||||
handles the rest of the connection as a second HTTP request which it then
|
||||
stuffs into the cache. I have no words for this. In any case, the
|
||||
defense against that is basically a strong 32bit random number as masking
|
||||
key. Or you know… use TLS and don't use shitty proxies.</p>
|
||||
<p>In the case of our proprietary protocol that's not even a problem because
|
||||
we only allow JSON requests. As such if you would attempt to attempt to
|
||||
submit a HTTP request in place of a JSON payload the server would respond
|
||||
with a generic error message which is not very useful attacking purposes.
|
||||
But really… use TLS and don't use shitty proxies.</p>
|
||||
</div>
|
||||
<div class="section" id="heartbeating">
|
||||
<h2>Heartbeating</h2>
|
||||
<p>Heartbeating is useful, I can agree with that. First of all certain
|
||||
things (like ELB \o/) will terminate idle connections, secondly is it not
|
||||
possible for the receiving side to see if the remote side terminated.
|
||||
Only at the next send would you realize that something went wrong. With
|
||||
websockets you can send the ping opcode at any time to ask the other side
|
||||
to pong. Pings can be sent whenever an endpoint thinks it should and a
|
||||
pong is sent “as soon as is practical”. Someone also decided that
|
||||
something like ping and pong are too simple so they were “improved” so
|
||||
that they can carry application data and if you pong you have to send the
|
||||
payload of the ping back. Sounds easy enough to implement but this
|
||||
actually can make it fairly annoying to deal with because now there is
|
||||
something you have to remember from a ping to a pong (and that application
|
||||
data an be up to 125 bytes).</p>
|
||||
<p>Since you can't send pings/pongs yourself anyways from JavaScript I chose
|
||||
to ignore the application data part for the moment until I find a browser
|
||||
that actually sends something there. (If it does, I will be very sad and
|
||||
complain loudly on Twitter)</p>
|
||||
</div>
|
||||
<div class="section" id="closing-the-connection">
|
||||
<h2>Closing the Connection</h2>
|
||||
<p>Lastly: closing connections. Now to go with the rest of the pattern
|
||||
websocket rolls its own thing here. In theory a TCP disconnect should
|
||||
work as well but it looks like that at least Firefox just reconnects on
|
||||
connection drop. Instead a connection is terminated by sending the close
|
||||
opcode (<tt class="docutils literal">0x08</tt>). There the pattern seems to be to exchange close
|
||||
opcodes first and then let the server shut down. The client is supposed
|
||||
to give the server some time to close the connection before attempting to
|
||||
do that on its own. The close can also signal why it terminated the
|
||||
connection. The lazy person I am I just did not care and just close.
|
||||
Important however is that you do send the opcode around, otherwise at
|
||||
least Firefox will not really believe that you closed the connection.</p>
|
||||
<p>It should be said that the specification does not introduce a close opcode
|
||||
just because to make the protocol more complex. It does have actual use
|
||||
in that it makes the disconnect more reliable. Anyone that ever had to
|
||||
deal with TCP disconnects will know that this can be a somewhat tricky
|
||||
thing to do and behaves differently on different environments. That being
|
||||
said, I don't believe that websocket implementations will get disconnects
|
||||
right either which now leaves developers on both sides hope that the
|
||||
implementation is correct. It's too far from TCP that you could fix the
|
||||
problem yourself if you are programmer that writes the client-side
|
||||
implementation.</p>
|
||||
</div>
|
||||
<div class="section" id="browser-differences">
|
||||
<h2>Browser Differences</h2>
|
||||
<p>There are also differences between how browsers respond to websockets.
|
||||
For instance if you do not provide an application level protocol with
|
||||
Chrome but the server emits the <tt class="docutils literal"><span class="pre">Sec-WebSocket-Protocol</span></tt> header Chrome
|
||||
will loudly complain about a protocol mismatch whereas Firefox does not
|
||||
care a single bit. Safari 5.1 (the one I have installed) does not speak
|
||||
the current protocol of websockets and sends different headers altogether.</p>
|
||||
<p>What's also interesting is that Firefox treats the upgrade as an actual
|
||||
HTTP request and sends the regular headers (User agent, DNT flag, accept
|
||||
headers, cache control, etc.). On the other hand Chrome will just submit
|
||||
the bare minimum and special cases the cookie header. As such you won't
|
||||
be able to do browser based detection just from the handshake.</p>
|
||||
</div>
|
||||
<div class="section" id="lessons-learned">
|
||||
<h2>Lessons Learned</h2>
|
||||
<p>Websockets are complex, way more complex than I anticipated. I can
|
||||
understand that they work that way but I definitely don't see a value in
|
||||
using websockets instead of regular TCP connections if all you want is to
|
||||
exchange data between different endpoints and neither is a browser. If
|
||||
you do want to make them work I recommend the following things:</p>
|
||||
<ul class="simple">
|
||||
<li>keep your websockets miles away from the rest of your application
|
||||
code. Keep them in separate server if you can. They have completely
|
||||
different performance characteristics than your regular HTTP requests
|
||||
and I would not be surprised if they moved further from HTTP in the
|
||||
future. Instead use something to communicate from your websocket
|
||||
server to the rest of the infrastructure (redis pubsub for instance).</li>
|
||||
<li>Expect websockets to not be implemented entirely correct. When I got
|
||||
my implementation working I was not even at ~50% of RFC compliance but
|
||||
it already worked good enough that I did not bother too much with it.
|
||||
I am pretty sure I won't be the last person that creates a half-arsed
|
||||
websocket implementation.</li>
|
||||
<li>Require TLS. Unless you are debugging websockets locally always
|
||||
require TLS/SSL. There are just too many broken HTTP implementations
|
||||
out there that break websockets.</li>
|
||||
<li>Consider a one-time-token auth system for establishing the connection
|
||||
instead of teaching your websocket server how to read your cookies /
|
||||
sessions. It's easier to handle and more flexible. And it's also the
|
||||
perfect solution to get the whole thing running behind the ELB (which
|
||||
I can recommend because it does perfect SSL offloading and provides
|
||||
you with virtual IP addresses in case you need to speak to endpoints
|
||||
that don't do SNI).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="going-forward">
|
||||
<h2>Going Forward</h2>
|
||||
<p>Looking at the RFC it's pretty clear that websockets won't be getting any
|
||||
simpler any time soon. It hints towards adding multiplexing support and
|
||||
dropping the remaining HTTP upgrade parts. Also there is a builtin
|
||||
extension system that will soon be used to negotiate per-frame
|
||||
compression and that's definitely not the end of it. I also did not
|
||||
mention that you can negotiate version numbers and application level
|
||||
protocols through the websocket handshake or that it defines termination
|
||||
codes when the connection closes. There is an awful lot of stuff in the
|
||||
specification and even more to come which gives me the impression that we
|
||||
will see some broken implementations of websockets in the future.</p>
|
||||
<p>Maybe a simple policy file like Unity3D and Flash are using would have
|
||||
been a better idea and just let people speak TCP themselves. At least it
|
||||
leaves you as application developer with more options to fix problems
|
||||
yourself instead of hoping the websocket implementation is 100% correct.
|
||||
But well, that's what we're stuck with now and since it already took three
|
||||
or four major revisions of the specification and god knows how many
|
||||
browser updates it's probably not a very wise decision to revisit the
|
||||
protocol now. I do however believe that when browsers finally get CORS
|
||||
running for SSE this might be a better solution for many use-cases where
|
||||
people might want to use websockets. And that is definitely easier to
|
||||
implement.</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p class=tags>This entry was tagged
|
||||
|
||||
<a href="/tags/python/">python</a> and
|
||||
<a href="/tags/websocket/">websocket</a>
|
||||
|
||||
|
||||
</div>
|
||||
<div class=footer>
|
||||
<p>© Copyright 2023 by Armin Ronacher.
|
||||
<p>
|
||||
Content licensed under the Creative Commons
|
||||
attribution-noncommercial-sharealike License.
|
||||
<p>
|
||||
Contact me via <a href="mailto:armin.ronacher@active-4.com">mail</a>,
|
||||
<a href="http://twitter.com/mitsuhiko">twitter</a>,
|
||||
<a href="http://github.com/mitsuhiko">github</a> or
|
||||
<a href="http://bitbucket.org/mitsuhiko">bitbucket</a>.
|
||||
<p>
|
||||
You can <a href="https://github.com/sponsors/mitsuhiko/">sponsor me on github</a>.
|
||||
<p>
|
||||
More info: <a href="/about#imprint">imprint</a>.
|
||||
Subscribe <a href="/feed.atom" rel="alternate" title="Armin Ronacher's
|
||||
Thoughts and Writings">to Atom feed</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue