Skip to content

Instantly share code, notes, and snippets.

@aleph-naught2tog
Created April 14, 2019 20:11
Show Gist options
  • Save aleph-naught2tog/337cedfb752e762d2c6ff6c171f06947 to your computer and use it in GitHub Desktop.
Save aleph-naught2tog/337cedfb752e762d2c6ff6c171f06947 to your computer and use it in GitHub Desktop.
WebSocket header encoding (key, accept)

Generating the Sec-WebSocket-* headers

The functions included are for generating the base64-encode of a sha1 hash of the input string sent in as an argument.

About

This is the mechanism for processing something to be used as the header for either the Sec-WebSocket-Key request header or Sec-WebSocket-Accept response header in the websocket opening handshake.

To generate the Sec-WebSocket-Key, you would use generateRandomBase64String; for the matching Sec-WebSocket-Accept header value, you would use base64EncodeOfSha1Hash(keyConcatenatedWithMagicString).

The client would send the initial request with the Sec-WebSocket-Key; the server, on receipt, would after having done some validation of the request, send a response containing a Sec-WebSocket-Accept header with the value as described above.

Finally, once the client received the response, it would do the same thing the server did with the key the client sent over -- i.e., calculate the base64EncodeOfSha1Hash of the key concatenated with the magic string, and compare that to the received Sec-WebSocket-Accept header value.

As an example, a Sec-WebSocket-Key of 'xqBt3ImNzJbYqRINxEFlkg==' should generate the Sec-WebSocket-Accept header value of 'K7DJLdLooIwIG/MOpvWFB3y3FE8='.

In a browser, 'K7DJLdLooIwIG/MOpvWFB3y3FE8=' === await generateWebSocketAcceptHeader('xqBt3ImNzJbYqRINxEFlkg==') would be true; in Node, you would instead expect 'K7DJLdLooIwIG/MOpvWFB3y3FE8=' === generateWebSocketAcceptHeader('xqBt3ImNzJbYqRINxEFlkg==') to be true.

Example

// client

/*
  Note: if you're opening a websocket from the browser, you wouldn't do any of
  this. This is only for scenarios when you can write requests manually, which
  the browser prevents.
*/
const key = generateRandomBase64String();

// this is just a less-error-prone way of making the request, in my opinion
// (no worrying about newlines, forgetting the final carriage return, etc.)
const rawRequest = [
  `GET / HTTP/1.1`,
  `Connection: Upgrade`,
  `Upgrade: websocket`,
  `Sec-WebSocket-Version: 13`,
  `Sec-WebSocket-Key: ${key}`, // <-- voila!
  `\r\n` // <- don't forget this! without it, the request never completes.
].join('\n');

somehowSendRequest(rawRequest);
// server (node)

// this string is just part of the RFC protocol spec.
const MAGIC_SOCKET_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

// on receiving the request...
//    this is a bit handwavey:
//    we are assuming SOMETHING gives us a way to get the header neatly
const requestObject = magicallyParseRequest(receivedFromClientRequest);
const key = requestObject['sec-websocket-key'];

// we need to process the key concatenated with the magic string
//    `${key}${MAGIC_SOCKET_STRING}` is how we concatenate it.
const acceptHeaderValue = base64EncodeOfSha1Hash(`${key}${MAGIC_SOCKET_STRING}`);

const rawResponse = [
  `HTTP/1.1 101 Websocket upgrade`,
  `Connection: upgrade`,
  `Upgrade: websocket`,
  `Sec-WebSocket-Accept: ${acceptHeaderValue}`,
  `\r\n` // <- don't forget this...!
].join('\n');

