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*>(¶ms[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: