// 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(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[pos]))) ++pos; return pos!=begin; }; while(true) { space(); // RFC2616 #rule permits empty list elements, but 1# needs a real one. while(pos(text[pos++]); if(ch=='\\') { if(pos==text.size()) return false; ch=static_cast(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(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 wsTokens(const std::string& text,const Source& s,bool lower=false) { std::vector 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(chhigh) 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(4*((n+2)/3))+1,'\0'); int size=EVP_EncodeBlock(reinterpret_cast(result.data()),data,n); result.resize(static_cast(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(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(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 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(0x80|opcode)); unsigned mask=client ? 0x80 : 0; if(data.size()<126) out.wire.push_back(static_cast(mask|data.size())); else if(data.size()<=65535) { out.wire.push_back(static_cast(mask|126)); out.wire.push_back(static_cast(data.size()>>8)); out.wire.push_back(static_cast(data.size())); } else { out.wire.push_back(static_cast(mask|127)); for(int i=7;i>=0;--i) out.wire.push_back(static_cast(static_cast(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(key),4); } for(std::size_t i=0;i(static_cast(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 headers; std::size_t pos=first+2,countHeaders=0; while(pos64) 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(input[0]),b1=static_cast(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(input[2])<<8)|static_cast(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(input[i]); header=10; if(n<65536 || (static_cast(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()(n)) { parseReady=false; return ""; } data=input.substr(header,static_cast(n)); if(mask) for(std::size_t i=0;i(static_cast(data[i])^static_cast(input[header-4+i%4])); input=std::string(input.data()+header+n,input.size()-header-static_cast(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(data[0])<<8)|static_cast(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); }