somehowSendResponse(rawResponse);
/**
* @async
*
* @param {string} inputString the string to be hashed and encoded
* @returns {string} the base64 encoded sha1 hash
* @example
```
const message = 'hello world';
const result = await async_browser_base64EncodeOfSha1Hash(message);
console.log(result);
// 'Kq5sNclPz7QV2+lfQIuc6R7oRu0='
```
*/
async function base64EncodeOfSha1Hash(inputString) {
const stringAsUIntArray = new TextEncoder().encode(inputString);
const hashedArrayBuffer = await window.crypto.subtle.digest(
'SHA-1',
stringAsUIntArray
);
const hashAsUIntArray = new Uint8Array(hashedArrayBuffer);
const hashAsString = String.fromCodePoint(...hashAsUIntArray);
const base64EncodedHash = btoa(hashAsString);
return base64EncodedHash;
}
/**
* The hashing function itself returns a promise, whose resolved value is the
* `ArrayBuffer` containing the digest.
*
* @param {string} inputString the string to be hashed and encoded
* @returns {Promise<string>} the base64-encoded sha1 hash
*
* @example
```
const message = 'hello world';
promise_browser_base64EncodeOfSha1Hash(message)
.then(hashedResult => console.log(result))
.catch(error => console.error(error));
// 'Kq5sNclPz7QV2+lfQIuc6R7oRu0='
```
*/
function promise_base64EncodeOfSha1Hash(inputString) {
const stringAsUIntArray = new TextEncoder().encode(inputString);
return window.crypto.subtle
.digest('SHA-1', stringAsUIntArray)
.then(hashedArrayBuffer => {
const hashAsUIntArray = new Uint8Array(hashedArrayBuffer);
const hashAsString = String.fromCodePoint(...hashAsUIntArray);
const base64EncodedHash = btoa(hashAsString);
return new Promise((resolve, _reject) => {
resolve(base64EncodedHash);
});
})
.catch(console.error);
}
/**
* Default value of 16 for the length of the string generated.
*
* @param {number} [howMany=16] the length of the string, in bytes
* @returns {string} the base64-encoded string of length `howMany`
*/
function generateRandomBase64String(howMany = 16) {
const buffer = new Uint8Array(howMany);
const randomBytes = window.crypto.getRandomValues(buffer);
const asString = String.fromCodePoint(...randomBytes);
return btoa(asString);
}
/**
* @returns {string} a randomly-generated base64-encoded string
*/
function generateWebSocketKeyHeader() {
return generateRandomBase64String();
}
/**
* @param {string} key the value of the `Sec-WebSocket-Key` header
* @returns {string} the matching value for a `Sec-WebSocket-Accept` header
*/
function generateWebSocketAcceptHeader(key) {
const MAGIC_SOCKET_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
return base64EncodeOfSha1Hash(`${key}${MAGIC_SOCKET_STRING}`);
}
/**
* @param {string} inputString the string to be hashed and encoded
* @returns {string} the base64 encoded sha1 hash
*
* @requires the node [crypto](https://nodejs.org/api/crypto.html) module
*
* @example
```
const message = 'hello world';
const result = node_base64EncodeOfSha1Hash(message);
console.log(result);
// 'Kq5sNclPz7QV2+lfQIuc6R7oRu0='
```
*/
function base64EncodeOfSha1Hash(inputString) {
const crypto = require('crypto');
const sha1Hash = crypto.createHash('sha1');
const hashedString = sha1Hash.update(inputString);
const base64EncodedHash = hashedString.digest('base64');
return base64EncodedHash;
}
/**
* Default value of 16 for the length of the string generated.
*
* @param {number} [howMany=16] the length of the string, in bytes
* @returns {string} the randomly generated string of length `howMany`
*/
function generateRandomBase64String(howMany = 16) {
const crypto = require('crypto');
const randomBytes = crypto.randomBytes(howMany);
return randomBytes.toString('base64');
}
/**
* @returns {string} a randomly-generated base64-encoded string
*/
function generateWebSocketKeyHeader() {
return generateRandomBase64String();
}
/**
* @param {string} key the value of the `Sec-WebSocket-Key` header
* @returns {string} the matching value for a `Sec-WebSocket-Accept` header
*/
function generateWebSocketAcceptHeader(key) {
const MAGIC_SOCKET_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
return base64EncodeOfSha1Hash(`${key}${MAGIC_SOCKET_STRING}`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment