PLUGIN ws lang: "C++" version: "1.0" date: "2022-01-30" author: "Julien BRUGUIER" maintainer: "Julien BRUGUIER " synopsis: "A low-level manipulation plugin for WebSocket frames." description: %{ This plugin supports decoding and encoding of WebSocket frames, and basic setters and getters on such frames. .P This plugin does not support the WebSocket handshake. %} example: "A demonstration WebSocket server" %{ .nf #!===SVMBIN=== LOG PLUGIN "svmcom.so" PLUGIN "svmrun.so" PLUGIN "svmstr.so" PLUGIN "svmint.so" PLUGIN "/usr/local/lib/svmpluginhttp/libsvmhttp.so" PLUGIN "/usr/local/lib/svmpluginuri/libsvmuri.so" PLUGIN "/usr/local/lib/svmpluginenc/libsvmenc.so" PLUGIN "===PLUGINLIB===" ARGUMENT STR port PROCESS "server" CODE "main" INLINE :memory com.device/s :com.open com.tcp < "0.0.0.0" @&port -> &s :label loop :call wait s :goto loop :label wait :memory (com.device, STR)/c :com.command @&s CLIENT -> &c @&port -> (c/1) :run.parallel_call $"client" c "client" SCHED=run.parallel :return :symbol client :memory STR/t, http.mesg_1_1/q, uri.address/u, BLN/b, ws.frame/w, INT/s, INT/i, STR/r, STR/id :label loop_http :com.read @&P http.mesg_1_1 -> &t :shutdown :unless &t INITIALISED :http.decode @&t -> &q :http.get_header @&q "Upgrade" -> &t :goto continue_http :unless &t INITIALISED :str.cmp @&t = "websocket" -> &b :goto websocket :when @&b TRUE :label continue_http :http.get_uri @&q -> &t :uri.decode @&t -> &u :uri.get_address @&u -> &t :str.cmp @&t = "/" -> &b :goto not_found :unless @&b TRUE :http.new REPLY 200 -> &q """
Click me
""" -> &t :str.replace @&t ALL CONST str.pattern "PORT" => @(P/1) :http.set_payload @&q @&t :str.size @&t -> &s :int.print @&s -> &t :http.set_header @&q "Content-Length" @&t :com.write @&P @&q :goto loop_http :label not_found :http.new REPLY 404 -> &q :http.set_header @&q "Content-Length" "0" :com.write @&P @&q :goto loop_http :label websocket :http.get_header @&q "Sec-WebSocket-Key" -> &t :str.join @&t "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -> &t :enc.sha1 @&t -> &t :enc.base64 ENCODE @&t -> &t CONST http.mesg_1_1 "HTTP/1.1 101 Switching Protocols\(rsr\(rsnUpgrade: websocket\(rsr\(rsnConnection: Upgrade\(rsr\(rsn\(rsr\(rsn" -> &q :http.set_header @&q "Sec-WebSocket-Accept" @&t :com.write @&P @&q :com.read @&P ws.frame -> &t :ws.decode @&t -> &w :ws.get_payload @&w -> &r 0 -> &i "" -> &t :label loop_websocket :int.print @&i -> &id :str.join @&t "
" @&r "
" -> &t :ws.new -> &w :ws.set_fin @&w TRUE :ws.set_opcode @&w TEXT :ws.set_payload @&w @&t :com.write @&P @&w :com.message @&r " -> " @&i :shift &i :run.sleep SOFT 1 :goto loop_websocket :when @&i IN &0*10 :ws.set_fin @&w TRUE :ws.set_opcode @&w END :com.write @&P @&w :goto loop_http END MEMORY port END .fi %} seealso: %{ .BR svm_plugin_com (7) for networking, and .BR svm_plugin_http (7) and .BR svm_plugin_enc (7) for WebSocket handshake. %} includes: %{ #include #include #include #include #include #include #include %} code: %{ struct WS { typedef enum { CONTINUATION = 0, TEXTE = 1, BINAIRE = 2, FIN = 8, PING = 9, PONG = 10 } OpCode; WS() :_valide(false), _fin(false), _reserve1(false), _reserve2(false), _reserve3(false), _opcode(0) {} WS(const bool fin, const char op_code) :_valide(true), _fin(fin), _reserve1(false), _reserve2(false), _reserve3(false), _opcode(op_code) {} static bool requete_ws_complete(const std::string& tampon, const bool fin, uint64_t& taille, bool& evacue) { evacue = false; if(tampon.size()<2) return false; taille=static_cast(static_cast(tampon[1])) bitand 0x7F; if(taille==126) { if(tampon.size()<4) return false; taille = 4 + (static_cast(static_cast(tampon[2])) << 8) + (static_cast(static_cast(tampon[3]))); } else if(taille==127) { if(tampon.size()<10) return false; taille = 10 + (static_cast(static_cast(tampon[2])) << 56) + (static_cast(static_cast(tampon[3])) << 48) + (static_cast(static_cast(tampon[4])) << 40) + (static_cast(static_cast(tampon[5])) << 32) + (static_cast(static_cast(tampon[6])) << 24) + (static_cast(static_cast(tampon[7])) << 16) + (static_cast(static_cast(tampon[8])) << 8) + static_cast(static_cast(tampon[9])); } else { taille += 2; } bool masque = (static_cast(static_cast(tampon[1])) & 0x80) >0; if(masque) { taille += 4; } if(fin and tampon.size()=taille; } static WS decode_ws(const std::string& requete) { if(requete.size()<2) return WS(); WS ws; ws._fin = (static_cast(requete[0]) & 0x80)>0; ws._reserve1 = (static_cast(requete[0]) & 0x40) > 0; ws._reserve2 = (static_cast(requete[0]) & 0x20) > 0; ws._reserve3 = (static_cast(requete[0]) & 0x10) > 0; ws._opcode = (static_cast(requete[0]) & 0x0F); bool masque = (static_cast(requete[1]) & 0x80) > 0; uint64_t taille = (static_cast(requete[1]) & 0x7F); size_t entetes = 2; if(taille==126) { if(requete.size()<4) { return WS(); } entetes = 4; taille = (static_cast(static_cast(requete[2])) << 8) + static_cast(static_cast(requete[3])); } else if(taille==127) { if(requete.size()<10) { return WS(); } entetes = 10; taille = (static_cast(static_cast(requete[2])) << 56) + (static_cast(static_cast(requete[3])) << 48) + (static_cast(static_cast(requete[4])) << 40) + (static_cast(static_cast(requete[5])) << 32) + (static_cast(static_cast(requete[6])) << 24) + (static_cast(static_cast(requete[7])) << 16) + (static_cast(static_cast(requete[8])) << 8) + static_cast(static_cast(requete[9])); } if(masque) { if(requete.size()>8); tampon += (unsigned char)(taille & 0xFF); } else { second |= 127; tampon += second; tampon += (unsigned char)(taille>>56); tampon += (unsigned char)(taille>>48 & 0xFF); tampon += (unsigned char)(taille>>40 & 0xFF); tampon += (unsigned char)(taille>>32 & 0xFF); tampon += (unsigned char)(taille>>24 & 0xFF); tampon += (unsigned char)(taille>>16 & 0xFF); tampon += (unsigned char)(taille>>8 & 0xFF); tampon += (unsigned char)(taille & 0xFF); } std::string message = ws._message; if(not ws._masque.empty()) { if(ws._masque.size()!=4) { return std::string(); } tampon += ws._masque; std::string clef(ws._masque); for(size_t i=0 ; i friend Flux& operator<<(Flux& f, const WS& w) { f << "WS " << (w._valide?"":"in") << "valid" << std::endl << "End: " << w._fin << std::endl << "Reserved1: " << w._reserve1 << std::endl << "Reserved2: " << w._reserve2 << std::endl << "Reserved3: " << w._reserve3 << std::endl << "OpCode: "; switch(w._opcode) { case CONTINUATION: f << "CONTINUATION"; break; case TEXTE: f << "TEXT"; break; case BINAIRE: f << "BINARY"; break; case FIN: f << "END"; break; case PING: f << "PING"; break; case PONG: f << "PONG"; break; default: f << "invalid"; break; } f << std::endl << "Size: " << w._message.size() << std::endl << "Mask: " << WS::encode_hex(w._masque) << std::endl << "Payload: " << ((w._opcode==TEXTE)?w._message:WS::encode_hex(w._message)) << std::endl; return f; } operator bool () const { return _valide; } bool _valide; bool _fin; bool _reserve1; bool _reserve2; bool _reserve3; unsigned char _opcode; std::string _masque; std::string _message; static bool opcode_valide(const int opcode) { switch(opcode) { case OpCode::CONTINUATION: case OpCode::TEXTE: case OpCode::BINAIRE: case OpCode::FIN: case OpCode::PING: case OpCode::PONG: return true; default: return false; } } static std::string encode_hex(const std::string& source) { std::string resultat; for(const auto& c: source) { unsigned char cc=c; unsigned char hc = cc >> 4; unsigned char lc = cc & 0xF; resultat += WS::caractere_hex(hc); resultat += WS::caractere_hex(lc); } return resultat; } static unsigned char caractere_hex(unsigned char c) { if(c>=10) return 'a'+c-10; return '0'+c; } }; %} USE TYPE com.device help: %{ This plugin is conceived to work with the official plugin com. %} TYPE http.mesg_1_1 help: %{ This type is mandatory to perform a WebSocket handshake. %} DEFINE FUNCTION ws.protocol_frame STR BLN -> INT ? %{ SVM_String tampon_brut = ARGV_VALUE(0,string); std::string tampon(tampon_brut.string,tampon_brut.size); SVM_Boolean fin = ARGV_VALUE(1,boolean); uint64_t taille; bool evacuation; bool trouve = WS::requete_ws_complete(tampon,fin==TRUE,taille,evacuation); if(trouve) { ssize_t t = taille; if(evacuation) t *= -1; return NEW_VALUE(integer,t); } return NEW_NULL_VALUE(integer); %} help: %{ Implement the sw.frame protocol for official plugin com. %} TYPE ws.frame %{ explicit type_frame(const WS& ws) :_ws(ws) {} WS _ws; %} delete default: %{} copy default: %{} print object: %{ std::string texte = WS::encode_ws(object->_ws); return NEW_STRING(texte); %} help: %{ Contain a WebSocket frame. .P This type supports copy and string conversion. %} INTERRUPTION ws.bad_frame help: "Interruption raised when a string contains an ill-formed WebSocket frame" INSTRUCTION ws.decode STR -> ws.frame %{ SVM_String texte = ARGV_VALUE(0,string); type_frame *trame = new type_frame(WS::decode_ws(std::string(texte.string,texte.size))); if(not trame->_ws._valide) { delete trame; ERROR_EXTERNAL(ws,bad_frame,"Invalid WebSocket frame"); } return NEW_PLUGIN(ws,frame,trame); %} help: %{ Decode a WebSocket frame into a ws.frame object. The input string can be extracted from a WebSocket flow. .P When the string is not a WebSocket frame, the interruption !ws.bad_frame is raised. %} INSTRUCTION ws.encode ws.frame -> STR %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); if(not trame->_ws._valide) { ERROR_EXTERNAL(ws,bad_frame,"Invalid WebSocket frame"); } std::string texte = WS::encode_ws(trame->_ws); return ::svm_value_string_new__buffer(svm,texte.c_str(),texte.size()); %} help: %{ Encode a WebSocket frame into a string. This string can be used for WebSocket flows. %} INSTRUCTION ws.new -> ws.frame %{ type_frame *trame = new type_frame(WS(true,WS::OpCode::TEXTE)); return NEW_PLUGIN(ws,frame,trame); %} help: %{ Create an empty WebSocket frame. .P The end flag is set and the operational code is set to TEXT. %} INSTRUCTION ws.get_fin ws.frame -> BLN %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); return NEW_VALUE(boolean,trame->_ws._fin?TRUE:FALSE); %} help: %{ Return the end flag on a WebSocket frame. %} INSTRUCTION ws.set_fin MUTABLE ws.frame BLN %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); SVM_Boolean fin = ARGV_VALUE(1,boolean); trame->_ws._fin = fin==TRUE; %} help: %{ Change the end flag on a WebSocket frame. %} INSTRUCTION ws.get_rsv ws.frame INT -> BLN %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); auto indice = ARGV_VALUE(1,integer); try { bool valeur = trame->_ws.reservation(indice); return NEW_VALUE(boolean,valeur?TRUE:FALSE); } catch(...) { ERROR_INTERNAL(FAILURE,"Invalid index"); throw; } %} help: %{ Return the status of a reserved flag on a WebSocket frame. .P The interruption FAILURE will be raised if the index is not 1, 2 or 3. %} INSTRUCTION ws.set_rsv MUTABLE ws.frame INT BLN %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); auto indice = ARGV_VALUE(1,integer); auto valeur = ARGV_VALUE(2,boolean); try { bool& v = trame->_ws.reservation(indice); v=valeur==TRUE; } catch(...) { ERROR_INTERNAL(FAILURE,"Invalid index"); throw; } %} help: %{ Change the status of a reserved flag on a WebSocket frame. .P The interruption FAILURE will be raised if the index is not 1, 2 or 3. %} INTERRUPTION ws.bad_opcode help: "Interruption raised when an invalid operational code is encountered." INSTRUCTION ws.get_opcode ws.frame -> INT %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); return NEW_VALUE(integer,trame->_ws._opcode); %} help: %{ Return the operational code on a WebSocket frame. %} INSTRUCTION ws.set_opcode MUTABLE ws.frame [ INT 'CONTINUE' 'TEXT' 'BINARY' 'END' 'PING' 'PONG' ] %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); int opcode = -1; if(::svm_parameter_type_is_keyword(svm,argv[1])) { std::string texte = ARGV_KEYWORD(1); if(texte=="CONTINUE") opcode = 0; if(texte=="TEXT") opcode = 1; if(texte=="BINARY") opcode = 2; if(texte=="END") opcode = 8; if(texte=="PING") opcode = 9; if(texte=="PONG") opcode = 10; } else { opcode = ARGV_VALUE(1,integer); } if(not WS::opcode_valide(opcode)) { ERROR_EXTERNAL(ws,bad_opcode,"Invalid operational code"); } trame->_ws._opcode = opcode; %} help: %{ Return the operational code on a WebSocket frame. .P An interruption !ws.bad_opcode will be raised if the provided operational code does not exist. %} INSTRUCTION ws.get_mask ws.frame -> STR ? %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); if(trame->_ws._masque.empty()) { return NEW_NULL_VALUE(string); } return ::svm_value_string_new__buffer(svm,trame->_ws._masque.c_str(),trame->_ws._masque.size()); %} help: %{ Return the mask on a WebSocket frame. .P When the mask is not defined, a null string is returned. %} INSTRUCTION ws.set_mask MUTABLE ws.frame STR ? %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); if(argc>1) { SVM_String masque = ARGV_VALUE(1,string); if(masque.size != 4) { ERROR_INTERNAL(FAILURE,"Invalid mask size"); } trame->_ws._masque = std::string(masque.string,masque.size); } else { trame->_ws._masque = ""; } %} help: %{ Change the mask on a WebSocket frame. When the value is not specified, the mask is removed. .P An interruption FAILURE is raised if the mask size is not 4. %} INSTRUCTION ws.get_payload ws.frame -> STR %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); return ::svm_value_string_new__buffer(svm,trame->_ws._message.c_str(),trame->_ws._message.size()); %} help: %{ Return the payload on a WebSocket frame. %} INSTRUCTION ws.set_payload MUTABLE ws.frame STR %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); SVM_String contenu = ARGV_VALUE(1,string); trame->_ws._message = std::string(contenu.string,contenu.size); %} help: %{ Change the payload on a WebSocket frame. %} INSTRUCTION ws.explain ws.frame -> STR %{ type_frame *trame = ARGV_PLUGIN(0,ws,frame); std::ostringstream oss; oss << trame->_ws ; return ::svm_value_string_new__buffer(svm,oss.str().c_str(),oss.str().size()); %} help: %{ Return a textual dump on a WebSocket frame. This string is not suitable for WebSocket flows. %}