@@ -32,12 +32,14 @@ struct Client {
3232/// ```moonbit no - check
3333/// let ws = Client::connect("example.com", "/ws")
3434/// ```
35+ ///
3536pub async fn Client ::connect (
3637 host : String ,
3738 path : String ,
3839 port ? : Int = 80 ,
3940 headers ? : Map [String , String ] = {},
4041) -> Client {
42+ // Ref : https://datatracker.ietf.org/doc/html/rfc6455#section-4.1
4143 let seed = FixedArray ::make (32 , b '\x00 ' )
4244 if @tls .rand_bytes (seed , 32 ) != 1 {
4345 fail ("Failed to get random bytes for WebSocket client" )
@@ -73,21 +75,61 @@ pub async fn Client::connect(
7375 conn .write ("\r\n " )
7476
7577 // Read and validate handshake response
76- let response_line = conn .read_exactly (1024 ) // Read initial response
77- let response_str = @encoding/utf8 .decode (response_line )
78- guard response_str .contains ("101" ) &&
79- response_str .contains ("Switching Protocols" ) else {
78+ let reader = @io .BufferedReader ::new (conn )
79+ guard reader .read_line () is Some (response_line ) else {
80+ conn .close ()
81+ raise InvalidHandshake ("Server closed connection during handshake" )
82+ }
83+ guard response_line .contains ("101" ) &&
84+ response_line .contains ("Switching Protocols" ) else {
8085 conn .close ()
8186 raise InvalidHandshake (
82- "Server did not respond with 101 Switching Protocols" ,
87+ "Server did not respond with 101 Switching Protocols: \{ response_line } " ,
8388 )
8489 }
90+ let headers : Map [String , String ] = {}
91+ while reader .read_line () is Some (line ) {
92+ if line .is_blank () {
93+ break
94+ }
95+ let line = line [:- 1 ] // Remove trailing \r
8596
86- // Basic validation that the response looks like a proper WebSocket upgrade
87- guard response_str .contains ("websocket" ) else {
88- conn .close ()
97+ // Parse header line
98+ if line .contains (":" ) {
99+ let parts = line .split (":" ).to_array ()
100+ if parts .length () >= 2 {
101+ let key = parts [0 ].trim (chars = " \t " ).to_string ().to_lower ()
102+ // Join remaining parts in case the value contains colons
103+ let value_parts = parts [1 :]
104+ let value = if value_parts .length () == 1 {
105+ value_parts [0 ].trim (chars = " \t " ).to_string ()
106+ } else {
107+ value_parts .join (":" ).trim (chars = " \t " ).to_string ()
108+ }
109+ // Handle multi-value headers by taking the first value
110+ if not (headers .contains (key )) {
111+ headers [key ] = value
112+ }
113+ }
114+ }
115+ }
116+
117+ // Validate WebSocket handshake headers
118+ guard headers .get ("upgrade" ) is Some (upgrade ) &&
119+ upgrade .to_lower () == "websocket" else {
120+ raise InvalidHandshake ("Missing or invalid Upgrade header" )
121+ }
122+ guard headers .get ("connection" ) is Some (connection ) &&
123+ connection .to_lower ().contains ("upgrade" ) else {
124+ raise InvalidHandshake ("Missing or invalid Connection header" )
125+ }
126+ guard headers .get ("sec-websocket-accept" ) is Some (accept_key ) else {
127+ raise InvalidHandshake ("Missing Sec-WebSocket-Accept header" )
128+ }
129+ let expected_accept_key = generate_accept_key (key )
130+ guard accept_key .trim (chars = " \t\r " ) == expected_accept_key else {
89131 raise InvalidHandshake (
90- "Server response does not contain websocket upgrade confirmation " ,
132+ "Invalid Sec-WebSocket-Accept value: \{ accept_key } != \{ expected_accept_key } " ,
91133 )
92134 }
93135 { conn , closed : None , rand }
@@ -102,6 +144,30 @@ pub fn Client::close(self : Client) -> Unit {
102144 }
103145}
104146
147+ ///|
148+ pub async fn Client ::send_close (
149+ self : Client ,
150+ code ? : CloseCode = Normal ,
151+ reason ? : BytesView = "" ,
152+ ) -> Unit {
153+ if self .closed is Some (_ ) {
154+ return
155+ }
156+ let mut payload = FixedArray ::make (0 , b '\x00 ' )
157+ let code_int = code .to_int ()
158+ payload = FixedArray ::make (2 + reason .length (), b '\x00 ' )
159+ payload .unsafe_write_uint16_be (0 , code_int .to_uint16 ())
160+ payload .blit_from_bytesview (2 , reason )
161+ write_frame (
162+ self .conn,
163+ true ,
164+ OpCode ::Close ,
165+ payload .unsafe_reinterpret_as_bytes (),
166+ self .rand.int ().to_be_bytes (),
167+ )
168+ self .closed = Some (code )
169+ }
170+
105171///|
106172/// Send a text message
107173pub async fn Client ::send_text (self : Client , text : String ) -> Unit {
@@ -190,15 +256,6 @@ pub async fn Client::receive(self : Client) -> Message {
190256 let code_int = (payload_arr [0 ].to_int () << 8 ) |
191257 payload_arr [1 ].to_int ()
192258 close_code = CloseCode ::from_int (code_int ).unwrap_or (Normal )
193- // Reason is parsed but not used in client close handling
194- // let mut reason = ""
195- // if frame.payload.length() > 2 {
196- // let reason_bytes = FixedArray::make(frame.payload.length() - 2, b'\x00')
197- // for i = 2; i < frame.payload.length(); i = i + 1 {
198- // reason_bytes[i - 2] = payload_arr[i]
199- // }
200- // reason = @encoding/utf8.decode_lossy(reason_bytes.unsafe_reinterpret_as_bytes())
201- // }
202259 }
203260 self .closed = Some (close_code )
204261 raise ConnectionClosed (close_code )
0 commit comments