Annotation of parser3/src/classes/websocket.C, revision 1.2

1.1       moko        1: /** @file
                      2:        Parser: @b websocket parser class.
                      3: 
                      4:        Copyright (c) 2001-2026 Art. Lebedev Studio (https://www.artlebedev.com)
                      5:        Authors: Konstantin Morshnev <moko@design.ru>
                      6: */
                      7: 
                      8: #include "pa_vmethod_frame.h"
                      9: #include "pa_request.h"
                     10: #include "pa_vhash.h"
                     11: #include "pa_vint.h"
                     12: #include "pa_vvoid.h"
                     13: #include "pa_vfile.h"
                     14: #include "pa_vresponse.h"
                     15: #include "pa_charsets.h"
                     16: #include "pa_websocket.h"
                     17: 
1.2     ! moko       18: volatile const char * IDENT_WEBSOCKET_C="$Id: websocket.C,v 1.1 2026/10/16 13:55:16 moko Exp $" IDENT_PA_WEBSOCKET_H;
1.1       moko       19: 
                     20: // class
                     21: 
                     22: class MWebsocket: public Methoded {
                     23: public:
                     24:        MWebsocket();
                     25: };
                     26: 
                     27: // global variable
                     28: 
                     29: DECLARE_CLASS_VAR(websocket, new MWebsocket);
                     30: 
                     31: // defines
                     32: 
                     33: #define MAX_CONTROL_SIZE 125
                     34: #define MAX_REASON_SIZE (MAX_CONTROL_SIZE-2)
                     35: 
                     36: static const String::Body status_name_upper("STATUS");
                     37: static const String::Body upgrade_name_upper("UPGRADE");
                     38: static const String::Body connection_name_upper("CONNECTION");
                     39: static const String::Body accept_name_upper("SEC-WEBSOCKET-ACCEPT");
                     40: 
                     41: // methods
                     42: 
                     43: static WebSocket& get_websocket(Request& r) {
                     44:        if(!r.websocket)
                     45:                throw Exception("websocket", 0, "websocket is available in httpd mode only");
                     46:        return *r.websocket;
                     47: }
                     48: 
                     49: static WebSocket& get_accepted_websocket(Request& r) {
                     50:        WebSocket& result=get_websocket(r);
                     51:        if(!result.accepted)
                     52:                throw Exception("websocket", 0, "connection is not accepted, use ^websocket:accept[] first");
                     53:        return result;
                     54: }
                     55: 
                     56: // text goes in UTF-8 and the string is in the source charset, tainted data is untainted as in the response
                     57: static String::C to_utf8(Request& r, const String& string) {
                     58:        String::Body body=string.cstr_to_string_body_untaint(r.response.lang, r.connection(false), &r.charsets);
                     59:        return Charset::transcode(String::C(body.cstr(), body.length()), r.charsets.source(), pa_UTF8_charset);
                     60: }
                     61: 
                     62: static void _accept(Request& r, MethodParams&) {
                     63:        WebSocket& websocket=get_websocket(r);
                     64:        if(websocket.accepted)
                     65:                throw Exception("websocket", 0, "connection is accepted already");
                     66:        if(r.response.started)
                     67:                throw Exception("websocket", 0, "unable to accept: headers are already sent");
                     68: 
                     69:        const char* accept_key=pa_websocket_accept_key(
                     70:                r.request_info.method,
                     71:                SAPI::Env::get(r.sapi_info, "HTTP_UPGRADE"),
                     72:                SAPI::Env::get(r.sapi_info, "HTTP_CONNECTION"),
                     73:                SAPI::Env::get(r.sapi_info, "HTTP_SEC_WEBSOCKET_KEY"),
                     74:                SAPI::Env::get(r.sapi_info, "HTTP_SEC_WEBSOCKET_VERSION"));
                     75: 
                     76:        // $response:fields, such as sec-websocket-protocol, are sent too
                     77:        HashStringValue& fields=r.response.fields();
                     78:        fields.put(status_name_upper, new VInt(101));
                     79:        fields.put(upgrade_name_upper, new VString(*new String("websocket")));
                     80:        fields.put(connection_name_upper, new VString(*new String("Upgrade")));
                     81:        fields.put(accept_name_upper, new VString(*new String(accept_key)));
                     82:        r.start(VResponse::WEBSOCKET);
                     83: 
1.2     ! moko       84:        websocket.start(pa_post_max_size);
1.1       moko       85: }
                     86: 
                     87: static void _receive(Request& r, MethodParams& params) {
                     88:        WebSocket& websocket=get_accepted_websocket(r);
                     89: 
                     90:        double timeout=params.as_double(0, "timeout must be expression", r);
                     91:        if(timeout<0)
                     92:                throw Exception(PARSER_RUNTIME, 0, "timeout must not be negative");
                     93: 
                     94:        WebSocket::Message message;
                     95:        if(!websocket.receive(timeout*1000 < INT_MAX ? (int)(timeout*1000) : INT_MAX, message)) {
                     96:                r.write(*VVoid::get());
                     97:                return;
                     98:        }
                     99: 
                    100:        // the data is copied as the buffer is reused
                    101:        const char* data=pa_strdup(message.data, message.size);
                    102: 
                    103:        VHash& result=*new VHash;
                    104:        HashStringValue& fields=result.hash();
                    105: 
                    106:        if(message.opcode==WebSocket::BINARY) {
                    107:                fields.put("type", new VString(*new String("binary")));
                    108:                VFile& file=*new VFile;
                    109:                file.set_binary(true/*tainted*/, data, message.size);
                    110:                fields.put("data", &file);
                    111:        } else {
                    112:                String::C text=Charset::transcode(String::C(data, message.size), pa_UTF8_charset, r.charsets.source());
                    113:                fields.put("type", new VString(*new String(message.opcode==WebSocket::TEXT ? "text" : "close")));
                    114:                fields.put("data", new VString(*new String(text, String::L_TAINTED)));
                    115:                if(message.opcode==WebSocket::CLOSE)
                    116:                        fields.put("code", new VInt(message.code));
                    117:        }
                    118: 
                    119:        r.write(result);
                    120: }
                    121: 
                    122: static void _send(Request& r, MethodParams& params) {
                    123:        WebSocket& websocket=get_accepted_websocket(r);
                    124: 
                    125:        if(VFile* file=dynamic_cast<VFile*>(&params[0])) {
                    126:                websocket.send(WebSocket::BINARY, file->value_ptr(), file->value_size());
                    127:        } else {
                    128:                String::C text=to_utf8(r, params.as_string(0, "message must be string or file"));
                    129:                websocket.send(WebSocket::TEXT, text.str, text.length);
                    130:        }
                    131: }
                    132: 
                    133: static void _ping(Request& r, MethodParams& params) {
                    134:        WebSocket& websocket=get_accepted_websocket(r);
                    135: 
                    136:        String::C data;
                    137:        if(params.count())
                    138:                data=to_utf8(r, params.as_string(0, "data must be string"));
                    139:        if(data.length>MAX_CONTROL_SIZE)
                    140:                throw Exception(PARSER_RUNTIME, 0, "data is too long, it must be up to %d bytes", MAX_CONTROL_SIZE);
                    141:        websocket.send(WebSocket::PING, data.str, data.length);
                    142: }
                    143: 
                    144: static void _close(Request& r, MethodParams& params) {
                    145:        WebSocket& websocket=get_accepted_websocket(r);
                    146: 
                    147:        int code=params.count()>0 ? params.as_int(0, "code must be int", r) : WebSocket::NORMAL;
                    148:        if(!WebSocket::valid_status(code))
                    149:                throw Exception(PARSER_RUNTIME, 0, "code(%d) is not valid", code);
                    150: 
                    151:        String::C reason;
                    152:        if(params.count()>1)
                    153:                reason=to_utf8(r, params.as_string(1, "reason must be string"));
                    154:        if(reason.length>MAX_REASON_SIZE)
                    155:                throw Exception(PARSER_RUNTIME, 0, "reason is too long, it must be up to %d bytes", MAX_REASON_SIZE);
                    156: 
                    157:        websocket.close(code, reason.str ? reason.str : "", reason.length);
                    158: }
                    159: 
                    160: // constructor
                    161: 
                    162: MWebsocket::MWebsocket(): Methoded("websocket") {
                    163:        // ^accept[]
                    164:        add_native_method("accept", Method::CT_STATIC, _accept, 0, 0);
                    165: 
                    166:        // ^receive(timeout in seconds), returns message hash
                    167:        add_native_method("receive", Method::CT_STATIC, _receive, 1, 1);
                    168: 
                    169:        // ^send[string or file]
                    170:        add_native_method("send", Method::CT_STATIC, _send, 1, 1);
                    171: 
                    172:        // ^ping[] or ^ping[data]
                    173:        add_native_method("ping", Method::CT_STATIC, _ping, 0, 1);
                    174: 
                    175:        // ^close[] or ^close(code) or ^close(code;reason)
                    176:        add_native_method("close", Method::CT_STATIC, _close, 0, 2);
                    177: }

E-mail: