/*
* opencog/network/WebSocket.cc
*
* Copyright (C) 2022 Linas Vepstas
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// OpenSSL is needed for only one thing: the SHA1 hash of the websocket
// key. It is not used for anything else.
#ifdef HAVE_OPENSSL
#include <string>
#include <openssl/sha.h>
#include <opencog/util/exceptions.h>
#include <opencog/util/Logger.h>
#include "ServerSocket.h"
using namespace opencog;
// ==================================================================
/// Read from the websocket, decoding all framing and control bits,
/// and return the text data as a string. This returns one frame
/// at a time. No attempt is made to consolidate fragments.
std::string ServerSocket::get_websocket_line()
{
// If we are here, then we are expecting a frame header.
// Get frame and opcode
unsigned char fop;
asio::read(*_socket, asio::buffer(&fop, 1));
// bool finbit = fop & 0x80;
unsigned char opcode = fop & 0xf;
// Handle pings
while (9 == opcode or 0xa == opcode)
{
std::string pingd = get_websocket_data();
// If ping, send a pong, copying the data.
if (9 == opcode)
{
size_t paylen = pingd.size();
char header[2];
header[0] = 0x8a;
header[1] = (char) paylen;
Send(asio::const_buffer(header, 2));
if (0 < paylen)
Send(asio::const_buffer(pingd.data(), paylen));
}
// And wait for the next frame...
asio::read(*_socket, asio::buffer(&fop, 1));
// finbit = fop & 0x80;
opcode = fop & 0xf;
}
// Socket close message .. just quit.
if (8 == opcode)
{
logger().info("Received WebSocket close");
throw SilentException();
}
// We only support text data.
if (1 != opcode)
{
logger().warn("Not expecting binary websocket data; opcode=%d",
opcode);
throw SilentException();
}
return get_websocket_data();
}
/// Read from the websocket, decoding the length and data.
/// Assumes the opcode has already been read.
/// Return the text data as a string. This returns one frame
/// at a time. No attempt is made to consolidate fragments.
std::string ServerSocket::get_websocket_data(void)
{
// Mask and payload length
unsigned char mpay;
asio::read(*_socket, asio::buffer(&mpay, 1));
bool maskbit = mpay & 0x80;
int8_t paybyte = mpay & 0x7f;
int64_t paylen = paybyte;
if (126 == paybyte)
{
uint16_t shore;
asio::read(*_socket, asio::buffer(&shore, 2));
paylen = ntohs(shore);
}
else if (127 == paybyte)
{
uint32_t lunglo, lunghi;
asio::read(*_socket, asio::buffer(&lunghi, 4));
asio::read(*_socket, asio::buffer(&lunglo, 4));
uint64_t lung = ntohl(lunghi);
lung = lung << 32 | ntohl(lunglo);
if ((1UL << 40) < lung)
{
logger().warn("Websocket insane length %lu\n", lung);
throw SilentException();
}
paylen = lung;
}
// It is an error if the maskbit is not set. Bail out.
if (not maskbit)
{
logger().warn("WebSocket received unmasked data!");
throw SilentException();
}
uint32_t mask;
asio::read(*_socket, asio::buffer(&mask, 4));
// Use malloc inside of std::string to get a buffer.
std::string blob;
blob.resize(paylen);
char* data = blob.data();
asio::read(*_socket, asio::buffer(data, paylen));
// Bulk unmask the data, using XOR.
uint32_t *dp = (uint32_t *) data;
int64_t i=0;
while (i <= paylen-4)
{
*dp = *dp ^ mask;
++dp;
i += 4;
}
// Unmask any remaining bytes.
for (unsigned int j=0; j<paylen%4; j++)
data[i+j] = data[i+j] ^ ((mask >> (8*j)) & 0xff);
// We're not actually going to use a line protocol, when we're
// using websockets. If the user wants to search for newline
// chars in the datastream, they are welcome to. We're not
// going to futz with that.
return blob;
}
/// Send a WebSocket pong message.
void ServerSocket::send_websocket_pong()
{
char header[2];
header[0] = 0x8a;
header[1] = 0;
Send(asio::const_buffer(header, 2));
}
/// Send string via websocket, performing framing.
void ServerSocket::send_websocket(const std::string& cmd)
{
// Send only one packet, and indicate it's length.
size_t paylen = cmd.size();
char header[10];
header[0] = 0x81;
if (paylen < 126)
{
header[1] = (char) paylen;
Send(asio::const_buffer(header, 2));
}
else if (paylen < 65536)
{
header[1] = 126;
header[2] = (paylen >> 8) & 0xff;
header[3] = paylen & 0xff;
Send(asio::const_buffer(header, 4));
}
else
{
header[1] = 127;
header[2] = (paylen >> 56) & 0xff;
header[3] = (paylen >> 48) & 0xff;
header[4] = (paylen >> 40) & 0xff;
header[5] = (paylen >> 32) & 0xff;
header[6] = (paylen >> 24) & 0xff;
header[7] = (paylen >> 16) & 0xff;
header[8] = (paylen >> 8) & 0xff;
header[9] = paylen & 0xff;
Send(asio::const_buffer(header, 10));
}
// Send the actual data.
Send(asio::const_buffer(cmd.c_str(), paylen));
}
// ==================================================================
/// Given a char buffer of data (possibly including nulls)
/// return the base64 encoding of it.
// Found code blob on stackexchange from user Manuel Martinez.
static std::string base64_encode(unsigned char* buf, int len)
{
std::string out;
unsigned int val = 0;
int valb = -6;
for (int i=0; i<len; i++)
{
unsigned char c = buf[i];
val = (val << 8) + c;
valb += 8;
while (valb >= 0)
{
out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(val>>valb)&0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[((val<<8)>>(valb+8))&0x3F]);
while (out.size()%4) out.push_back('=');
return out;
}
/// Process the HTTP header and optionally perform the websockets
/// handshake. That is, listen for the HTTP header, and pick through
/// the various fields in it. If it has an `Upgrade: websocket` line in
/// it, then upgrade to websockets i.e. perform the magic-key exchange,
/// etc. Upon upgrade, the socket is ready to send and receive websocket
/// frames. Otherwise, treat the socket as an ordinary telnet-like
/// socket, with keep-alive set. In either case, the remaining I/O gets
/// routed to some shell handler, depending on the URL.
void ServerSocket::HandshakeLine(const std::string& line)
{
// The very first HTTP line.
if (not _got_first_line)
{
_got_first_line = true;
if (0 == line.compare(0, 4, "GET "))
{
_url = line.substr(4, line.find(" ", 4) - 4);
}
else if (0 == line.compare(0, 5, "POST "))
{
_url = line.substr(5, line.find(" ", 5) - 5);
}
else
{
Send("HTTP/1.1 501 Not Implemented\r\n"
"Server: CogServer\r\n"
"\r\n");
throw SilentException();
}
return;
}
// If the line-size is zero, then we've reached the end of
// header sent by the client.
if (not _got_http_header and 0 == line.size())
{
_got_http_header = true;
}
// Extract stuff from the header the client is sending us.
if (not _got_http_header)
{
static const char* CLen = "Content-Length: ";
if (0 == line.compare(0, strlen(CLen), CLen))
{ _content_length = std::stoi(line.substr(strlen(CLen))); return; }
static const char* clen = "content-length: ";
if (0 == line.compare(0, strlen(clen), clen))
{ _content_length = std::stoi(line.substr(strlen(clen))); return; }
static const char* host = "Host: ";
if (0 == line.compare(0, strlen(host), host))
{ _host_header = line.substr(strlen(host)); return; }
static const char* host_lower = "host: ";
if (0 == line.compare(0, strlen(host_lower), host_lower))
{ _host_header = line.substr(strlen(host_lower)); return; }
static const char* kpal = "connection: keep-alive";
if (0 == line.compare(0, strlen(kpal), kpal))
{ _keep_alive = true; return; }
static const char* upg = "Upgrade: websocket";
if (0 == line.compare(0, strlen(upg), upg))
{ _got_websock_header = true; return; }
static const char* key = "Sec-WebSocket-Key: ";
if (0 == line.compare(0, strlen(key), key))
{ _webkey = line.substr(strlen(key)); return; }
return;
}
// If we are here, then the full HTTP header was received. This
// is enough to get started: call the user's OnConnection()
// method. The user is supposed to handle the rest:
// (a) Do they like the URL in the header? If not, they
// should send some response e.g. 404 Not Found
// and then `throw SilentException()` to close the sock.
// (b) If the URL is reasonable, the socket should be piped to
// the correspondng shell to handle the remaining traffic.
// By default, the socket should be kept open; it can be
// closed at any time with `throw SilentException()` to
// close the sock.
OnConnection();
// A websocket upgrade will not be performed. We are done.
if (not _got_websock_header)
return;
// If we are here, we've received an HTTP header, and it
// as a WebSocket header. Do the websocket reply.
_webkey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
unsigned char hash[SHA_DIGEST_LENGTH]; // == 20
memset(hash, 0, SHA_DIGEST_LENGTH);
SHA1((const unsigned char*) _webkey.c_str(), _webkey.size(), hash);
std::string b64hash = base64_encode(hash, SHA_DIGEST_LENGTH);
std::string response =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: ";
response += b64hash;
response +=
"\r\n"
"\r\n";
Send(response);
// After this point, websockets will send frames.
// Need to change the mode to work with frames.
_do_frame_io = true;
}
#endif // HAVE_OPENSSL
// ==================================================================