restored/websocket_protocol.inc
← All source files · Download this file
14,943 bytes · SHA256 d6c157de7f247c3765329c2c48839eff7e39fa2197a1522d2875669140c8c4b9
// Original bounded RFC6455 codec; transport is in websocket.inc.
static bool wsTokenChar(unsigned char ch) {
return (ch>='a'&&ch<='z') || (ch>='A'&&ch<='Z') || (ch>='0'&&ch<='9') ||
std::string("!#$%&'*+-.^_`|~").find(static_cast<char>(ch))!=std::string::npos;
}
static bool wsToken(const std::string& text) {
if(text.empty()) return false;
for(unsigned char ch:text) if(!wsTokenChar(ch)) return false;
return true;
}
// RFC6455 9.1: syntax only, never extension selection. The containing HTTP
// header is capped at 8192 bytes; this scanner is linear with constant storage.
static bool wsExtensionOffer(const std::string& text) {
std::size_t pos=0; bool found=false;
auto space=[&]() { while(pos<text.size() && (text[pos]==' '||text[pos]=='\t')) ++pos; };
auto token=[&]() { auto begin=pos; while(pos<text.size() && wsTokenChar(static_cast<unsigned char>(text[pos]))) ++pos; return pos!=begin; };
while(true) {
space();
// RFC2616 #rule permits empty list elements, but 1# needs a real one.
while(pos<text.size() && text[pos]==',') { ++pos; space(); }
if(pos==text.size()) return found;
if(!token()) return false;
found=true; space();
while(pos<text.size() && text[pos]==';') {
++pos; space(); if(!token()) return false; space();
if(pos<text.size() && text[pos]=='=') {
++pos; space();
if(pos<text.size() && text[pos]=='"') {
++pos; bool nonempty=false;
while(pos<text.size() && text[pos]!='"') {
unsigned char ch=static_cast<unsigned char>(text[pos++]);
if(ch=='\\') {
if(pos==text.size()) return false;
ch=static_cast<unsigned char>(text[pos++]);
}
// After quoted-pair unescaping the value MUST be token.
if(!wsTokenChar(ch)) return false;
nonempty=true;
}
if(!nonempty || pos==text.size()) return false;
++pos;
} else if(!token()) return false;
space();
}
}
if(pos==text.size()) return true;
if(text[pos]!=',') return false;
++pos;
}
}
static std::string wsLower(std::string text) { for(char& ch:text) if(ch>='A'&&ch<='Z') ch=static_cast<char>(ch-'A'+'a'); return text; }
static std::string wsTrim(const std::string& text) {
auto begin=text.find_first_not_of(" \t"); if(begin==std::string::npos) return "";
return text.substr(begin,text.find_last_not_of(" \t")-begin+1);
}
static std::vector<std::string> wsTokens(const std::string& text,const Source& s,bool lower=false) {
std::vector<std::string> out; std::size_t pos=0;
do { auto end=text.find(',',pos); auto t=wsTrim(text.substr(pos,end==std::string::npos ? end : end-pos));
if(!wsToken(t)) fail(s,"websocket invalid HTTP token list");
out.push_back(lower ? wsLower(t) : t);
if(out.size()>64) fail(s,"websocket HTTP token count limit");
if(end==std::string::npos) break;
pos=end+1;
} while(true);
return out;
}
static bool wsContains(const std::string& text,const std::string& token,const Source& s,bool lower=false) {
auto list=wsTokens(text,s,lower); return std::find(list.begin(),list.end(),token)!=list.end();
}
// Continuation bounds reject impossible prefixes before a codepoint finishes.
// A valid incomplete prefix is retained across frames (and interleaved controls).
struct WSUTF8State {
unsigned left=0; unsigned char low=0x80,high=0xbf;
bool feed(const std::string& text) {
for(unsigned char ch:text) {
if(left) {
if(ch<low || ch>high) return false;
--left; low=0x80; high=0xbf;
} else if(ch<0x80) continue;
else if(ch>=0xc2 && ch<=0xdf) left=1;
else if(ch>=0xe0 && ch<=0xef) {
left=2; low=ch==0xe0 ? 0xa0 : 0x80; high=ch==0xed ? 0x9f : 0xbf;
} else if(ch>=0xf0 && ch<=0xf4) {
left=3; low=ch==0xf0 ? 0x90 : 0x80; high=ch==0xf4 ? 0x8f : 0xbf;
} else return false;
}
return true;
}
bool complete() const { return left==0; }
};
static bool wsUTF8(const std::string& text) {
WSUTF8State state; return state.feed(text) && state.complete();
}
static bool wsCode(unsigned code) { return (code>=1000&&code<=1014&&code!=1004&&code!=1005&&code!=1006) || (code>=3000&&code<=4999); }
static std::string wsBase64(const unsigned char* data,int n) {
std::string result(static_cast<std::size_t>(4*((n+2)/3))+1,'\0');
int size=EVP_EncodeBlock(reinterpret_cast<unsigned char*>(result.data()),data,n); result.resize(static_cast<std::size_t>(size)); return result;
}
static std::string wsAccept(const std::string& key,const Source& s) {
auto text=key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; unsigned char hash[EVP_MAX_MD_SIZE]; unsigned n=0;
if(EVP_Digest(text.data(),text.size(),hash,&n,EVP_sha1(),nullptr)!=1) fail(s,"websocket SHA1 failed");
return wsBase64(hash,static_cast<int>(n));
}
static bool wsKey(const std::string& key) {
if(key.size()!=24 || key.substr(22)!="==") return false;
unsigned char raw[24]; int n=EVP_DecodeBlock(raw,reinterpret_cast<const unsigned char*>(key.data()),24);
return n==18 && wsBase64(raw,16)==key;
}
static void wsSafeField(const std::string& text,const Source& s) {
if(text.size()>1024) fail(s,"websocket HTTP config field limit");
for(unsigned char ch:text) if(ch<=32 || ch>=127) fail(s,"websocket unsafe HTTP config field");
}
struct SocketWebSocket {
enum Phase { receiving, sending, open, closing } phase=receiving;
bool client=false, parseReady=false, closeSent=false, closeReceived=false, applicationPending=false;
unsigned fragment=0, closeCode=0;
WSUTF8State textUTF8;
std::string host,path,origin,protocol,accept,input,message;
struct Output { std::string wire; std::size_t payload=0; unsigned opcode=0; };
std::vector<Output> queue;
std::size_t activePayload=0; unsigned activeOpcode=0;
SocketClock::time_point deadline{};
bool timed() const { return phase!=open; }
std::size_t storage() const {
std::size_t n=sizeof(*this)+host.capacity()+path.capacity()+origin.capacity()+protocol.capacity()+accept.capacity()+input.capacity()+message.capacity()+queue.capacity()*sizeof(Output);
for(const auto& q:queue) n+=q.wire.capacity();
return n;
}
void enqueue(unsigned opcode,const std::string& data,const Source& s) {
if(queue.size()>=8) fail(s,"websocket control/output queue limit");
Output out; out.opcode=opcode; out.payload=(opcode==1||opcode==2) ? data.size() : 0;
out.wire.push_back(static_cast<char>(0x80|opcode)); unsigned mask=client ? 0x80 : 0;
if(data.size()<126) out.wire.push_back(static_cast<char>(mask|data.size()));
else if(data.size()<=65535) { out.wire.push_back(static_cast<char>(mask|126)); out.wire.push_back(static_cast<char>(data.size()>>8)); out.wire.push_back(static_cast<char>(data.size())); }
else { out.wire.push_back(static_cast<char>(mask|127)); for(int i=7;i>=0;--i) out.wire.push_back(static_cast<char>(static_cast<std::uint64_t>(data.size())>>(i*8))); }
unsigned char key[4]{};
if(client) { if(RAND_bytes(key,4)!=1) fail(s,"websocket random mask failed"); out.wire.append(reinterpret_cast<char*>(key),4); }
for(std::size_t i=0;i<data.size();++i) out.wire.push_back(static_cast<char>(static_cast<unsigned char>(data[i])^(client ? key[i%4] : 0)));
queue.push_back(std::move(out));
}
// Complete HTTP header only; all bytes after its delimiter stay in input.
bool upgrade(const Source& s) {
auto end=input.find("\r\n\r\n");
if(end==std::string::npos) { if(input.size()>=8192) fail(s,"websocket HTTP header limit"); parseReady=false; return false; }
if(end+4>8192) fail(s,"websocket HTTP header limit");
auto first=input.find("\r\n"); auto start=input.substr(0,first);
if(client) { if(start.size()<13 || start.substr(0,13)!="HTTP/1.1 101 ") fail(s,"websocket expected HTTP 101"); }
else if(start!="GET "+path+" HTTP/1.1") fail(s,"websocket request path/method mismatch");
for(unsigned char ch:start) if(ch<32||ch>=127) fail(s,"websocket invalid HTTP start line");
std::map<std::string,std::string> headers; std::size_t pos=first+2,countHeaders=0;
while(pos<end) {
auto e=input.find("\r\n",pos); auto line=input.substr(pos,e-pos); auto colon=line.find(':');
if(colon==std::string::npos || !wsToken(line.substr(0,colon))) fail(s,"websocket invalid HTTP header");
auto name=wsLower(line.substr(0,colon)); auto content=wsTrim(line.substr(colon+1));
for(unsigned char ch:content) if((ch<32&&ch!=9)||ch==127) fail(s,"websocket invalid HTTP header value");
if(++countHeaders>64) fail(s,"websocket HTTP header count limit");
if(headers.count(name)) {
if(name!="connection"&&name!="upgrade"&&name!="sec-websocket-protocol") fail(s,"websocket duplicate HTTP header");
headers[name]+=","+content;
} else headers[name]=content;
pos=e+2;
}
if(!wsContains(headers["upgrade"],"websocket",s,true) || !wsContains(headers["connection"],"upgrade",s,true)) fail(s,"websocket missing upgrade tokens");
if(headers.count("content-length") || headers.count("transfer-encoding")) fail(s,"websocket HTTP body forbidden");
if(client) {
if(headers["sec-websocket-accept"]!=accept) fail(s,"websocket accept verification failed");
if(headers.count("sec-websocket-extensions")) fail(s,"websocket extensions unsupported");
if(protocol.empty() ? headers.count("sec-websocket-protocol")!=0 : headers["sec-websocket-protocol"]!=protocol) fail(s,"websocket subprotocol mismatch");
phase=open;
} else {
if(headers["host"]!=host || (origin.empty() ? headers.count("origin")!=0 : (!headers.count("origin") || headers["origin"]!=origin))) fail(s,"websocket Host/Origin mismatch");
if(headers["sec-websocket-version"]!="13" || !wsKey(headers["sec-websocket-key"])) fail(s,"websocket invalid version/key");
if(headers.count("sec-websocket-protocol")) (void)wsTokens(headers["sec-websocket-protocol"],s);
if(headers.count("sec-websocket-extensions") && !wsExtensionOffer(headers["sec-websocket-extensions"])) fail(s,"websocket invalid extension offer");
if(!protocol.empty() && !wsContains(headers["sec-websocket-protocol"],protocol,s)) fail(s,"websocket subprotocol mismatch");
Output response; response.wire="HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+wsAccept(headers["sec-websocket-key"],s)+"\r\n";
if(!protocol.empty()) response.wire+="Sec-WebSocket-Protocol: "+protocol+"\r\n";
response.wire+="\r\n"; queue.push_back(std::move(response)); phase=sending;
}
input=std::string(input.data()+end+4,input.size()-end-4); parseReady=!input.empty(); return true;
}
// At most one frame, returning an event (empty status means need more bytes).
std::string parse(std::string& data,std::string& type,const Source& s) {
if(input.size()<2) { parseReady=false; return ""; }
auto b0=static_cast<unsigned char>(input[0]),b1=static_cast<unsigned char>(input[1]); unsigned op=b0&15; bool fin=(b0&128)!=0,mask=(b1&128)!=0;
if((b0&112) || (op!=0&&op!=1&&op!=2&&op!=8&&op!=9&&op!=10)) fail(s,"websocket invalid RSV/opcode");
if(mask==client) fail(s,"websocket wrong masking direction");
std::uint64_t n=b1&127; std::size_t header=2;
if(op>=8 && (!fin || n>125)) fail(s,"websocket invalid control frame");
if(n==126) { if(input.size()<4) { parseReady=false; return ""; } n=(static_cast<unsigned char>(input[2])<<8)|static_cast<unsigned char>(input[3]); header=4; if(n<126) fail(s,"websocket noncanonical length"); }
else if(n==127) { if(input.size()<10) { parseReady=false; return ""; } n=0; for(std::size_t i=2;i<10;++i) n=(n<<8)|static_cast<unsigned char>(input[i]); header=10; if(n<65536 || (static_cast<unsigned char>(input[2])&128)) fail(s,"websocket noncanonical/overflow length"); }
if(n>65536) fail(s,"websocket frame size limit (65536)");
if(mask) header+=4;
if(input.size()<header+static_cast<std::size_t>(n)) { parseReady=false; return ""; }
data=input.substr(header,static_cast<std::size_t>(n));
if(mask) for(std::size_t i=0;i<data.size();++i) data[i]=static_cast<char>(static_cast<unsigned char>(data[i])^static_cast<unsigned char>(input[header-4+i%4]));
input=std::string(input.data()+header+n,input.size()-header-static_cast<std::size_t>(n)); parseReady=!input.empty();
if(op>=8) {
if(op==8) {
if(data.size()==1) fail(s,"websocket invalid close payload");
closeCode=data.empty() ? 1005U : (static_cast<unsigned char>(data[0])<<8)|static_cast<unsigned char>(data[1]);
if(!data.empty() && (!wsCode(closeCode)||!wsUTF8(data.substr(2)))) fail(s,"websocket invalid close code/reason");
closeReceived=true; return "closing";
}
return op==9 ? "ping" : "pong";
}
// Data already in flight when we sent Close is legal. Validate its
// framing/text, but do not deliver application messages while closing.
bool discard=phase==closing;
if(op==0 ? fragment==0 : fragment!=0) fail(s,"websocket invalid fragmentation sequence");
if(message.size()+data.size()>65536) fail(s,"websocket message size limit (65536)");
if(op) { fragment=op; textUTF8=WSUTF8State{}; }
if(fragment==1 && (!textUTF8.feed(data) || (fin && !textUTF8.complete()))) fail(s,"websocket invalid UTF8 text");
std::string joined=message; joined+=data; message.swap(joined);
data.clear(); if(!fin) return "progress";
type=fragment==1 ? "text" : "binary";
data.swap(message); std::string().swap(message); fragment=0;
if(discard) { data.clear(); type.clear(); return "progress"; }
return "message";
}
};
static void websocketValidate(const CodeLine& c) {
count(c,2,7); auto sub=c.arg[1].text;
if(sub=="abort") { count(c,3,3); return; }
if(sub=="client"||sub=="server"||sub=="send") count(c,6,6);
else if(sub=="step") { count(c,5,5); destination(c.arg[3],c.source); }
else if(sub=="close") count(c,7,7);
else fail(c.source,"unknown websocket operation");
destination(c.arg[2],c.source);
}