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

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.3     ! moko       18: volatile const char * IDENT_WEBSOCKET_C="$Id: websocket.C,v 1.2 2026/10/16 15:16:21 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: 
1.3     ! moko       56: /// the timer is stopped while waiting for the message (not response time) and then started from scratch
        !            57: class Temp_disable_response_timeout {
        !            58:        Request& r;
        !            59: public:
        !            60:        Temp_disable_response_timeout(Request& ar) : r(ar) { SAPI::set_response_timeout(r.sapi_info, 0); }
        !            61:        ~Temp_disable_response_timeout() { SAPI::set_response_timeout(r.sapi_info, r.response.timeout); }
        !            62: };
        !            63: 
1.1       moko       64: // text goes in UTF-8 and the string is in the source charset, tainted data is untainted as in the response
                     65: static String::C to_utf8(Request& r, const String& string) {
                     66:        String::Body body=string.cstr_to_string_body_untaint(r.response.lang, r.connection(false), &r.charsets);
                     67:        return Charset::transcode(String::C(body.cstr(), body.length()), r.charsets.source(), pa_UTF8_charset);
                     68: }
                     69: 
                     70: static void _accept(Request& r, MethodParams&) {
                     71:        WebSocket& websocket=get_websocket(r);
                     72:        if(websocket.accepted)
                     73:                throw Exception("websocket", 0, "connection is accepted already");
                     74:        if(r.response.started)
                     75:                throw Exception("websocket", 0, "unable to accept: headers are already sent");
                     76: 
                     77:        const char* accept_key=pa_websocket_accept_key(
                     78:                r.request_info.method,
                     79:                SAPI::Env::get(r.sapi_info, "HTTP_UPGRADE"),
                     80:                SAPI::Env::get(r.sapi_info, "HTTP_CONNECTION"),
                     81:                SAPI::Env::get(r.sapi_info, "HTTP_SEC_WEBSOCKET_KEY"),
                     82:                SAPI::Env::get(r.sapi_info, "HTTP_SEC_WEBSOCKET_VERSION"));
                     83: 
                     84:        // $response:fields, such as sec-websocket-protocol, are sent too
                     85:        HashStringValue& fields=r.response.fields();
                     86:        fields.put(status_name_upper, new VInt(101));
                     87:        fields.put(upgrade_name_upper, new VString(*new String("websocket")));
                     88:        fields.put(connection_name_upper, new VString(*new String("Upgrade")));
                     89:        fields.put(accept_name_upper, new VString(*new String(accept_key)));
                     90:        r.start(VResponse::WEBSOCKET);
                     91: 
1.2       moko       92:        websocket.start(pa_post_max_size);
1.1       moko       93: }
                     94: 
                     95: static void _receive(Request& r, MethodParams& params) {
                     96:        WebSocket& websocket=get_accepted_websocket(r);
                     97: 
                     98:        double timeout=params.as_double(0, "timeout must be expression", r);
                     99:        if(timeout<0)
                    100:                throw Exception(PARSER_RUNTIME, 0, "timeout must not be negative");
                    101: 
                    102:        WebSocket::Message message;
1.3     ! moko      103:        bool received;
        !           104:        {
        !           105:                Temp_disable_response_timeout temp(r);
        !           106:                received=websocket.receive(timeout*1000 < INT_MAX ? (int)(timeout*1000) : INT_MAX, message);
        !           107:        }
        !           108:        if(!received) {
1.1       moko      109:                r.write(*VVoid::get());
                    110:                return;
                    111:        }
                    112: 
                    113:        // the data is copied as the buffer is reused
                    114:        const char* data=pa_strdup(message.data, message.size);
                    115: 
                    116:        VHash& result=*new VHash;
                    117:        HashStringValue& fields=result.hash();
                    118: 
                    119:        if(message.opcode==WebSocket::BINARY) {
                    120:                fields.put("type", new VString(*new String("binary")));
                    121:                VFile& file=*new VFile;
                    122:                file.set_binary(true/*tainted*/, data, message.size);
                    123:                fields.put("data", &file);
                    124:        } else {
                    125:                String::C text=Charset::transcode(String::C(data, message.size), pa_UTF8_charset, r.charsets.source());
                    126:                fields.put("type", new VString(*new String(message.opcode==WebSocket::TEXT ? "text" : "close")));
                    127:                fields.put("data", new VString(*new String(text, String::L_TAINTED)));
                    128:                if(message.opcode==WebSocket::CLOSE)
                    129:                        fields.put("code", new VInt(message.code));
                    130:        }
                    131: 
                    132:        r.write(result);
                    133: }
                    134: 
                    135: static void _send(Request& r, MethodParams& params) {
                    136:        WebSocket& websocket=get_accepted_websocket(r);
                    137: 
                    138:        if(VFile* file=dynamic_cast<VFile*>(&params[0])) {
                    139:                websocket.send(WebSocket::BINARY, file->value_ptr(), file->value_size());
                    140:        } else {
                    141:                String::C text=to_utf8(r, params.as_string(0, "message must be string or file"));
                    142:                websocket.send(WebSocket::TEXT, text.str, text.length);
                    143:        }
                    144: }
                    145: 
                    146: static void _ping(Request& r, MethodParams& params) {
                    147:        WebSocket& websocket=get_accepted_websocket(r);
                    148: 
                    149:        String::C data;
                    150:        if(params.count())
                    151:                data=to_utf8(r, params.as_string(0, "data must be string"));
                    152:        if(data.length>MAX_CONTROL_SIZE)
                    153:                throw Exception(PARSER_RUNTIME, 0, "data is too long, it must be up to %d bytes", MAX_CONTROL_SIZE);
                    154:        websocket.send(WebSocket::PING, data.str, data.length);
                    155: }
                    156: 
                    157: static void _close(Request& r, MethodParams& params) {
                    158:        WebSocket& websocket=get_accepted_websocket(r);
                    159: 
                    160:        int code=params.count()>0 ? params.as_int(0, "code must be int", r) : WebSocket::NORMAL;
                    161:        if(!WebSocket::valid_status(code))
                    162:                throw Exception(PARSER_RUNTIME, 0, "code(%d) is not valid", code);
                    163: 
                    164:        String::C reason;
                    165:        if(params.count()>1)
                    166:                reason=to_utf8(r, params.as_string(1, "reason must be string"));
                    167:        if(reason.length>MAX_REASON_SIZE)
                    168:                throw Exception(PARSER_RUNTIME, 0, "reason is too long, it must be up to %d bytes", MAX_REASON_SIZE);
                    169: 
                    170:        websocket.close(code, reason.str ? reason.str : "", reason.length);
                    171: }
                    172: 
                    173: // constructor
                    174: 
                    175: MWebsocket::MWebsocket(): Methoded("websocket") {
                    176:        // ^accept[]
                    177:        add_native_method("accept", Method::CT_STATIC, _accept, 0, 0);
                    178: 
                    179:        // ^receive(timeout in seconds), returns message hash
                    180:        add_native_method("receive", Method::CT_STATIC, _receive, 1, 1);
                    181: 
                    182:        // ^send[string or file]
                    183:        add_native_method("send", Method::CT_STATIC, _send, 1, 1);
                    184: 
                    185:        // ^ping[] or ^ping[data]
                    186:        add_native_method("ping", Method::CT_STATIC, _ping, 0, 1);
                    187: 
                    188:        // ^close[] or ^close(code) or ^close(code;reason)
                    189:        add_native_method("close", Method::CT_STATIC, _close, 0, 2);
                    190: }

E-mail: