├── .github └── workflows │ ├── autobahn.yml │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples └── ping.v ├── tests └── autobahn │ ├── README.md │ ├── autobahn_client.v │ ├── autobahn_server.v │ ├── docker-compose.yml │ ├── fuzzing_server │ ├── Dockerfile │ ├── check_results.py │ └── config │ │ ├── fuzzingclient.json │ │ └── fuzzingserver.json │ ├── local_run │ ├── Dockerfile │ └── README.md │ └── ws_test │ └── Dockerfile ├── v.mod └── websocket ├── err.v ├── events.v ├── handshake.v ├── io.v ├── message.v ├── ssl.v ├── uri.v ├── utils.v ├── websocket_client.v ├── websocket_client_test.v ├── websocket_nix.c.v ├── websocket_server.v └── websocket_windows.c.v /.github/workflows/autobahn.yml: -------------------------------------------------------------------------------- 1 | name: Autobahn test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Autobahn integrations tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | 11 | # # Following are for running locally with https://github.com/nektos/act 12 | # - name: Down autobahn services 13 | # run: docker-compose -f ${{github.workspace}}/tests/autobahn/docker-compose.yml down 14 | # - name: Build autobahn services 15 | # run: docker-compose -f ${{github.workspace}}/tests/autobahn/docker-compose.yml build 16 | 17 | - name: Run autobahn services 18 | run: docker-compose -f ${{github.workspace}}/tests/autobahn/docker-compose.yml up -d 19 | - name: Build client test 20 | run: docker exec autobahn_client "v" "/src/tests/autobahn/autobahn_client.v" 21 | - name: Run client test 22 | run: docker exec autobahn_client "/src/tests/autobahn/autobahn_client" 23 | - name: Run server test 24 | run: docker exec autobahn_server "wstest" "-m" "fuzzingclient" "-s" "/config/fuzzingclient.json" 25 | - name: Copy reports 26 | run: docker cp autobahn_server:/reports ${{github.workspace}}/reports 27 | - name: Test success 28 | run: docker exec autobahn_server "python" "/check_results.py" 29 | 30 | - name: Publish all reports 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: full report 34 | path: ${{github.workspace}}/reports 35 | - name: Publish report client 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: client 39 | path: ${{github.workspace}}/reports/clients/index.html 40 | - name: Publish report server 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: server 44 | path: ${{github.workspace}}/reports/servers/index.html 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: CI 6 | runs-on: ubuntu-latest 7 | container: 8 | image: thevlang/vlang:buster-dev 9 | volumes: 10 | - ${{github.workspace}}:/src 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Install dependencies 16 | run: | 17 | v install emily33901.net 18 | - name: Tests 19 | run: | 20 | v test . 21 | - name: Build 22 | run: | 23 | v examples/ping.v 24 | # - name: Test V fixed tests 25 | # run: | 26 | # v test-fixed -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdb 2 | *.exe 3 | *.ilc 4 | *.ilk 5 | .vs/ 6 | *.dll 7 | .vscode/ 8 | autobahn_client 9 | autobahn_server 10 | ping -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tomas Hellström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS IS NOT MAINTAINED 2 | This implementation is now a part of V and this means there are no reason to maintain this version. I keep it for future references. 3 | 4 | # New V-websocket library 5 | 6 | This is a refactor of the current web socket library to comply with V-like style of programming. It is built on top of Emily net library and passes all the autobahn tests for clients. 7 | 8 | ## Changes from original websocket implementation 9 | 10 | - Use only built-in datatypes, no voidptrs or byteptr (for now the eventbus require it for callbacks) 11 | - Relying on V automatic free of resources (not done yet) 12 | - Client and Server pass autobahn tests excluding compression 13 | - Use new socket implementation from @Ememily33901 that will soon be standard in V 14 | - Use Option error handling 15 | - Refactor code so websocket.v contains only the code to understand basic implementations 16 | - moved communication to io.v 17 | - separation of handling frames and finished messages 18 | - Eventbus dependency removed and using own fn types. Now register callbacks with on_message, on_error, on_open, on_close functions 19 | - Easy to use webbsocket server 20 | 21 | ## Proposed / planned changes 22 | 23 | * [x] Make server autobahn compliant like client 24 | * [x] Set own timeouts 25 | * [x] Make sure it works on windows 26 | * [ ] Strict comply to utf8 autobahn fast fail rule (it comply now but non strict) 27 | * [ ] Publish as module 28 | * [ ] Set if autoping in server = false if time is 0 29 | 30 | ## Future changes after v0.2 of V 31 | * [ ] Generics, use vweb type of app instead of voidptr for reference 32 | * [ ] Interfaces, IO operations as interfaces for making tests more easy 33 | 34 | ## Remarks 35 | 36 | - It should not be used in production since memory management is not done yet in V 37 | 38 | ## Attribution 39 | - the original author of client @thecodrr 40 | - original code that was updated and moved to V 41 | https://github.com/thecodrr/vws 42 | -------------------------------------------------------------------------------- /examples/ping.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import time 4 | import os 5 | import websocket 6 | 7 | fn main() { 8 | go start_server() 9 | time.sleep_ms(100) 10 | go start_client() 11 | go start_client_disconnect() 12 | println('press enter to quit...') 13 | os.get_line() 14 | } 15 | 16 | fn start_server() ? { 17 | mut s := websocket.new_server(30000, '') 18 | // Make that in execution test time give time to execute at least one time 19 | s.ping_interval = 1 20 | s.on_connect(fn (mut s websocket.ServerClient) ?bool { 21 | // Here you can look att the client info and accept or not accept 22 | // just returning a true/false 23 | if s.resource_name != '/' { 24 | return false 25 | } 26 | return true 27 | })? 28 | s.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ? { 29 | payload := if msg.payload.len == 0 { '' } else { string(msg.payload) } 30 | println('client ($ws.id) got message: opcode: $msg.opcode, payload: $payload') 31 | ws.write(msg.payload, msg.opcode) or { 32 | panic(err) 33 | } 34 | }) 35 | s.on_close(fn (mut ws websocket.Client, code int, reason string) ? { 36 | // println('client ($ws.id) closed connection') 37 | }) 38 | s.listen() or { 39 | // println('error on server listen: $err') 40 | } 41 | } 42 | 43 | fn start_client() ? { 44 | mut ws := websocket.new_client('ws://localhost:30000')? 45 | // mut ws := websocket.new_client('wss://echo.websocket.org:443')? 46 | // use on_open_ref if you want to send any reference object 47 | ws.on_open(fn (mut ws websocket.Client) ? { 48 | println('open!') 49 | }) 50 | // use on_error_ref if you want to send any reference object 51 | ws.on_error(fn (mut ws websocket.Client, err string) ? { 52 | println('error: $err') 53 | }) 54 | // use on_close_ref if you want to send any reference object 55 | ws.on_close(fn (mut ws websocket.Client, code int, reason string) ? { 56 | println('closed') 57 | }) 58 | // use on_message_ref if you want to send any reference object 59 | ws.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ? { 60 | println('client got type: $msg.opcode payload:\n$msg.payload') 61 | }) 62 | // you can add any pointer reference to use in callback 63 | // t := TestRef{count: 10} 64 | // ws.on_message_ref(fn (mut ws websocket.Client, msg &websocket.Message, r &SomeRef)? { 65 | // // println('type: $msg.opcode payload:\n$msg.payload ref: $r') 66 | // }, &r) 67 | ws.connect() or { 68 | println('error on connect: $err') 69 | } 70 | go write_echo(mut ws) or { 71 | println('error on write_echo $err') 72 | } 73 | ws.listen() or { 74 | println('error on listen $err') 75 | } 76 | } 77 | 78 | fn start_client_disconnect() ? { 79 | mut ws := websocket.new_client('ws://localhost:30000')? 80 | // mut ws := websocket.new_client('wss://echo.websocket.org:443')? 81 | // use on_open_ref if you want to send any reference object 82 | ws.on_open(fn (mut ws websocket.Client) ? { 83 | println('open!') 84 | }) 85 | // use on_error_ref if you want to send any reference object 86 | ws.on_error(fn (mut ws websocket.Client, err string) ? { 87 | println('error: $err') 88 | }) 89 | // use on_close_ref if you want to send any reference object 90 | ws.on_close(fn (mut ws websocket.Client, code int, reason string) ? { 91 | println('closed') 92 | }) 93 | // use on_message_ref if you want to send any reference object 94 | ws.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ? { 95 | println('client got type: $msg.opcode payload:\n$msg.payload') 96 | ws.close(1000, 'close') 97 | }) 98 | // you can add any pointer reference to use in callback 99 | // t := TestRef{count: 10} 100 | // ws.on_message_ref(fn (mut ws websocket.Client, msg &websocket.Message, r &SomeRef)? { 101 | // // println('type: $msg.opcode payload:\n$msg.payload ref: $r') 102 | // }, &r) 103 | ws.connect() or { 104 | println('error on connect: $err') 105 | } 106 | go write_echo(mut ws) or { 107 | println('error on write_echo $err') 108 | } 109 | ws.listen() or { 110 | println('error on listen $err') 111 | } 112 | } 113 | 114 | fn write_echo(mut ws websocket.Client) ? { 115 | for i := 0; i <= 3; i++ { 116 | // Server will send pings every 30 seconds 117 | ws.write('echo this!'.bytes(), .text_frame) or { 118 | println('panicing writing $err') 119 | } 120 | time.sleep_ms(100) 121 | } 122 | /* 123 | ws.close(1000, "normal") or { 124 | println('panicing $err') 125 | } 126 | */ 127 | } 128 | -------------------------------------------------------------------------------- /tests/autobahn/README.md: -------------------------------------------------------------------------------- 1 | # Autobahn tests 2 | 3 | This is the autobahn automatic tests on build. The performance tests are skipped due to timeouts in Github actions. -------------------------------------------------------------------------------- /tests/autobahn/autobahn_client.v: -------------------------------------------------------------------------------- 1 | // use this test to test the websocket client in the autobahn test 2 | 3 | module main 4 | 5 | import websocket 6 | 7 | fn main() { 8 | for i in 1 ..304 { 9 | println('\ncase: $i') 10 | handle_case(i) or { 11 | println('error should be ok: $err') 12 | } 13 | } 14 | // update the reports 15 | uri := 'ws://autobahn_server:9001/updateReports?agent=v-client' 16 | mut ws := websocket.new_client(uri)? 17 | ws.connect()? 18 | ws.listen()? 19 | } 20 | 21 | fn handle_case(case_nr int) ? { 22 | uri := 'ws://autobahn_server:9001/runCase?case=$case_nr&agent=v-client' 23 | mut ws := websocket.new_client(uri)? 24 | ws.on_message(on_message) 25 | ws.connect()? 26 | ws.listen()? 27 | } 28 | 29 | fn on_message(mut ws websocket.Client, msg &websocket.Message)? { 30 | // autobahn tests expects to send same message back 31 | if msg.opcode == .pong { 32 | // We just wanna pass text and binary message back to autobahn 33 | return 34 | } 35 | ws.write(msg.payload, msg.opcode) or { 36 | panic(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/autobahn/autobahn_server.v: -------------------------------------------------------------------------------- 1 | // use this to test websocket server to the autobahn test 2 | 3 | module main 4 | 5 | import websocket 6 | 7 | fn main() { 8 | mut s := websocket.new_server(9002, '/') 9 | s.on_message(on_message) 10 | s.listen() 11 | } 12 | 13 | fn handle_case(case_nr int) ? { 14 | uri := 'ws://localhost:9002/runCase?case=$case_nr&agent=v-client' 15 | mut ws := websocket.new_client(uri)? 16 | ws.on_message(on_message) 17 | ws.connect()? 18 | ws.listen()? 19 | } 20 | 21 | fn on_message(mut ws websocket.Client, msg &websocket.Message)? { 22 | // autobahn tests expects to send same message back 23 | if msg.opcode == .pong { 24 | // We just wanna pass text and binary message back to autobahn 25 | return 26 | } 27 | ws.write(msg.payload, msg.opcode) or { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/autobahn/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | web: 4 | container_name: autobahn_server 5 | build: fuzzing_server 6 | 7 | ports: 8 | - "9001:9001" 9 | - "8080:8080" 10 | client: 11 | container_name: autobahn_client 12 | build: 13 | dockerfile: tests/autobahn/ws_test/Dockerfile 14 | context: ../../ 15 | # volumes: 16 | # - ../../:/src 17 | # redis: 18 | # container_name: redis-backend 19 | # image: "redis:alpine" -------------------------------------------------------------------------------- /tests/autobahn/fuzzing_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM crossbario/autobahn-testsuite 2 | COPY check_results.py /check_results.py 3 | RUN chmod +x /check_results.py 4 | 5 | COPY config/fuzzingserver.json /config/fuzzingserver.json 6 | COPY config/fuzzingclient.json /config/fuzzingclient.json 7 | -------------------------------------------------------------------------------- /tests/autobahn/fuzzing_server/check_results.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | nr_of_client_errs = 0 4 | nr_of_client_tests = 0 5 | 6 | nr_of_server_errs = 0 7 | nr_of_server_tests = 0 8 | 9 | with open("/reports/clients/index.json") as f: 10 | data = json.load(f) 11 | 12 | for i in data["v-client"]: 13 | # Count errors 14 | if ( 15 | data["v-client"][i]["behavior"] == "FAILED" 16 | or data["v-client"][i]["behaviorClose"] == "FAILED" 17 | ): 18 | nr_of_client_errs = nr_of_client_errs + 1 19 | 20 | nr_of_client_tests = nr_of_client_tests + 1 21 | 22 | with open("/reports/clients/index.json") as f: 23 | data = json.load(f) 24 | 25 | for i in data["v-client"]: 26 | # Count errors 27 | if ( 28 | data["v-client"][i]["behavior"] == "FAILED" 29 | or data["v-client"][i]["behaviorClose"] == "FAILED" 30 | ): 31 | nr_of_client_errs = nr_of_client_errs + 1 32 | 33 | nr_of_client_tests = nr_of_client_tests + 1 34 | 35 | with open("/reports/servers/index.json") as f: 36 | data = json.load(f) 37 | 38 | for i in data["AutobahnServer"]: 39 | if ( 40 | data["AutobahnServer"][i]["behavior"] == "FAILED" 41 | or data["AutobahnServer"][i]["behaviorClose"] == "FAILED" 42 | ): 43 | nr_of_server_errs = nr_of_server_errs + 1 44 | 45 | nr_of_server_tests = nr_of_server_tests + 1 46 | 47 | if nr_of_client_errs > 0 or nr_of_server_errs > 0: 48 | print( 49 | "FAILED AUTOBAHN TESTS, CLIENT ERRORS {0}(of {1}), SERVER ERRORS {2}(of {3})".format( 50 | nr_of_client_errs, nr_of_client_tests, nr_of_server_errs, nr_of_server_tests 51 | ) 52 | ) 53 | exit(1) 54 | 55 | print( 56 | "TEST SUCCESS!, CLIENT TESTS({0}), SERVER TESTS ({1})".format( 57 | nr_of_client_tests, nr_of_server_tests 58 | ) 59 | ) 60 | -------------------------------------------------------------------------------- /tests/autobahn/fuzzing_server/config/fuzzingclient.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "failByDrop": false 4 | }, 5 | "outdir": "./reports/servers", 6 | "servers": [ 7 | { 8 | "agent": "AutobahnServer", 9 | "url": "ws://autobahn_client:9002" 10 | } 11 | ], 12 | "cases": [ 13 | "*" 14 | ], 15 | "exclude-cases": [ 16 | "9.*", 17 | "11.*", 18 | "12.*", 19 | "13.*" 20 | ], 21 | "exclude-agent-cases": {} 22 | } -------------------------------------------------------------------------------- /tests/autobahn/fuzzing_server/config/fuzzingserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "ws://127.0.0.1:9001", 3 | "outdir": "./reports/clients", 4 | "cases": [ 5 | "*" 6 | ], 7 | "exclude-cases": [ 8 | "9.*", 9 | "11.*", 10 | "12.*", 11 | "13.*" 12 | ], 13 | "exclude-agent-cases": {} 14 | } -------------------------------------------------------------------------------- /tests/autobahn/local_run/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use this as docker builder with https://github.com/nektos/act 2 | # build with: docker build tests/autobahn/. -t myimage 3 | # use in act: act -P ubuntu-latest=myimage 4 | 5 | FROM node:12.6-buster-slim 6 | 7 | COPY config/fuzzingserver.json /config/fuzzingserver.json 8 | RUN chmod +775 /config/fuzzingserver.json 9 | RUN apt-get update && \ 10 | apt-get install -y \ 11 | docker \ 12 | docker-compose -------------------------------------------------------------------------------- /tests/autobahn/local_run/README.md: -------------------------------------------------------------------------------- 1 | # Run tests locally 2 | 3 | Todo: document how, also how to use https://github.com/nektos/act 4 | 5 | -------------------------------------------------------------------------------- /tests/autobahn/ws_test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thevlang/vlang:buster-dev 2 | 3 | # ARG WORKSPACE_ROOT 4 | 5 | # WORKDIR ${WORKSPACE_ROOT} 6 | COPY ./ /src/ 7 | # COPY tests/autobahn/ws_test/run.sh /run.sh 8 | # RUN chmod +x /run.sh 9 | RUN v install emily33901.net 10 | RUN v -autofree /src/tests/autobahn/autobahn_server.v 11 | RUN chmod +x /src/tests/autobahn/autobahn_server 12 | ENTRYPOINT [ "/src/tests/autobahn/autobahn_server" ] 13 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'websocket', 3 | description: 'New websocket library with websocket server', 4 | version: '0.0.1' 5 | dependencies: [ 6 | "emily33901.net" 7 | ] 8 | } -------------------------------------------------------------------------------- /websocket/err.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | // These are errors regarding the net.Socket2 api 4 | const ( 5 | socket_errors_base = 0 6 | err_new_socket_failed = error_with_code('net: new_socket failed to create socket', 7 | socket_errors_base + 1) 8 | err_option_not_settable = error_with_code('net: set_option_xxx option not settable', 9 | socket_errors_base + 2) 10 | err_option_wrong_type = error_with_code('net: set_option_xxx option wrong type', 11 | socket_errors_base + 3) 12 | err_invalid_port = error_with_code('', socket_errors_base + 4) 13 | err_port_out_of_range = error_with_code('', socket_errors_base + 5) 14 | err_no_udp_remote = error_with_code('', socket_errors_base + 6) 15 | err_connect_failed = error_with_code('net: connect failed', socket_errors_base + 7) 16 | err_connect_timed_out = error_with_code('net: connect timed out', socket_errors_base + 8) 17 | err_read_timed_out = error_with_code('net: read timed out', socket_errors_base + 9) 18 | err_read_timed_out_code = socket_errors_base + 9 19 | err_write_timed_out = error_with_code('net: write timed out', socket_errors_base + 10) 20 | err_write_timed_out_code = socket_errors_base + 10 21 | ) 22 | 23 | pub fn socket_error(potential_code int) ?int { 24 | $if windows { 25 | if potential_code < 0 { 26 | last_error := net.wsa_error(C.WSAGetLastError()) 27 | return error_with_code('net: socket error: $last_error', last_error) 28 | } 29 | } $else { 30 | if potential_code < 0 { 31 | last_error := error_code() 32 | return error_with_code('net: socket error: $last_error', last_error) 33 | } 34 | } 35 | return potential_code 36 | } 37 | 38 | pub fn wrap_error(error_code int) ? { 39 | $if windows { 40 | enum_error := net.wsa_error(error_code) 41 | return error_with_code('socket error: $enum_error', error_code) 42 | } $else { 43 | return error_with_code('net: socket error: $error_code', error_code) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /websocket/events.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | // All this plumbing will go awauy when we can do EventHandler properly 4 | struct MessageEventHandler { 5 | handler SocketMessageFn 6 | handler2 SocketMessageFn2 7 | is_ref bool = false 8 | ref voidptr 9 | } 10 | 11 | struct ErrorEventHandler { 12 | handler SocketErrorFn 13 | handler2 SocketErrorFn2 14 | is_ref bool = false 15 | ref voidptr 16 | } 17 | 18 | struct OpenEventHandler { 19 | handler SocketOpenFn 20 | handler2 SocketOpenFn2 21 | is_ref bool = false 22 | ref voidptr 23 | } 24 | 25 | struct CloseEventHandler { 26 | handler SocketCloseFn 27 | handler2 SocketCloseFn2 28 | is_ref bool = false 29 | ref voidptr 30 | } 31 | 32 | pub type AcceptClientFn = fn (mut c ServerClient) ?bool 33 | pub type SocketMessageFn = fn (mut c Client, msg &Message)? 34 | pub type SocketMessageFn2 = fn (mut c Client, msg &Message, v voidptr)? 35 | pub type SocketErrorFn = fn (mut c Client, err string)? 36 | pub type SocketErrorFn2 = fn (mut c Client, err string, v voidptr)? 37 | pub type SocketOpenFn = fn (mut c Client)? 38 | pub type SocketOpenFn2 = fn (mut c Client, v voidptr)? 39 | pub type SocketCloseFn = fn (mut c Client, code int, reason string)? 40 | pub type SocketCloseFn2 = fn (mut c Client, code int, reason string, v voidptr)? 41 | 42 | pub fn (mut s Server) on_connect(fun AcceptClientFn) ? { 43 | if s.accept_client_callbacks.len > 0 { 44 | return error('only one callback can be registered for accept client') 45 | } 46 | s.accept_client_callbacks << fun 47 | } 48 | 49 | fn (mut s Server) send_connect_event(mut c ServerClient) ?bool { 50 | if s.accept_client_callbacks.len == 0 { 51 | // If no callback all client will be accepted 52 | return true 53 | } 54 | fun := s.accept_client_callbacks[0] 55 | res := fun(mut c)? 56 | return res 57 | } 58 | 59 | // on_message, register a callback on new messages 60 | pub fn (mut s Server) on_message(fun SocketMessageFn) { 61 | s.message_callbacks << MessageEventHandler{ 62 | handler: fun 63 | } 64 | } 65 | 66 | // on_message_ref, register a callback on new messages and provide a reference 67 | pub fn (mut s Server) on_message_ref(fun SocketMessageFn2, ref voidptr) { 68 | s.message_callbacks << MessageEventHandler{ 69 | handler2: fun 70 | ref: ref 71 | is_ref: true 72 | } 73 | } 74 | 75 | // on_close, register a callback on closed socket 76 | pub fn (mut s Server) on_close(fun SocketCloseFn) { 77 | s.close_callbacks << CloseEventHandler{ 78 | handler: fun 79 | } 80 | } 81 | 82 | // on_close_ref, register a callback on closed socket and provide a reference 83 | pub fn (mut s Server) on_close_ref(fun SocketCloseFn2, ref voidptr) { 84 | s.close_callbacks << CloseEventHandler{ 85 | handler2: fun 86 | ref: ref 87 | is_ref: true 88 | } 89 | } 90 | 91 | // on_message, register a callback on new messages 92 | pub fn (mut ws Client) on_message(fun SocketMessageFn) { 93 | ws.message_callbacks << MessageEventHandler{ 94 | handler: fun 95 | } 96 | } 97 | 98 | // on_message_ref, register a callback on new messages and provide a reference 99 | pub fn (mut ws Client) on_message_ref(fun SocketMessageFn2, ref voidptr) { 100 | ws.message_callbacks << MessageEventHandler{ 101 | handler2: fun 102 | ref: ref 103 | is_ref: true 104 | } 105 | } 106 | 107 | // on_error, register a callback on errors 108 | pub fn (mut ws Client) on_error(fun SocketErrorFn) { 109 | ws.error_callbacks << ErrorEventHandler{ 110 | handler: fun 111 | } 112 | } 113 | 114 | // on_error_ref, register a callback on errors and provida a reference 115 | pub fn (mut ws Client) on_error_ref(fun SocketErrorFn2, ref voidptr) { 116 | ws.error_callbacks << ErrorEventHandler{ 117 | handler2: fun 118 | ref: ref 119 | is_ref: true 120 | } 121 | } 122 | 123 | // on_open, register a callback on successful open 124 | pub fn (mut ws Client) on_open(fun SocketOpenFn) { 125 | ws.open_callbacks << OpenEventHandler{ 126 | handler: fun 127 | } 128 | } 129 | 130 | // on_open_ref, register a callback on successful open and provide a reference 131 | pub fn (mut ws Client) on_open_ref(fun SocketOpenFn2, ref voidptr) { 132 | ws.open_callbacks << OpenEventHandler{ 133 | handler2: fun 134 | ref: ref 135 | is_ref: true 136 | } 137 | } 138 | 139 | // on_close, register a callback on closed socket 140 | pub fn (mut ws Client) on_close(fun SocketCloseFn) { 141 | ws.close_callbacks << CloseEventHandler{ 142 | handler: fun 143 | } 144 | } 145 | 146 | // on_close_ref, register a callback on closed socket and provide a reference 147 | pub fn (mut ws Client) on_close_ref(fun SocketCloseFn2, ref voidptr) { 148 | ws.close_callbacks << CloseEventHandler{ 149 | handler2: fun 150 | ref: ref 151 | is_ref: true 152 | } 153 | } 154 | 155 | fn (mut ws Client) send_message_event(mut msg Message) { 156 | ws.debug_log('sending on_message event') 157 | for ev_handler in ws.message_callbacks { 158 | if !ev_handler.is_ref { 159 | ev_handler.handler(ws, msg) 160 | } else { 161 | ev_handler.handler2(ws, msg, ev_handler.ref) 162 | } 163 | } 164 | } 165 | 166 | fn (mut ws Client) send_error_event(err string) { 167 | ws.debug_log('sending on_error event') 168 | for ev_handler in ws.error_callbacks { 169 | if !ev_handler.is_ref { 170 | ev_handler.handler(mut ws, err) 171 | } else { 172 | ev_handler.handler2(mut ws, err, ev_handler.ref) 173 | } 174 | } 175 | } 176 | 177 | fn (mut ws Client) send_close_event(code int, reason string) { 178 | ws.debug_log('sending on_close event') 179 | for ev_handler in ws.close_callbacks { 180 | if !ev_handler.is_ref { 181 | ev_handler.handler(mut ws, code, reason) 182 | } else { 183 | ev_handler.handler2(mut ws, code, reason, ev_handler.ref) 184 | } 185 | } 186 | } 187 | 188 | fn (mut ws Client) send_open_event() { 189 | ws.debug_log('sending on_open event') 190 | for ev_handler in ws.open_callbacks { 191 | if !ev_handler.is_ref { 192 | ev_handler.handler(mut ws) 193 | } else { 194 | ev_handler.handler2(mut ws, ev_handler.ref) 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /websocket/handshake.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import encoding.base64 4 | 5 | // handshake manage the handshake part of connecting 6 | fn (mut ws Client) handshake() ? { 7 | nonce := get_nonce(ws.nonce_size) 8 | seckey := base64.encode(nonce) 9 | handshake := 'GET $ws.uri.resource$ws.uri.querystring HTTP/1.1\r\nHost: $ws.uri.hostname:$ws.uri.port\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $seckey\r\nSec-WebSocket-Version: 13\r\n\r\n' 10 | handshake_bytes := handshake.bytes() 11 | ws.debug_log('sending handshake: $handshake') 12 | ws.socket_write(handshake_bytes)? 13 | ws.read_handshake(seckey)? 14 | } 15 | 16 | // handshake manage the handshake part of connecting 17 | fn (mut s Server) handle_server_handshake(mut c Client) ?(string, &ServerClient) { 18 | msg := c.read_handshake_str()? 19 | handshake_response, client := s.parse_client_handshake(msg, mut c)? 20 | return handshake_response, client 21 | } 22 | 23 | fn (mut s Server) parse_client_handshake(client_handshake string, mut c Client) ?(string, &ServerClient) { 24 | s.logger.debug('server-> client handshake:\n$client_handshake') 25 | lines := client_handshake.split_into_lines() 26 | get_tokens := lines[0].split(' ') 27 | if get_tokens.len < 3 { 28 | return error('unexpected get operation, $get_tokens') 29 | } 30 | if get_tokens[0].trim_space() != 'GET' { 31 | return error("unexpected request '${get_tokens[0]}', expected 'GET'") 32 | } 33 | if get_tokens[2].trim_space() != 'HTTP/1.1' { 34 | return error("unexpected request $get_tokens, expected 'HTTP/1.1'") 35 | } 36 | // path := get_tokens[1].trim_space() 37 | mut seckey := '' 38 | mut flags := []Flag{} 39 | mut key := '' 40 | for i in 1 .. lines.len { 41 | if lines[i].len <= 0 || lines[i] == '\r\n' { 42 | continue 43 | } 44 | keys := lines[i].split(':') 45 | match keys[0] { 46 | 'Upgrade', 'upgrade' { 47 | flags << .has_upgrade 48 | } 49 | 'Connection', 'connection' { 50 | flags << .has_connection 51 | } 52 | 'Sec-WebSocket-Key', 'sec-websocket-key' { 53 | key = keys[1].trim_space() 54 | s.logger.debug('server-> got key: $key') 55 | seckey = create_key_challenge_response(key)? 56 | s.logger.debug('server-> challenge: $seckey, response: ${keys[1]}') 57 | flags << .has_accept 58 | } 59 | else { 60 | // We ignore other headers like protocol for now 61 | } 62 | } 63 | } 64 | if flags.len < 3 { 65 | return error('invalid client handshake, $client_handshake') 66 | } 67 | server_handshake := 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $seckey\r\n\r\n' 68 | server_client := &ServerClient{ 69 | resource_name: get_tokens[1] 70 | client_key: key 71 | client: c 72 | server: s 73 | } 74 | return server_handshake, server_client 75 | } 76 | 77 | fn (mut ws Client) read_handshake_str() ?string { 78 | mut total_bytes_read := 0 79 | max_buffer := 1024 80 | mut msg := []byte{cap: max_buffer} 81 | mut buffer := []byte{len: 1} 82 | for total_bytes_read < max_buffer { 83 | bytes_read := ws.socket_read_into(mut buffer)? 84 | if bytes_read == 0 { 85 | return error('unexpected no response from handshake') 86 | } 87 | total_bytes_read++ 88 | msg << buffer[0] 89 | if total_bytes_read > 5 && msg[total_bytes_read - 1] == `\n` && msg[total_bytes_read - 90 | 2] == `\r` && msg[total_bytes_read - 3] == `\n` && msg[total_bytes_read - 4] == `\r` { 91 | break 92 | } 93 | } 94 | return string(msg) 95 | } 96 | 97 | // read_handshake reads the handshake and check if valid 98 | fn (mut ws Client) read_handshake(seckey string) ? { 99 | msg := ws.read_handshake_str()? 100 | ws.check_handshake_response(msg, seckey)? 101 | } 102 | 103 | fn (mut ws Client) check_handshake_response(handshake_response, seckey string) ? { 104 | ws.debug_log('handshake response:\n$handshake_response') 105 | lines := handshake_response.split_into_lines() 106 | header := lines[0] 107 | if !header.starts_with('HTTP/1.1 101') && !header.starts_with('HTTP/1.0 101') { 108 | return error('handshake_handler: invalid HTTP status response code') 109 | } 110 | for i in 1 .. lines.len { 111 | if lines[i].len <= 0 || lines[i] == '\r\n' { 112 | continue 113 | } 114 | keys := lines[i].split(':') 115 | match keys[0] { 116 | 'Upgrade', 'upgrade' { 117 | ws.flags << .has_upgrade 118 | } 119 | 'Connection', 'connection' { 120 | ws.flags << .has_connection 121 | } 122 | 'Sec-WebSocket-Accept', 'sec-websocket-accept' { 123 | ws.debug_log('seckey: $seckey') 124 | challenge := create_key_challenge_response(seckey)? 125 | ws.debug_log('challenge: $challenge, response: ${keys[1]}') 126 | if keys[1].trim_space() != challenge { 127 | return error('handshake_handler: Sec-WebSocket-Accept header does not match computed sha1/base64 response.') 128 | } 129 | ws.flags << .has_accept 130 | } 131 | else {} 132 | } 133 | } 134 | if ws.flags.len < 3 { 135 | ws.close(1002, 'invalid websocket HTTP headers')? 136 | return error('invalid websocket HTTP headers') 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /websocket/io.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import emily33901.net 4 | import time 5 | 6 | interface WebsocketIO { 7 | socket_read_into(mut buffer []byte) ?int 8 | socket_write(bytes []byte) ? 9 | } 10 | 11 | // socket_read_into reads into the provided buffer with it's lenght 12 | fn (mut ws Client) socket_read_into(mut buffer []byte) ?int { 13 | lock { 14 | if ws.is_ssl { 15 | r := ws.ssl_conn.read_into(mut buffer)? 16 | return r 17 | } else { 18 | for { 19 | r := ws.conn.read_into(mut buffer) or { 20 | if errcode == net.err_read_timed_out_code { 21 | continue 22 | } 23 | return error(err) 24 | } 25 | return r 26 | } 27 | } 28 | } 29 | } 30 | 31 | // socket_write, writes the whole byte array provided to the socket 32 | fn (mut ws Client) socket_write(bytes []byte) ? { 33 | lock { 34 | if ws.state == .closed || ws.conn.sock.handle <= 1 { 35 | ws.debug_log('write: Socket allready closed') 36 | return error('Socket allready closed') 37 | } 38 | if ws.is_ssl { 39 | ws.ssl_conn.write(bytes)? 40 | } else { 41 | for { 42 | ws.conn.write(bytes) or { 43 | if errcode == net.err_write_timed_out_code { 44 | continue 45 | } 46 | return error(err) 47 | } 48 | return 49 | } 50 | } 51 | } 52 | } 53 | 54 | // shutdown_socket, proper shutdown make PR in Emeliy repo 55 | fn (mut ws Client) shutdown_socket() ? { 56 | ws.debug_log('shutting down socket') 57 | if ws.is_ssl { 58 | ws.ssl_conn.shutdown()? 59 | } else { 60 | ws.conn.close()? 61 | } 62 | return none 63 | } 64 | 65 | // dial_socket, setup socket communication, options and timeouts 66 | fn (mut ws Client) dial_socket() ?net.TcpConn { 67 | mut t := net.dial_tcp('$ws.uri.hostname:$ws.uri.port')? 68 | optval := int(1) 69 | t.sock.set_option_int(.keep_alive, optval)? 70 | t.set_read_timeout(10 * time.millisecond) 71 | t.set_write_timeout(10 * time.millisecond) 72 | if ws.is_ssl { 73 | ws.ssl_conn.connect(mut t)? 74 | } 75 | return t 76 | } 77 | -------------------------------------------------------------------------------- /websocket/message.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import encoding.utf8 4 | 5 | const ( 6 | header_len_offset = 2 7 | buffer_size = 256 8 | extended_payload16_end_byte = 4 9 | extended_payload64_end_byte = 10 10 | ) 11 | 12 | struct Fragment { 13 | data []byte 14 | opcode OPCode 15 | } 16 | 17 | struct Frame { 18 | mut: 19 | header_len int = 2 20 | frame_size int = 2 21 | fin bool 22 | rsv1 bool 23 | rsv2 bool 24 | rsv3 bool 25 | opcode OPCode 26 | has_mask bool 27 | payload_len int 28 | masking_key []byte = []byte{len: 4} 29 | } 30 | 31 | const ( 32 | invalid_close_codes = [999, 1004, 1005, 1006, 1014, 1015, 1016, 1100, 2000, 2999, 5000, 65536] 33 | ) 34 | 35 | // validate_client, validate client frame rules from RFC6455 36 | pub fn (mut ws Client) validate_frame(frame &Frame) ? { 37 | if frame.rsv1 || frame.rsv2 || frame.rsv3 { 38 | ws.close(1002, 'rsv cannot be other than 0, not negotiated') 39 | return error('rsv cannot be other than 0, not negotiated') 40 | } 41 | if (int(frame.opcode) >= 3 && int(frame.opcode) <= 7) || 42 | (int(frame.opcode) >= 11 && int(frame.opcode) <= 15) { 43 | ws.close(1002, 'use of reserved opcode')? 44 | return error('use of reserved opcode') 45 | } 46 | if frame.has_mask && !ws.is_server { 47 | // Server should never send masked frames 48 | // to client, close connection 49 | ws.close(1002, 'client got masked frame')? 50 | return error('client sent masked frame') 51 | } 52 | if is_control_frame(frame.opcode) { 53 | if !frame.fin { 54 | ws.close(1002, 'control message must not be fragmented')? 55 | return error('unexpected control frame with no fin') 56 | } 57 | if frame.payload_len > 125 { 58 | ws.close(1002, 'control frames must not exceed 125 bytes')? 59 | return error('unexpected control frame payload length') 60 | } 61 | } 62 | if frame.fin == false && ws.fragments.len == 0 && frame.opcode == .continuation { 63 | ws.close(1002, 'unexecpected continuation, there are no frames to continue, $frame')? 64 | return error('unexecpected continuation, there are no frames to continue, $frame') 65 | } 66 | } 67 | 68 | [inline] 69 | fn is_control_frame(opcode OPCode) bool { 70 | return opcode !in [.text_frame, .binary_frame, .continuation] 71 | } 72 | 73 | [inline] 74 | fn is_data_frame(opcode OPCode) bool { 75 | return opcode in [.text_frame, .binary_frame] 76 | } 77 | 78 | [inline] 79 | // read_payload, reads the payload from socket 80 | fn (mut ws Client) read_payload(payload_len int) ?[]byte { 81 | if payload_len == 0 { 82 | return []byte{} 83 | } 84 | // TODO: make a dynamic reusable memory pool here 85 | mut buffer := []byte{cap: payload_len} 86 | mut read_buf := []byte{len: 1} 87 | mut bytes_read := 0 88 | for bytes_read < payload_len { 89 | len := ws.socket_read_into(mut read_buf)? 90 | if len != 1 { 91 | return error('expected read all message, got zero') 92 | } 93 | bytes_read += len 94 | buffer << read_buf[0] 95 | } 96 | if bytes_read != payload_len { 97 | return error('failed to read payload') 98 | } 99 | return buffer 100 | } 101 | 102 | // validate_utf_8, validates payload for valid utf encoding 103 | // todo: support fail fast utf errors for strict autobahn conformance 104 | fn (mut ws Client) validate_utf_8(opcode OPCode, payload []byte) ? { 105 | if opcode in [.text_frame, .close] && !utf8.validate(payload.data, payload.len) { 106 | ws.logger.error('malformed utf8 payload, payload len: ($payload.len)') 107 | // ws.send_error_event('Recieved malformed utf8.') 108 | ws.close(1007, 'malformed utf8 payload') 109 | return error('malformed utf8 payload') 110 | } 111 | } 112 | 113 | // read_next_message reads 1 to n frames to compose a message 114 | pub fn (mut ws Client) read_next_message() ?&Message { 115 | for { 116 | frame := ws.parse_frame_header()? 117 | ws.debug_log('read_next_message: frame\n$frame') 118 | ws.validate_frame(&frame)? 119 | mut frame_payload := ws.read_payload(frame.payload_len)? 120 | if frame.has_mask { 121 | for i in 0 .. frame_payload.len { 122 | frame_payload[i] ^= frame.masking_key[i % 4] & 0xff 123 | } 124 | // frame.unmask_sequence(mut frame_payload) 125 | } 126 | if is_control_frame(frame.opcode) { 127 | // Control frames can interject other frames 128 | // and need to be returned immediately 129 | return &Message{ 130 | opcode: OPCode(frame.opcode) 131 | payload: frame_payload 132 | } 133 | } 134 | // If the message is fragmented we just put it on fragments 135 | // a fragment is allowed to have zero size payload 136 | if !frame.fin { 137 | ws.fragments << &Fragment{ 138 | data: frame_payload 139 | opcode: frame.opcode 140 | } 141 | continue 142 | } 143 | if ws.fragments.len == 0 { 144 | ws.validate_utf_8(frame.opcode, frame_payload) or { 145 | ws.logger.error('UTF8 validation error: $err, len of payload($frame_payload.len)') 146 | return error(err) 147 | } 148 | return &Message{ 149 | opcode: OPCode(frame.opcode) 150 | payload: frame_payload 151 | } 152 | } 153 | defer { 154 | ws.fragments = [] 155 | } 156 | if is_data_frame(frame.opcode) { 157 | ws.close(0, '')? 158 | return error('Unexpected frame opcode') 159 | } 160 | payload := ws.payload_from_fragments(frame_payload)? 161 | opcode := ws.opcode_from_fragments() 162 | ws.validate_utf_8(opcode, payload)? 163 | return &Message{ 164 | opcode: opcode 165 | payload: payload 166 | } 167 | } 168 | } 169 | 170 | [inline] 171 | // payload_from_fragments, returs the whole paylaod from fragmented message 172 | fn (ws Client) payload_from_fragments(fin_payload []byte) ?[]byte { 173 | mut total_size := 0 174 | for f in ws.fragments { 175 | if f.data.len > 0 { 176 | total_size += f.data.len 177 | } 178 | } 179 | total_size += fin_payload.len 180 | if total_size == 0 { 181 | return []byte{} 182 | } 183 | mut total_buffer := []byte{cap: total_size} 184 | for f in ws.fragments { 185 | if f.data.len > 0 { 186 | total_buffer << f.data 187 | } 188 | } 189 | total_buffer << fin_payload 190 | return total_buffer 191 | } 192 | 193 | // opcode_from_fragments, returns the opcode for message from the first fragment sent 194 | fn (ws Client) opcode_from_fragments() OPCode { 195 | return OPCode(ws.fragments[0].opcode) 196 | } 197 | 198 | // parse_frame_header parses next message by decoding the incoming frames 199 | pub fn (mut ws Client) parse_frame_header() ?Frame { 200 | // TODO: make a dynamic reusable memory pool here 201 | mut buffer := []byte{cap: buffer_size} 202 | // mut bytes_read := u64(0) 203 | mut frame := Frame{} 204 | mut rbuff := []byte{len: 1} 205 | mut mask_end_byte := 0 206 | for ws.state == .open { 207 | // Todo: different error scenarios to make sure we close correctly on error 208 | // reader.read_into(mut rbuff) ? 209 | read_bytes := ws.socket_read_into(mut rbuff)? 210 | if read_bytes == 0 { 211 | // This is probably a timeout or close 212 | continue 213 | } 214 | buffer << rbuff[0] 215 | // bytes_read++ 216 | // parses the first two header bytes to get basic frame information 217 | if buffer.len == u64(header_len_offset) { 218 | frame.fin = (buffer[0] & 0x80) == 0x80 219 | frame.rsv1 = (buffer[0] & 0x40) == 0x40 220 | frame.rsv2 = (buffer[0] & 0x20) == 0x20 221 | frame.rsv3 = (buffer[0] & 0x10) == 0x10 222 | frame.opcode = OPCode(int(buffer[0] & 0x7F)) 223 | frame.has_mask = (buffer[1] & 0x80) == 0x80 224 | frame.payload_len = buffer[1] & 0x7F 225 | // if has mask set the byte postition where mask ends 226 | if frame.has_mask { 227 | mask_end_byte = if frame.payload_len < 126 { 228 | header_len_offset + 4 229 | } else if frame.payload_len == 126 { 230 | header_len_offset + 6 231 | } else if frame.payload_len == 127 { 232 | header_len_offset + 12 233 | } else { 234 | 0 235 | } // Impossible 236 | } 237 | frame.payload_len = frame.payload_len 238 | frame.frame_size = frame.header_len + frame.payload_len 239 | if !frame.has_mask && frame.payload_len < 126 { 240 | return frame 241 | } 242 | } 243 | if frame.payload_len == 126 && buffer.len == u64(extended_payload16_end_byte) { 244 | frame.header_len += 2 245 | frame.payload_len = 0 246 | frame.payload_len |= buffer[2] << 8 247 | frame.payload_len |= buffer[3] << 0 248 | frame.frame_size = frame.header_len + frame.payload_len 249 | if !frame.has_mask { 250 | return frame 251 | } 252 | } 253 | if frame.payload_len == 127 && buffer.len == u64(extended_payload64_end_byte) { 254 | frame.header_len += 8 // TODO Not sure... 255 | frame.payload_len = 0 256 | frame.payload_len |= buffer[2] << 56 257 | frame.payload_len |= buffer[3] << 48 258 | frame.payload_len |= buffer[4] << 40 259 | frame.payload_len |= buffer[5] << 32 260 | frame.payload_len |= buffer[6] << 24 261 | frame.payload_len |= buffer[7] << 16 262 | frame.payload_len |= buffer[8] << 8 263 | frame.payload_len |= buffer[9] << 0 264 | if !frame.has_mask { 265 | return frame 266 | } 267 | } 268 | // We have a mask and we read the whole mask data 269 | if frame.has_mask && buffer.len == mask_end_byte { 270 | frame.masking_key[0] = buffer[mask_end_byte - 4] 271 | frame.masking_key[1] = buffer[mask_end_byte - 3] 272 | frame.masking_key[2] = buffer[mask_end_byte - 2] 273 | frame.masking_key[3] = buffer[mask_end_byte - 1] 274 | return frame 275 | } 276 | } 277 | } 278 | 279 | [inline] 280 | // unmask_sequence unmask any given sequence 281 | fn (f Frame) unmask_sequence(mut buffer []byte) { 282 | for i in 0 .. buffer.len { 283 | buffer[i] ^= f.masking_key[i % 4] & 0xff 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /websocket/ssl.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import net.openssl 4 | import emily33901.net 5 | import time 6 | 7 | const ( 8 | is_used = openssl.is_used 9 | ) 10 | 11 | fn C.SSL_get_error() int 12 | 13 | // Todo: move this to openssl lib later 14 | // For SSL to compile on windows we need to 15 | // change the open ssl C flags to : 16 | // #flag linux -I/usr/local/include/openssl -L/usr/local/lib 17 | // #flag linux -l ssl -l crypto 18 | // #flag windows -l libssl -l libcrypto 19 | // // MacPorts 20 | // #flag darwin -I/opt/local/include 21 | // #flag darwin -L/opt/local/lib 22 | // #flag darwin -l ssl -l crypto 23 | pub struct SSLConn { 24 | mut: 25 | sslctx &C.SSL_CTX 26 | ssl &C.SSL 27 | handle int 28 | duration time.Duration 29 | } 30 | 31 | enum Select { 32 | read 33 | write 34 | except 35 | } 36 | 37 | pub fn new_ssl_conn() &SSLConn { 38 | return &SSLConn{ 39 | sslctx: 0 40 | ssl: 0 41 | handle: 0 42 | } 43 | } 44 | 45 | // shutdown closes the ssl connection and do clean up 46 | pub fn (mut s SSLConn) shutdown() ? { 47 | if s.ssl != 0 { 48 | mut res := 0 49 | for { 50 | res = int(C.SSL_shutdown(s.ssl)) 51 | if res < 0 { 52 | err_res := s.ssl_error(res) or { 53 | break // We break to free rest of resources 54 | } 55 | if err_res == .ssl_error_want_read { 56 | for { 57 | ready := @select(s.handle, .read, s.duration)? 58 | if ready { 59 | break 60 | } 61 | } 62 | continue 63 | } else if err_res == .ssl_error_want_write { 64 | for { 65 | ready := @select(s.handle, .write, s.duration)? 66 | if ready { 67 | break 68 | } 69 | } 70 | continue 71 | } else { 72 | println('error: $err_res') 73 | return error('unexepedted ssl error $err_res') 74 | } 75 | C.SSL_free(s.ssl) 76 | if s.sslctx != 0 { 77 | C.SSL_CTX_free(s.sslctx) 78 | } 79 | return error('Could not connect using SSL. ($err_res),err') 80 | } else if res == 0 { 81 | continue 82 | } else if res == 1 { 83 | break 84 | } 85 | } 86 | C.SSL_free(s.ssl) 87 | } 88 | if s.sslctx != 0 { 89 | C.SSL_CTX_free(s.sslctx) 90 | } 91 | } 92 | 93 | // connect to server using open ssl 94 | pub fn (mut s SSLConn) connect(mut tcp_conn net.TcpConn) ? { 95 | s.handle = tcp_conn.sock.handle 96 | s.duration = tcp_conn.read_timeout() 97 | C.SSL_load_error_strings() 98 | s.sslctx = C.SSL_CTX_new(C.SSLv23_client_method()) 99 | if s.sslctx == 0 { 100 | return error("Couldn't get ssl context") 101 | } 102 | s.ssl = C.SSL_new(s.sslctx) 103 | if s.ssl == 0 { 104 | return error("Couldn't create OpenSSL instance.") 105 | } 106 | if C.SSL_set_fd(s.ssl, tcp_conn.sock.handle) != 1 { 107 | return error("Couldn't assign ssl to socket.") 108 | } 109 | for { 110 | res := C.SSL_connect(s.ssl) 111 | if res != 1 { 112 | err_res := s.ssl_error(res)? 113 | if err_res == .ssl_error_want_read { 114 | for { 115 | ready := @select(s.handle, .read, s.duration)? 116 | if ready { 117 | break 118 | } 119 | } 120 | continue 121 | } else if err_res == .ssl_error_want_write { 122 | for { 123 | ready := @select(s.handle, .write, s.duration)? 124 | if ready { 125 | break 126 | } 127 | } 128 | continue 129 | } 130 | return error('Could not connect using SSL. ($err_res),err') 131 | } 132 | break 133 | } 134 | } 135 | 136 | pub fn (mut s SSLConn) read_into(mut buffer []Byte) ?int { 137 | mut res := 0 138 | for { 139 | res = C.SSL_read(s.ssl, buffer.data, buffer.len) 140 | if res < 0 { 141 | err_res := s.ssl_error(res)? 142 | if err_res == .ssl_error_want_read { 143 | for { 144 | ready := @select(s.handle, .read, s.duration)? 145 | if ready { 146 | break 147 | } 148 | } 149 | continue 150 | } else if err_res == .ssl_error_want_write { 151 | for { 152 | ready := @select(s.handle, .write, s.duration)? 153 | if ready { 154 | break 155 | } 156 | } 157 | continue 158 | } else if err_res == .ssl_error_zero_return { 159 | println(err_res) 160 | return 0 161 | } 162 | println(err_res) 163 | return error('Could not read using SSL. ($err_res),err') 164 | } 165 | break 166 | } 167 | return res 168 | } 169 | 170 | // write number of bytes to SSL connection 171 | pub fn (mut s SSLConn) write(bytes []Byte) ? { 172 | unsafe { 173 | mut ptr_base := byteptr(bytes.data) 174 | mut total_sent := 0 175 | for total_sent < bytes.len { 176 | ptr := ptr_base + total_sent 177 | remaining := bytes.len - total_sent 178 | mut sent := C.SSL_write(s.ssl, ptr, remaining) 179 | if sent <= 0 { 180 | err_res := s.ssl_error(sent)? 181 | if err_res == .ssl_error_want_read { 182 | for { 183 | ready := @select(s.handle, .read, s.duration)? 184 | if ready { 185 | break 186 | } 187 | } 188 | } else if err_res == .ssl_error_want_write { 189 | for { 190 | ready := @select(s.handle, .write, s.duration)? 191 | if ready { 192 | break 193 | } 194 | } 195 | continue 196 | } else if err_res == .ssl_error_zero_return { 197 | return error('ssl write on closed connection') // Todo error_with_code close 198 | } 199 | return error_with_code('Could not write SSL. ($err_res),err', err_res) 200 | } 201 | total_sent += sent 202 | } 203 | } 204 | } 205 | 206 | // ssl_error returns non error ssl code or error if unrecoverable and we should panic 207 | fn (mut s SSLConn) ssl_error(ret int) ?SSLError { 208 | res := C.SSL_get_error(s.ssl, ret) 209 | match SSLError(res) { 210 | .ssl_error_syscall { return error_with_code('unrecoverable syscall ($res)', res) } 211 | .ssl_error_ssl { return error_with_code('unrecoverable ssl protocol error ($res)', 212 | res) } 213 | else { return res } 214 | } 215 | } 216 | 217 | enum SSLError { 218 | ssl_error_none = C.SSL_ERROR_NONE 219 | ssl_error_ssl = C.SSL_ERROR_SSL 220 | ssl_error_want_read = C.SSL_ERROR_WANT_READ 221 | ssl_error_want_write = C.SSL_ERROR_WANT_WRITE 222 | ssl_error_want_x509_lookup = C.SSL_ERROR_WANT_X509_LOOKUP 223 | ssl_error_syscall = C.SSL_ERROR_SYSCALL 224 | ssl_error_zero_return = C.SSL_ERROR_ZERO_RETURN 225 | ssl_error_want_connect = C.SSL_ERROR_WANT_CONNECT 226 | ssl_error_want_accept = C.SSL_ERROR_WANT_ACCEPT 227 | ssl_error_want_async = C.SSL_ERROR_WANT_ASYNC 228 | ssl_error_want_async_job = C.SSL_ERROR_WANT_ASYNC_JOB 229 | ssl_error_want_client_hello_cb = C.SSL_ERROR_WANT_CLIENT_HELLO_CB 230 | } 231 | 232 | /* 233 | This is basically a copy of Emily socket implementation of select. 234 | This have to be consolidated into common net lib features 235 | when merging this to V 236 | */ 237 | [typedef] 238 | pub struct C.fd_set { 239 | } 240 | 241 | // Select waits for an io operation (specified by parameter `test`) to be available 242 | fn @select(handle int, test Select, timeout time.Duration) ?bool { 243 | set := C.fd_set{} 244 | C.FD_ZERO(&set) 245 | C.FD_SET(handle, &set) 246 | timeval_timeout := C.timeval{ 247 | tv_sec: u64(0) 248 | tv_usec: u64(timeout.microseconds()) 249 | } 250 | match test { 251 | .read { socket_error(C.@select(handle, &set, C.NULL, C.NULL, &timeval_timeout))? } 252 | .write { socket_error(C.@select(handle, C.NULL, &set, C.NULL, &timeval_timeout))? } 253 | .except { socket_error(C.@select(handle, C.NULL, C.NULL, &set, &timeval_timeout))? } 254 | } 255 | return C.FD_ISSET(handle, &set) 256 | } 257 | -------------------------------------------------------------------------------- /websocket/uri.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | struct Uri { 4 | mut: 5 | url string 6 | hostname string 7 | port string 8 | resource string 9 | querystring string 10 | } 11 | 12 | pub fn (u Uri) str() string { 13 | return u.url 14 | } 15 | -------------------------------------------------------------------------------- /websocket/utils.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import rand 4 | import crypto.sha1 5 | import encoding.base64 6 | 7 | fn htonl64(payload_len u64) []byte { 8 | mut ret := []byte{len: 8} 9 | // mut ret := malloc(8) 10 | ret[0] = byte(((payload_len & (u64(0xff) << 56)) >> 56) & 0xff) 11 | ret[1] = byte(((payload_len & (u64(0xff) << 48)) >> 48) & 0xff) 12 | ret[2] = byte(((payload_len & (u64(0xff) << 40)) >> 40) & 0xff) 13 | ret[3] = byte(((payload_len & (u64(0xff) << 32)) >> 32) & 0xff) 14 | ret[4] = byte(((payload_len & (u64(0xff) << 24)) >> 24) & 0xff) 15 | ret[5] = byte(((payload_len & (u64(0xff) << 16)) >> 16) & 0xff) 16 | ret[6] = byte(((payload_len & (u64(0xff) << 8)) >> 8) & 0xff) 17 | ret[7] = byte(((payload_len & (u64(0xff) << 0)) >> 0) & 0xff) 18 | return ret 19 | } 20 | 21 | fn create_masking_key() []byte { 22 | mask_bit := byte(rand.intn(255)) 23 | buf := [`0`].repeat(4) 24 | unsafe { 25 | C.memcpy(buf.data, &mask_bit, 4) 26 | } 27 | return buf 28 | } 29 | 30 | fn create_key_challenge_response(seckey string) ?string { 31 | if seckey.len == 0 { 32 | return error('unexpected seckey lengt zero') 33 | } 34 | guid := '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 35 | sha1buf := seckey + guid 36 | hash := sha1.sum(sha1buf.bytes()) 37 | b64 := base64.encode(tos(hash.data, hash.len)) 38 | return b64 39 | } 40 | 41 | fn get_nonce(nonce_size int) string { 42 | mut nonce := []byte{len: nonce_size, cap: nonce_size} 43 | alphanum := '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz' 44 | for i in 0 .. nonce_size { 45 | nonce[i] = alphanum[rand.intn(alphanum.len)] 46 | } 47 | return tos(nonce.data, nonce.len).clone() 48 | } 49 | -------------------------------------------------------------------------------- /websocket/websocket_client.v: -------------------------------------------------------------------------------- 1 | // The module websocket implements the websocket capabilities 2 | // it is a refactor of the original V-websocket client class 3 | // from @thecoderr 4 | module websocket 5 | 6 | import emily33901.net 7 | import net.urllib 8 | import time 9 | import log 10 | import sync 11 | import rand 12 | 13 | // Client represents websocket client state 14 | pub struct Client { 15 | is_server bool = false 16 | mut: 17 | ssl_conn &SSLConn 18 | flags []Flag 19 | fragments []Fragment 20 | logger &log.Log 21 | message_callbacks []MessageEventHandler 22 | error_callbacks []ErrorEventHandler 23 | open_callbacks []OpenEventHandler 24 | close_callbacks []CloseEventHandler 25 | pub: 26 | is_ssl bool 27 | uri Uri 28 | id string 29 | pub mut: 30 | conn net.TcpConn 31 | nonce_size int = 16 // you can try 18 too 32 | panic_on_callback bool = false 33 | state State 34 | resource_name string 35 | last_pong_ut u64 36 | } 37 | 38 | enum Flag { 39 | has_accept 40 | has_connection 41 | has_upgrade 42 | } 43 | 44 | // State of the websocket connection. 45 | // Messages should be sent only on state .open 46 | enum State { 47 | connecting = 0 48 | open 49 | closing 50 | closed 51 | } 52 | 53 | // Message, represents a whole message conbined from 1 to n frames 54 | pub struct Message { 55 | pub: 56 | opcode OPCode 57 | payload []byte 58 | } 59 | 60 | // OPCode, the supported websocket frame types 61 | pub enum OPCode { 62 | continuation = 0x00 63 | text_frame = 0x01 64 | binary_frame = 0x02 65 | close = 0x08 66 | ping = 0x09 67 | pong = 0x0A 68 | } 69 | 70 | // new_client, instance a new websocket client 71 | pub fn new_client(address string) ?&Client { 72 | uri := parse_uri(address)? 73 | return &Client{ 74 | is_server: false 75 | ssl_conn: new_ssl_conn() 76 | is_ssl: address.starts_with('wss') 77 | logger: &log.Log{ 78 | level: .info 79 | } 80 | uri: uri 81 | state: .closed 82 | id: rand.uuid_v4() 83 | } 84 | } 85 | 86 | // connect, connects and do handshake procedure with remote server 87 | pub fn (mut ws Client) connect() ? { 88 | ws.assert_not_connected() 89 | ws.set_state(.connecting) 90 | ws.logger.info('connecting to host $ws.uri') 91 | ws.conn = ws.dial_socket()? 92 | ws.handshake()? 93 | ws.set_state(.open) 94 | ws.logger.info('successfully connected to host $ws.uri') 95 | ws.send_open_event() 96 | } 97 | 98 | // listen, listens to incoming messages and handles them 99 | pub fn (mut ws Client) listen() ? { 100 | ws.logger.info('Starting client listener, server($ws.is_server)...') 101 | defer { 102 | ws.logger.info('Quit client listener, server($ws.is_server)...') 103 | } 104 | for ws.state == .open { 105 | msg := ws.read_next_message() or { 106 | if ws.state in [.closed, .closing] { 107 | return 108 | } 109 | ws.debug_log('failed to read next message: $err') 110 | return error(err) 111 | } 112 | ws.debug_log('got message: $msg.opcode, payload: $msg.payload') 113 | match msg.opcode { 114 | .text_frame { 115 | ws.debug_log('read: text') 116 | ws.send_message_event(mut msg) 117 | } 118 | .binary_frame { 119 | ws.debug_log('read: binary') 120 | ws.send_message_event(mut msg) 121 | } 122 | .ping { 123 | ws.debug_log('read: ping, sending pong') 124 | ws.send_control_frame(.pong, 'PONG', msg.payload) or { 125 | ws.logger.error('error in message callback sending PONG: $err') 126 | if ws.panic_on_callback { 127 | panic(err) 128 | } 129 | continue 130 | } 131 | } 132 | .pong { 133 | ws.debug_log('read: pong') 134 | ws.last_pong_ut = time.now().unix 135 | ws.send_message_event(mut msg) 136 | } 137 | .close { 138 | ws.debug_log('read: close') 139 | defer { 140 | ws.manage_clean_close() 141 | } 142 | if msg.payload.len > 0 { 143 | if msg.payload.len == 1 { 144 | ws.close(1002, 'close payload cannot be 1 byte')? 145 | return error('close payload cannot be 1 byte') 146 | } 147 | code := (int(msg.payload[0]) << 8) + int(msg.payload[1]) 148 | if code in invalid_close_codes { 149 | ws.close(1002, 'invalid close code: $code')? 150 | return error('invalid close code: $code') 151 | } 152 | reason := if msg.payload.len > 2 { msg.payload[2..] } else { []byte{} } 153 | if reason.len > 0 { 154 | ws.validate_utf_8(.close, reason)? 155 | } 156 | if ws.state !in [.closing, .closed] { 157 | // sending close back according to spec 158 | ws.debug_log('close with reason, code: $code, reason: $reason') 159 | r := if reason.len > 0 { string(reason) } else { '' } 160 | ws.close(code, r)? 161 | } 162 | } else { 163 | if ws.state !in [.closing, .closed] { 164 | ws.debug_log('close with reason, no code') 165 | // sending close back according to spec 166 | ws.close(1000, 'normal')? 167 | } 168 | } 169 | return 170 | } 171 | .continuation { 172 | ws.logger.error('unexpected opcode continuation, nothing to continue') 173 | ws.close(1002, 'nothing to continue')? 174 | return error('unexpected opcode continuation, nothing to continue') 175 | } 176 | } 177 | } 178 | } 179 | 180 | // this function was needed for defer 181 | fn (mut ws Client) manage_clean_close() { 182 | ws.send_close_event(1000, 'closed by client') 183 | } 184 | 185 | // ping, sends ping message to server, 186 | // ping response will be pushed to message callback 187 | pub fn (mut ws Client) ping() ? { 188 | ws.send_control_frame(.ping, 'PING', [])? 189 | } 190 | 191 | // pong, sends pog message to server, 192 | // pongs are normally automatically sent back to server 193 | pub fn (mut ws Client) pong() ? { 194 | ws.send_control_frame(.pong, 'PONG', [])? 195 | } 196 | 197 | // write, writes a byte array with a websocket messagetype 198 | pub fn (mut ws Client) write(bytes []byte, code OPCode) ? { 199 | ws.debug_log('write code: $code, payload: $bytes') 200 | if ws.state != .open || ws.conn.sock.handle < 1 { 201 | // send error here later 202 | return error('trying to write on a closed socket!') 203 | } 204 | payload_len := bytes.len 205 | mut header_len := 2 + if payload_len > 125 { 2 } else { 0 } + if payload_len > 0xffff { 6 } else { 0 } 206 | if !ws.is_server { 207 | header_len += 4 208 | } 209 | mut header := [`0`].repeat(header_len) 210 | header[0] = byte(int(code)) | 0x80 211 | mut masking_key := []byte{} 212 | if ws.is_server { 213 | if payload_len <= 125 { 214 | header[1] = byte(payload_len) 215 | // 0x80 216 | } else if payload_len > 125 && payload_len <= 0xffff { 217 | len16 := C.htons(payload_len) 218 | header[1] = 126 219 | // 0x80 220 | // todo: fix v style copy instead 221 | unsafe { 222 | C.memcpy(&header[2], &len16, 2) 223 | } 224 | } else if payload_len > 0xffff && payload_len <= 0xffffffffffffffff { 225 | len_bytes := htonl64(u64(payload_len)) 226 | header[1] = 127 // 0x80 227 | // todo: fix v style copy instead 228 | unsafe { 229 | C.memcpy(&header[2], len_bytes.data, 8) 230 | } 231 | } 232 | } else { 233 | masking_key = create_masking_key() 234 | if payload_len <= 125 { 235 | header[1] = byte(payload_len | 0x80) 236 | header[2] = masking_key[0] 237 | header[3] = masking_key[1] 238 | header[4] = masking_key[2] 239 | header[5] = masking_key[3] 240 | } else if payload_len > 125 && payload_len <= 0xffff { 241 | len16 := C.htons(payload_len) 242 | header[1] = (126 | 0x80) 243 | // todo: fix v style copy instead 244 | unsafe { 245 | C.memcpy(&header[2], &len16, 2) 246 | } 247 | header[4] = masking_key[0] 248 | header[5] = masking_key[1] 249 | header[6] = masking_key[2] 250 | header[7] = masking_key[3] 251 | } else if payload_len > 0xffff && payload_len <= 0xffffffffffffffff { // 65535 && 18446744073709551615 252 | len64 := htonl64(u64(payload_len)) 253 | header[1] = (127 | 0x80) 254 | // todo: fix v style copy instead 255 | unsafe { 256 | C.memcpy(&header[2], len64.data, 8) 257 | } 258 | header[10] = masking_key[0] 259 | header[11] = masking_key[1] 260 | header[12] = masking_key[2] 261 | header[13] = masking_key[3] 262 | } else { 263 | // l.c('write: frame too large') 264 | ws.close(1009, 'frame too large')? 265 | return error('frame too large') 266 | } 267 | } 268 | mut frame_buf := []byte{} 269 | frame_buf << header 270 | frame_buf << bytes 271 | if !ws.is_server { 272 | for i in 0 .. payload_len { 273 | frame_buf[header_len + i] ^= masking_key[i % 4] & 0xff 274 | } 275 | } 276 | ws.socket_write(frame_buf)? 277 | } 278 | 279 | // close, closes the websocket connection 280 | pub fn (mut ws Client) close(code int, message string) ? { 281 | ws.debug_log('sending close, $code, $message') 282 | if ws.state in [.closed, .closing] || ws.conn.sock.handle <= 1 { 283 | ws.debug_log('close: Websocket allready closed ($ws.state), $message, $code handle($ws.conn.sock.handle)') 284 | return error('Socket allready closed: $code') 285 | } 286 | defer { 287 | ws.shutdown_socket() 288 | } 289 | defer { 290 | ws.reset_state() 291 | } 292 | ws.set_state(.closing) 293 | mut code32 := 0 294 | if code > 0 { 295 | code_ := C.htons(code) 296 | message_len := message.len + 2 297 | mut close_frame := [`0`].repeat(message_len) 298 | close_frame[0] = byte(code_ & 0xFF) 299 | close_frame[1] = byte(code_ >> 8) 300 | code32 = (close_frame[0] << 8) + close_frame[1] 301 | for i in 0 .. message.len { 302 | close_frame[i + 2] = message[i] 303 | } 304 | ws.send_control_frame(.close, 'CLOSE', close_frame)? 305 | ws.send_close_event(code, message) 306 | } else { 307 | ws.send_control_frame(.close, 'CLOSE', [])? 308 | ws.send_close_event(code, '') 309 | } 310 | ws.fragments = [] 311 | } 312 | 313 | // send_control_frame, sends a control frame to the server 314 | fn (mut ws Client) send_control_frame(code OPCode, frame_typ string, payload []byte) ? { 315 | ws.debug_log('send control frame $code, frame_type: $frame_typ, payload: $payload') 316 | if ws.state !in [.open, .closing] && ws.conn.sock.handle > 1 { 317 | return error('socket is not connected') 318 | } 319 | header_len := if ws.is_server { 2 } else { 6 } 320 | frame_len := header_len + payload.len 321 | mut control_frame := [`0`].repeat(frame_len) 322 | mut masking_key := []byte{} 323 | control_frame[0] = byte(int(code) | 0x80) 324 | if !ws.is_server { 325 | masking_key = create_masking_key() 326 | control_frame[1] = byte(payload.len | 0x80) 327 | control_frame[2] = masking_key[0] 328 | control_frame[3] = masking_key[1] 329 | control_frame[4] = masking_key[2] 330 | control_frame[5] = masking_key[3] 331 | } else { 332 | control_frame[1] = byte(payload.len) 333 | } 334 | if code == .close { 335 | if payload.len >= 2 { 336 | if !ws.is_server { 337 | mut parsed_payload := [`0`].repeat(payload.len + 1) 338 | unsafe { 339 | C.memcpy(parsed_payload.data, &payload[0], payload.len) 340 | } 341 | parsed_payload[payload.len] = `\0` 342 | for i in 0 .. payload.len { 343 | control_frame[6 + i] = (parsed_payload[i] ^ masking_key[i % 4]) & 0xff 344 | } 345 | } else { 346 | unsafe { 347 | C.memcpy(&control_frame[2], &payload[0], payload.len) 348 | } 349 | } 350 | } 351 | } else { 352 | if !ws.is_server { 353 | for i in 0 .. payload.len { 354 | control_frame[header_len + i] = (payload[i] ^ masking_key[i % 4]) & 0xff 355 | } 356 | } else { 357 | if payload.len > 0 { 358 | unsafe { 359 | C.memcpy(&control_frame[2], &payload[0], payload.len) 360 | } 361 | } 362 | } 363 | } 364 | ws.socket_write(control_frame) or { 365 | return error('send_control_frame: error sending $frame_typ control frame.') 366 | } 367 | } 368 | 369 | // parse_uri, parses the url string to it's components 370 | // todo: support not using port to default ones 371 | fn parse_uri(url string) ?&Uri { 372 | u := urllib.parse(url) or { 373 | return error(err) 374 | } 375 | v := u.request_uri().split('?') 376 | querystring := if v.len > 1 { '?' + v[1] } else { '' } 377 | return &Uri{ 378 | url: url 379 | hostname: u.hostname() 380 | port: u.port() 381 | resource: v[0] 382 | querystring: querystring 383 | } 384 | } 385 | 386 | [inline] 387 | // set_state sets current state in a thread safe way 388 | fn (mut ws Client) set_state(state State) { 389 | lock { 390 | ws.state = state 391 | } 392 | } 393 | 394 | [inline] 395 | fn (ws Client) assert_not_connected() ? { 396 | match ws.state { 397 | .connecting { return error('connect: websocket is connecting') } 398 | .open { return error('connect: websocket already open') } 399 | else {} 400 | } 401 | } 402 | 403 | [inline] 404 | // reset_state, resets the websocket and can connect again 405 | fn (mut ws Client) reset_state() { 406 | lock { 407 | ws.state = .closed 408 | ws.ssl_conn = new_ssl_conn() 409 | ws.flags = [] 410 | ws.fragments = [] 411 | } 412 | } 413 | 414 | fn (mut ws Client) debug_log(text string) { 415 | if ws.is_server { 416 | ws.logger.debug('server-> $text') 417 | } else { 418 | ws.logger.debug('client-> $text') 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /websocket/websocket_client_test.v: -------------------------------------------------------------------------------- 1 | import websocket 2 | import time 3 | 4 | // Tests with external ws & wss servers 5 | fn test_ws() ? { 6 | ws_test('ws://echo.websocket.org')? 7 | ws_test('wss://echo.websocket.org')? 8 | } 9 | 10 | fn ws_test(uri string) ? { 11 | println('connecting to $uri ...') 12 | mut ws := websocket.new_client(uri)? 13 | ws.on_open(fn (mut ws websocket.Client) ? { 14 | println('open!') 15 | ws.pong() 16 | assert true 17 | }) 18 | ws.on_error(fn (mut ws websocket.Client, err string) ? { 19 | println('error: $err') 20 | // this can be thrown by internet connection problems 21 | assert false 22 | }) 23 | ws.on_close(fn (mut ws websocket.Client, code int, reason string) ? { 24 | println('closed') 25 | }) 26 | ws.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ? { 27 | println('client got type: $msg.opcode payload:\n$msg.payload') 28 | if msg.opcode == .text_frame { 29 | println('Message: ${string(msg.payload, msg.payload.len)}') 30 | assert string(msg.payload, msg.payload.len) == 'a' 31 | } else { 32 | println('Binary message: $msg') 33 | } 34 | }) 35 | ws.connect() 36 | go ws.listen() 37 | text := ['a'].repeat(2) 38 | for msg in text { 39 | ws.write(msg.bytes(), .text_frame)? 40 | // sleep to give time to recieve response before send a new one 41 | time.sleep_ms(100) 42 | } 43 | // sleep to give time to recieve response before asserts 44 | time.sleep_ms(500) 45 | } 46 | -------------------------------------------------------------------------------- /websocket/websocket_nix.c.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | fn error_code() int { 4 | return C.errno 5 | } 6 | 7 | const ( 8 | error_ewouldblock = C.EWOULDBLOCK 9 | ) 10 | -------------------------------------------------------------------------------- /websocket/websocket_server.v: -------------------------------------------------------------------------------- 1 | // The module websocket implements the websocket server capabilities 2 | module websocket 3 | 4 | import emily33901.net 5 | import log 6 | import sync 7 | import time 8 | import rand 9 | 10 | pub struct Server { 11 | mut: 12 | clients map[string]&ServerClient 13 | logger &log.Log 14 | ls net.TcpListener 15 | accept_client_callbacks []AcceptClientFn 16 | message_callbacks []MessageEventHandler 17 | close_callbacks []CloseEventHandler 18 | pub: 19 | port int 20 | is_ssl bool = false 21 | pub mut: 22 | // If it's set to 0, it don't handle the ping sending to the clients 23 | ping_interval int = 30 // in seconds 24 | state State 25 | } 26 | 27 | struct ServerClient { 28 | pub: 29 | resource_name string 30 | client_key string 31 | pub mut: 32 | server &Server 33 | client &Client 34 | } 35 | 36 | pub fn new_server(port int, route string) &Server { 37 | return &Server{ 38 | port: port 39 | logger: &log.Log{ 40 | level: .info 41 | } 42 | state: .closed 43 | } 44 | } 45 | 46 | pub fn (mut s Server) set_ping_interval(seconds int) { 47 | s.ping_interval = seconds 48 | } 49 | 50 | pub fn (mut s Server) listen() ? { 51 | s.logger.info('websocket server: start listen on port $s.port') 52 | s.ls = net.listen_tcp(s.port)? 53 | s.set_state(.open) 54 | if s.ping_interval == 0 { 55 | go s.handle_ping() 56 | } 57 | for { 58 | c := s.accept_new_client() or { 59 | continue 60 | } 61 | go s.serve_client(mut c) 62 | } 63 | s.logger.info('websocket server: end listen on port $s.port') 64 | } 65 | 66 | fn (mut s Server) close() { 67 | } 68 | 69 | fn (mut s Server) handle_ping() { 70 | for s.state == .open { 71 | time.sleep(s.ping_interval) 72 | s.priv_send_ping() 73 | } 74 | } 75 | 76 | pub fn (mut s Server) send_ping()? { 77 | if s.ping_interval != 0 { 78 | s.priv_send_ping() 79 | } else { 80 | return error("WS Server it's handling ping sending") 81 | } 82 | } 83 | 84 | // Todo: make thread safe 85 | fn (mut s Server) priv_send_ping() { 86 | mut clients_to_remove := []string{} 87 | for _, cli in s.clients { 88 | mut c := cli 89 | if c.client.state == .open { 90 | c.client.ping() or { 91 | s.logger.debug('server-> error sending ping to client') 92 | // todo fix better close message, search the standard 93 | c.client.close(1002, 'Clossing connection: ping send error') or { 94 | // we want to continue even if error 95 | continue 96 | } 97 | clients_to_remove << c.client.id 98 | } 99 | if (time.now().unix - c.client.last_pong_ut) > s.ping_interval * 2 { 100 | clients_to_remove << c.client.id 101 | c.client.close(1000, 'no pong received') or { 102 | continue 103 | } 104 | } 105 | } 106 | } 107 | for client in clients_to_remove { 108 | lock { 109 | s.clients.delete(client) 110 | } 111 | } 112 | clients_to_remove.clear() 113 | } 114 | 115 | fn (mut s Server) serve_client(mut c Client) ? { 116 | c.logger.debug('server-> Start serve client ($c.id)') 117 | defer { 118 | c.logger.debug('server-> End serve client ($c.id)') 119 | } 120 | handshake_response, server_client := s.handle_server_handshake(mut c)? 121 | accept := s.send_connect_event(mut server_client)? 122 | if !accept { 123 | s.logger.debug('server-> client not accepted') 124 | c.shutdown_socket()? 125 | return 126 | } 127 | // The client is accepted 128 | c.socket_write(handshake_response.bytes())? 129 | lock { 130 | s.clients[server_client.client.id] = server_client 131 | } 132 | s.setup_callbacks(mut server_client) 133 | c.listen() or { 134 | s.logger.error(err) 135 | return error(err) 136 | } 137 | } 138 | 139 | fn (mut s Server) setup_callbacks(mut sc ServerClient) { 140 | if s.message_callbacks.len > 0 { 141 | for cb in s.message_callbacks { 142 | if cb.is_ref { 143 | sc.client.on_message_ref(cb.handler2, cb.ref) 144 | } else { 145 | sc.client.on_message(cb.handler) 146 | } 147 | } 148 | } 149 | if s.close_callbacks.len > 0 { 150 | for cb in s.close_callbacks { 151 | if cb.is_ref { 152 | sc.client.on_close_ref(cb.handler2, cb.ref) 153 | } else { 154 | sc.client.on_close(cb.handler) 155 | } 156 | } 157 | } 158 | // Set standard close so we can remove client if closed 159 | sc.client.on_close_ref(fn (mut c Client, code int, reason string, mut sc ServerClient) ? { 160 | c.logger.debug('server-> Delete client') 161 | lock { 162 | sc.server.clients.delete(sc.client.id) 163 | } 164 | }, mut sc) 165 | } 166 | 167 | fn (mut s Server) accept_new_client() ?&Client { 168 | mut new_conn := s.ls.accept()? 169 | c := &Client{ 170 | is_server: true 171 | conn: new_conn 172 | ssl_conn: new_ssl_conn() 173 | logger: s.logger 174 | state: .open 175 | last_pong_ut: time.now().unix 176 | id: rand.uuid_v4() 177 | } 178 | return c 179 | } 180 | 181 | // set_state sets current state in a thread safe way 182 | [inline] 183 | fn (mut s Server) set_state(state State) { 184 | lock { 185 | s.state = state 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /websocket/websocket_windows.c.v: -------------------------------------------------------------------------------- 1 | module websocket 2 | 3 | import emily33901.net 4 | 5 | fn error_code() int { 6 | return C.WSAGetLastError() 7 | } 8 | 9 | const ( 10 | error_ewouldblock = net.WsaError.wsaewouldblock 11 | ) 12 | --------------------------------------------------------------------------------