├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bundle.json ├── corral.json ├── examples ├── broadcast │ └── main.pony ├── echo-server │ └── main.pony ├── request-headers │ └── main.pony ├── simple-echo │ └── main.pony └── ssl-echo │ └── main.pony ├── reports └── .gitkeep ├── tests ├── fuzzingclient.json └── fuzzingserver.json └── websocket ├── _frame_decoder.pony ├── _http_parser.pony ├── connection.pony ├── connection_notify.pony ├── frame.pony ├── handshake.pony ├── listen_notify.pony ├── listener.pony ├── simple_server.pony └── utf8.pony /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | vs-ponyc-release: 5 | docker: 6 | - image: ponylang/ponyc:0.49.1 7 | steps: 8 | - checkout 9 | - run: apt-get update 10 | - run: apt-get install -y libssl-dev 11 | - run: make examples 12 | 13 | workflows: 14 | version: 2.1 15 | 16 | commit: 17 | jobs: 18 | - vs-ponyc-release 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | .vscode 3 | reports 4 | websocket-docs 5 | .deps 6 | _corral 7 | _repos 8 | /lock.json 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Oraoto 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PONYC ?= ponyc 2 | PONYC_FLAGS ?=--checktree --verify 3 | config ?= release 4 | 5 | BUILD_DIR ?= build/$(config) 6 | SRC_DIR ?= websocket 7 | EXAMPLES_DIR ?= examples 8 | 9 | SOURCE_FILES := $(shell find $(SRC_DIR) -name \*.pony) 10 | EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name \*.pony) 11 | 12 | OPENSSL_VERSION ?= openssl_1.1.x 13 | 14 | ifdef config 15 | ifeq (,$(filter $(config),debug release)) 16 | $(error Unknown configuration "$(config)") 17 | endif 18 | endif 19 | 20 | ifeq ($(config),debug) 21 | PONYC_FLAGS += --debug 22 | endif 23 | 24 | 25 | $(BUILD_DIR): 26 | mkdir -p $(BUILD_DIR) 27 | 28 | examples: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) 29 | corral fetch 30 | corral run -- $(PONYC) -D$(OPENSSL_VERSION) --path=. $(EXAMPLES_DIR)/broadcast -o $(BUILD_DIR) $(PONYC_FLAGS) 31 | corral run -- $(PONYC) -D$(OPENSSL_VERSION) --path=. $(EXAMPLES_DIR)/echo-server -o $(BUILD_DIR) $(PONYC_FLAGS) 32 | corral run -- $(PONYC) -D$(OPENSSL_VERSION) --path=. $(EXAMPLES_DIR)/simple-echo -o $(BUILD_DIR) $(PONYC_FLAGS) 33 | corral run -- $(PONYC) -D$(OPENSSL_VERSION) --path=. $(EXAMPLES_DIR)/ssl-echo -o $(BUILD_DIR) $(PONYC_FLAGS) 34 | 35 | clean: 36 | rm -rf $(BUILD_DIR) .coverage 37 | 38 | test: 39 | docker run -it --rm \ 40 | -v ${PWD}/tests:/config \ 41 | -v ${PWD}/reports:/reports \ 42 | --network host \ 43 | --name fuzzingclient \ 44 | crossbario/autobahn-testsuite \ 45 | /opt/pypy/bin/wstest --mode fuzzingclient --spec /config/fuzzingclient.json 46 | 47 | .PHONY: clean examples test 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pony-websocket 2 | 3 | [![CircleCI](https://img.shields.io/circleci/project/github/oraoto/pony-websocket.svg)](https://circleci.com/gh/oraoto/pony-websocket/tree/master) 4 | ![stability](https://img.shields.io/badge/stability-experimental-red.svg) 5 | 6 | WebSocket server for pony 7 | 8 | It's RFC6455 conformant, see [test report](https://oraoto.github.io/pony-websocket/). 9 | 10 | ## Installation 11 | 12 | * Install [corral](https://github.com/ponylang/corral) 13 | * `corral add github github.com/ponylang/net_ssl.git` 14 | * `corral fetch` to fetch your dependencies 15 | * `use "websocket"` to include this package 16 | * `corral run -- ponyc` to compile your applicatpion 17 | 18 | ## Select OpenSSL Version 19 | 20 | You must select an SSL version to use. 21 | 22 | Using OpenSSL 0.9.0 23 | 24 | ``` 25 | corral run -- ponyc -Dopenssl_0.9.0 26 | ``` 27 | 28 | Using OpenSSL 1.1.x: 29 | 30 | ``` 31 | corral run -- ponyc -Dopenssl_1.1.x 32 | ``` 33 | 34 | ## Usage 35 | 36 | The API is model after the [net](https://stdlib.ponylang.org/net--index) package and much simplified. 37 | 38 | Here is a simple echo server: 39 | 40 | ```pony 41 | use "websocket" 42 | 43 | actor Main 44 | new create(env: Env) => 45 | try 46 | let listener = WebSocketListener( 47 | env.root as AmbientAuth, EchoListenNotify, "127.0.0.1","8989") 48 | end 49 | 50 | class EchoListenNotify is WebSocketListenNotify 51 | // A tcp connection connected, return a WebsocketConnectionNotify instance 52 | fun ref connected(): EchoConnectionNotify iso^ => 53 | EchoConnectionNotify 54 | 55 | class EchoConnectionNotify is WebSocketConnectionNotify 56 | // A websocket connection enters the OPEN state 57 | fun ref opened(conn: WebSocketConnection ref) => 58 | @printf[I32]("New client connected\n".cstring()) 59 | 60 | // UTF-8 text data received 61 | fun ref text_received(conn: WebSocketConnection ref, text: String) => 62 | // Send the text back 63 | conn.send_text(text) 64 | 65 | // Binary data received 66 | fun ref binary_received(conn: WebSocketConnection ref, data: Array[U8] val) => 67 | conn.send_binary(data) 68 | 69 | // A websocket connection enters the CLOSED state 70 | fun ref closed(conn: WebSocketConnection ref) => 71 | @printf[I32]("Connection closed\n".cstring()) 72 | ``` 73 | 74 | An simplified API is also provided: [example](./examples/simple-echo/main.pony). 75 | 76 | See more [examples](./examples). 77 | -------------------------------------------------------------------------------- /bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "deps": [ 3 | { 4 | "type": "github", 5 | "repo": "ponylang/net-ssl" 6 | }, 7 | { 8 | "type": "github", 9 | "repo": "ponylang/crypto" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /corral.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [], 3 | "deps": [ 4 | { 5 | "locator": "github.com/ponylang/net_ssl.git", 6 | "version": "main" 7 | }, 8 | { 9 | "locator": "github.com/ponylang/crypto.git", 10 | "version": "main" 11 | } 12 | ], 13 | "info": { 14 | "description": "", 15 | "homepage": "", 16 | "license": "", 17 | "documentation_url": "", 18 | "version": "", 19 | "name": "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/broadcast/main.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "package:../../websocket" 3 | use "collections" 4 | use @printf[I32](fmt: Pointer[U8] tag, ...) 5 | 6 | actor Main 7 | new create(env: Env) => 8 | env.out.print("Start server") 9 | let tcplauth: TCPListenAuth = TCPListenAuth(env.root) 10 | 11 | let listener = WebSocketListener(tcplauth, BroadcastListenNotify, 12 | "127.0.0.1","8989") 13 | 14 | actor ConnectionManager 15 | var _connections: SetIs[WebSocketConnection] = 16 | SetIs[WebSocketConnection].create() 17 | 18 | be add(conn: WebSocketConnection) => 19 | @printf("Add connection\n".cstring()) 20 | _connections.set(conn) 21 | 22 | be remove(conn: WebSocketConnection) => 23 | @printf("Remove connection\n".cstring()) 24 | _connections.unset(conn) 25 | 26 | be broadcast_text(text: String) => 27 | for c in _connections.values() do 28 | c.send_text_be(text) 29 | end 30 | 31 | be broadcast_binary(data: Array[U8] val) => 32 | for c in _connections.values() do 33 | c.send_binary_be(data) 34 | end 35 | 36 | class BroadcastListenNotify is WebSocketListenNotify 37 | var _conn_manager: ConnectionManager = ConnectionManager.create() 38 | 39 | fun ref connected(): BroadcastConnectionNotify iso^ => 40 | BroadcastConnectionNotify(_conn_manager) 41 | 42 | fun ref not_listening() => 43 | @printf("Failed listening\n".cstring()) 44 | 45 | class BroadcastConnectionNotify is WebSocketConnectionNotify 46 | var _conn_manager: ConnectionManager 47 | 48 | new iso create(conn_manager: ConnectionManager) => 49 | _conn_manager = conn_manager 50 | 51 | fun ref opened(conn: WebSocketConnection tag) => 52 | _conn_manager.add(conn) 53 | 54 | fun ref text_received(conn: WebSocketConnection tag, text: String) => 55 | _conn_manager.broadcast_text(text) 56 | 57 | fun ref binary_received(conn: WebSocketConnection tag, data: Array[U8] val) => 58 | _conn_manager.broadcast_binary(data) 59 | 60 | fun ref closed(conn: WebSocketConnection tag) => 61 | _conn_manager.remove(conn) 62 | -------------------------------------------------------------------------------- /examples/echo-server/main.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "package:../../websocket" 3 | use @printf[I32](fmt: Pointer[U8] tag, ...) 4 | 5 | actor Main 6 | new create(env: Env) => 7 | env.out.print("Start server") 8 | let tcplauth: TCPListenAuth = TCPListenAuth(env.root) 9 | 10 | let listener = WebSocketListener(tcplauth, 11 | EchoListenNotify, "0.0.0.0", "8989", 0, 16777216 + 4) 12 | 13 | class EchoListenNotify is WebSocketListenNotify 14 | // A tcp connection connected, return a WebsocketConnectionNotify instance 15 | fun ref connected(): EchoConnectionNotify iso^ => 16 | EchoConnectionNotify 17 | 18 | fun ref not_listening() => 19 | @printf("Failed listening\n".cstring()) 20 | 21 | class EchoConnectionNotify is WebSocketConnectionNotify 22 | // A websocket connection enters the OPEN state 23 | fun ref opened(conn: WebSocketConnection ref) => 24 | @printf("New client connected\n".cstring()) 25 | 26 | // UTF-8 text data received 27 | fun ref text_received(conn: WebSocketConnection ref, text: String) => 28 | // Send the text back 29 | conn.send_text(text) 30 | 31 | // Binary data received 32 | fun ref binary_received(conn: WebSocketConnection ref, data: Array[U8] val) => 33 | conn.send_binary(data) 34 | 35 | // A websocket connection enters the CLOSED state 36 | fun ref closed(conn: WebSocketConnection ref) => 37 | @printf("Connection closed\n".cstring()) 38 | -------------------------------------------------------------------------------- /examples/request-headers/main.pony: -------------------------------------------------------------------------------- 1 | use "package:../../websocket" 2 | 3 | actor Main 4 | new create(env: Env) => 5 | env.out.print("Listening on port 8989") 6 | try 7 | WebSocketListener( 8 | env.root as AmbientAuth, 9 | MyListenNotify(env), 10 | "0.0.0.0", 11 | "8989") 12 | end 13 | 14 | class MyListenNotify is WebSocketListenNotify 15 | let env: Env 16 | 17 | new iso create(env': Env) => 18 | env = env' 19 | 20 | fun ref connected(): MyConnectionNotify iso^ => 21 | MyConnectionNotify(env) 22 | 23 | fun ref not_listening() => 24 | env.out.print("Not listening") 25 | 26 | class MyConnectionNotify is WebSocketConnectionNotify 27 | let env: Env 28 | 29 | new iso create(env': Env) => 30 | env = env' 31 | 32 | fun ref opened(conn: WebSocketConnection ref) => 33 | env.out.print("Connection opened, sending request headers") 34 | for (k, v) in conn.request.headers.pairs() do 35 | conn.send_text(k + ": " + v) 36 | end 37 | 38 | fun ref closed(conn: WebSocketConnection ref) => 39 | env.out.print("Connection closed") 40 | -------------------------------------------------------------------------------- /examples/simple-echo/main.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "package:../../websocket" 3 | 4 | use @printf[I32](fmt: Pointer[U8] tag, ...) 5 | 6 | actor Main 7 | new create(env: Env) => 8 | let tcplauth: TCPListenAuth = TCPListenAuth(env.root) 9 | let listener = WebSocketListener(tcplauth, 10 | recover SimpleServer(EchoWebSocketNotify) end, 11 | "127.0.0.1","8989") 12 | 13 | actor EchoWebSocketNotify is SimpleWebSocketNotify 14 | 15 | be opened(conn: WebSocketConnection tag) => 16 | @printf("New client connected\n".cstring()) 17 | 18 | be text_received(conn: WebSocketConnection tag, text: String) => 19 | conn.send_text_be(text) 20 | 21 | be binary_received(conn: WebSocketConnection tag, data: Array[U8] val) => 22 | conn.send_binary_be(data) 23 | 24 | be closed(conn: WebSocketConnection tag) => 25 | @printf("Connection closed\n".cstring()) 26 | -------------------------------------------------------------------------------- /examples/ssl-echo/main.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "package:../../websocket" 3 | use "net_ssl" 4 | use "files" 5 | 6 | use @printf[I32](fmt: Pointer[U8] tag, ...) 7 | 8 | // Put your test cert.pem and key.pem in this directory 9 | 10 | actor Main 11 | new create(env: Env) => 12 | env.out.print("Start server") 13 | let tcplauth: TCPListenAuth = TCPListenAuth(env.root) 14 | let fileauth: FileAuth = FileAuth(env.root) 15 | 16 | try 17 | let ssl_context: SSLContext val = recover 18 | SSLContext 19 | .>set_cert( 20 | FilePath(fileauth, "./cert.pem"), 21 | FilePath(fileauth, "./key.pem"))? 22 | end 23 | let listener = WebSocketListener(tcplauth, EchoListenNotify, 24 | "0.0.0.0", "8989", 0, 16384, 16384, 16384, ssl_context) 25 | else 26 | env.out.print("Failed to start server") 27 | end 28 | 29 | // Same as echo-server 30 | class EchoListenNotify is WebSocketListenNotify 31 | fun ref connected(): EchoConnectionNotify iso^ => 32 | @printf("Connected\n".cstring()) 33 | EchoConnectionNotify 34 | 35 | fun ref not_listening() => 36 | @printf("Failed listening\n".cstring()) 37 | 38 | class EchoConnectionNotify is WebSocketConnectionNotify 39 | fun ref opened(conn: WebSocketConnection ref) => 40 | @printf("New client connected\n".cstring()) 41 | 42 | fun ref text_received(conn: WebSocketConnection ref, text: String) => 43 | conn.send_text(text) 44 | 45 | fun ref binary_received(conn: WebSocketConnection ref, data: Array[U8] val) => 46 | conn.send_binary(data) 47 | 48 | fun ref closed(conn: WebSocketConnection ref) => 49 | @printf("Connection closed\n".cstring()) 50 | -------------------------------------------------------------------------------- /reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oraoto/pony-websocket/f725eb13e508bae32ca735dd4fa2f6000c51f6ab/reports/.gitkeep -------------------------------------------------------------------------------- /tests/fuzzingclient.json: -------------------------------------------------------------------------------- 1 | { 2 | "outdir": "./reports/servers", 3 | 4 | "servers": [{ 5 | "agent": "pony-websocket", 6 | "url": "ws://127.0.0.1:8989" 7 | }], 8 | 9 | "cases": [ 10 | "*" 11 | ], 12 | "exclude-cases": [ 13 | "12.*", "13.*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/fuzzingserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "ws://127.0.0.1:8989", 3 | "outdir": "./reports/clients", 4 | "cases": ["*"], 5 | "exclude-cases": [], 6 | "exclude-agent-cases": {} 7 | } -------------------------------------------------------------------------------- /websocket/_frame_decoder.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | 3 | primitive _ExpectHeader 4 | primitive _ExpectExtendedPayloadLen16 5 | primitive _ExpectExtendedPayloadLen64 6 | primitive _ExpectMaskKeyAndPayload 7 | 8 | type _DecodeState is ( 9 | _ExpectHeader 10 | | _ExpectExtendedPayloadLen16 11 | | _ExpectExtendedPayloadLen64 12 | | _ExpectMaskKeyAndPayload) 13 | 14 | class _FrameDecoder 15 | var prev_opcode: Opcode = Text 16 | var opcode : Opcode = Text 17 | var state: _DecodeState = _ExpectHeader 18 | var status: U16 = 1000 19 | var masked: Bool = true 20 | var mask_key: Array[U8] = mask_key.create(4) 21 | var _expect: USize = 2 22 | var _payload_len: USize = 0 23 | var fin: Bool = true 24 | var fragment_started: Bool = false 25 | var fragment: Writer = Writer // fragment buffer 26 | 27 | // USize: expect more data 28 | // Frame: an parsed frame 29 | fun ref decode(buffer: Reader): (USize | Frame val)? => 30 | match state 31 | | _ExpectHeader => _parse_header(buffer)? 32 | | _ExpectExtendedPayloadLen16 => _parse_extended_16(buffer)? 33 | | _ExpectExtendedPayloadLen64 => _parse_extended_64(buffer)? 34 | | _ExpectMaskKeyAndPayload => _parse_payload(buffer)? 35 | end 36 | 37 | fun ref _parse_payload(buffer: Reader): (USize| Frame val)? => 38 | if masked then 39 | mask_key = buffer.block(4)? 40 | end 41 | var payload: Array[U8 val] iso = buffer.block(_payload_len)? 42 | state = _ExpectHeader // expect next header 43 | 44 | if masked then 45 | payload = _unmask(consume payload) 46 | end 47 | 48 | if not _is_control(opcode) then 49 | if fragment_started and (not fin) then 50 | fragment.write(consume payload) 51 | return 2 // next header 52 | else 53 | // frame completed 54 | if fragment.size() > 0 then 55 | let size = payload.size() + fragment.size() 56 | let fragment_data: Array[ByteSeq] iso = fragment.done() 57 | payload = _concat_fragment(consume fragment_data, consume payload, size) 58 | end 59 | if opcode is Text then 60 | payload = validate_utf8(consume payload , 0)? 61 | end 62 | status = 1000 63 | fragment_started = false 64 | end 65 | end 66 | 67 | match opcode 68 | | Text => Frame.text(String.from_array(consume payload)) 69 | | Binary => Frame.binary(consume payload) 70 | | Ping => 71 | opcode = prev_opcode 72 | Frame.ping(consume payload) 73 | | Pong => 74 | opcode = prev_opcode 75 | Frame.pong(consume payload) 76 | | Continuation => 2 // expect next frame 77 | | Close => 78 | if payload.size() >= 2 then 79 | payload = validate_utf8(consume payload, 2)? 80 | let code = try U16.from[U8](payload(0)?).shl(8) + U16.from[U8](payload(1)?) else 1000 end 81 | if (code < 1000) or ((code >= 1004) and (code <= 1006)) or ((code >= 1014) and (code <= 2999)) or (code > 4999) then 82 | _throw[Frame val](1002)? 83 | else 84 | Frame.close(code) 85 | end 86 | else 87 | Frame.close(1000) 88 | end 89 | end 90 | 91 | fun ref validate_utf8(data: Array[U8 val] iso, idx: USize): Array[U8 val] iso^? => 92 | try 93 | UTF8.validate(consume data, idx)? 94 | else 95 | status = 1007 96 | error 97 | end 98 | 99 | fun ref _concat_fragment(fragment_data: Array[ByteSeq] iso, payload: Array[U8 val] iso, size: USize): Array[U8 val] iso^ => 100 | recover 101 | let new_p = Array[U8].create(size) 102 | var i: USize = 0 103 | // var c: USize = 0 104 | for f in (consume fragment_data).values() do 105 | match f 106 | | let u: Array[U8] val => 107 | let s = u.size() 108 | u.copy_to(new_p, 0, i, s) 109 | i = i + s 110 | // c = c + 1 111 | end 112 | end 113 | let p: Array[U8 val] val = consume payload 114 | let s = p.size() 115 | p.copy_to(new_p, 0, i, s) 116 | new_p 117 | end 118 | 119 | fun ref _parse_header(buffer: Reader): (USize | Frame val)? => 120 | status = 1000 121 | 122 | let first_byte = buffer.u8()? 123 | fin = first_byte.shr(7) == 0b1 124 | 125 | // RSV must be 0 126 | let rsv = (first_byte and 0b0111_0000).shr(4) 127 | if rsv != 0 then _throw[None](1002)? end 128 | 129 | let current_op = match first_byte and 0b00001111 130 | | Continuation() => Continuation 131 | | Text() => Text 132 | | Binary() => Binary 133 | | Ping() => Ping 134 | | Pong() => Pong 135 | | Close() => Close 136 | | let other: U8 => _throw[Opcode](1002)? 137 | end 138 | 139 | // Save prev_opcode, recover in _parse_payload 140 | if _is_pingpong(current_op) then 141 | prev_opcode = opcode 142 | opcode = current_op 143 | end 144 | 145 | // A FIN + Continuation when we're not doing fragmentation 146 | if fin and (current_op is Continuation) and (not fragment_started) then 147 | _throw[None](1002)? 148 | end 149 | 150 | if fin and (not (current_op is Continuation)) then // FIN = 1 & OP != 0 151 | if fragment_started and (not _is_control(current_op)) then 152 | _throw[None](1002)? 153 | else 154 | opcode = current_op 155 | end 156 | end 157 | 158 | if not fin then 159 | if _is_control(current_op) then 160 | // control frame must be FIN 161 | _throw[None](1001)? 162 | elseif (not fragment_started) and (current_op is Continuation) then 163 | // Continuation when not fragmented 164 | _throw[None](1001)? 165 | elseif not fragment_started then 166 | opcode = current_op 167 | fragment_started = true 168 | end 169 | end 170 | 171 | let second_byte = buffer.u8()? 172 | masked = second_byte.shr(7) == 0b1 173 | let payload_len = second_byte and 0b01111111 174 | 175 | // validate control op and payload len 176 | if _is_control(current_op) then 177 | if payload_len > 125 then _throw[None](1002)? end 178 | end 179 | if (current_op is Close) and (payload_len == 1) then 180 | _throw[None](1002)? 181 | end 182 | 183 | // set state and return expect bytes 184 | if (payload_len == 0) and (not masked) then 185 | state = _ExpectHeader 186 | return match opcode 187 | | Text => Frame.text("") 188 | | Binary => Frame.binary([]) 189 | | Ping => Frame.ping([]) 190 | | Ping => Frame.pong([]) 191 | else 192 | // Close should always have a payload of at least 2 bytes. 193 | // Continuation should also have a non-zero payload 194 | _throw[Frame val](1002)? 195 | end 196 | elseif payload_len == 126 then 197 | state = _ExpectExtendedPayloadLen16 198 | return 2 199 | elseif payload_len == 127 then 200 | state = _ExpectExtendedPayloadLen64 201 | return 8 202 | else 203 | state = _ExpectMaskKeyAndPayload 204 | _payload_len = USize.from[U8](payload_len) 205 | _expect = _payload_len + if masked then 4 else 0 end 206 | return _expect 207 | end 208 | 209 | fun ref _parse_extended_16(buffer: Reader): USize? => 210 | let payload_len = buffer.u16_be()? 211 | state = _ExpectMaskKeyAndPayload 212 | _payload_len = USize.from[U16](payload_len) 213 | _payload_len + if masked then 4 else 0 end 214 | 215 | fun ref _parse_extended_64(buffer: Reader): USize? => 216 | let payload_len = buffer.u64_be()? 217 | state = _ExpectMaskKeyAndPayload 218 | _payload_len = USize.from[U64](payload_len) 219 | _payload_len + if masked then 4 else 0 end 220 | 221 | fun _unmask(payload: Array[U8 val] iso): Array[U8 val] iso^ => 222 | let p = consume payload 223 | let size = p.size() 224 | var i: USize = 0 225 | try 226 | let m1 = mask_key(0)? 227 | let m2 = mask_key(1)? 228 | let m3 = mask_key(2)? 229 | let m4 = mask_key(3)? 230 | while (i + 4) < size do 231 | p(i)? = p(i)? xor m1 232 | p(i + 1)? = p(i + 1)? xor m2 233 | p(i + 2)? = p(i + 2)? xor m3 234 | p(i + 3)? = p(i + 3)? xor m4 235 | i = i + 4 236 | end 237 | while i < size do 238 | p(i)? = p(i)? xor mask_key(i % 4)? 239 | i = i + 1 240 | end 241 | end 242 | p 243 | 244 | fun _is_control(op: Opcode) : Bool => 245 | (op is Ping) or (op is Pong) or (op is Close) 246 | 247 | fun _is_pingpong(op: Opcode): Bool => 248 | (op is Ping) or (op is Pong) 249 | 250 | fun ref _throw[A](s: U16): A? => 251 | status = s 252 | error 253 | -------------------------------------------------------------------------------- /websocket/_http_parser.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | use "collections" 3 | 4 | primitive _ExpectRequest 5 | primitive _ExpectHeaders 6 | primitive _ExpectError 7 | 8 | type _ParserState is (_ExpectRequest | _ExpectHeaders | _ExpectError) 9 | 10 | class _HttpParser 11 | """ 12 | A cutdown version of net/http/_http_parser, just parse request line and headers. 13 | """ 14 | 15 | var _request: HandshakeRequest trn = HandshakeRequest 16 | var _state: _ParserState = _ExpectRequest 17 | 18 | fun ref parse(buffer: Reader ref): (HandshakeRequest val | None) ? => 19 | """ 20 | Return a HandshakeRequest on success. 21 | Return None for more data. 22 | """ 23 | match _state 24 | | _ExpectRequest => _parse_request(buffer)? 25 | | _ExpectHeaders => _parse_headers(buffer)? 26 | end 27 | 28 | fun ref _parse_request(buffer: Reader): None ? => 29 | """ 30 | Parse request-line: " " 31 | """ 32 | try 33 | let line = buffer.line()? 34 | try 35 | let method_end = line.find(" ")? 36 | _request.method = line.substring(0, method_end) 37 | let url_end = line.find(" ", method_end + 1)? 38 | _request.resource = line.substring(method_end + 1, url_end) 39 | _state = _ExpectHeaders 40 | else 41 | _state = _ExpectError 42 | end 43 | else 44 | return None // expect more data for a line 45 | end 46 | 47 | if _state is _ExpectError then error end // Not a valid request-line 48 | 49 | fun ref _parse_headers(buffer: Reader): (HandshakeRequest val | None) ? => 50 | while true do 51 | try 52 | let line = buffer.line()? 53 | if line.size() == 0 then 54 | _state = _ExpectRequest 55 | return _request = HandshakeRequest // Finish parsing and reset 56 | else 57 | try 58 | _process_header(consume line)? 59 | else 60 | _state = _ExpectError 61 | break 62 | end 63 | end 64 | else 65 | return None 66 | end 67 | end 68 | 69 | if _state is _ExpectError then error end 70 | 71 | fun ref _process_header(line: String iso) ? => 72 | let i = line.find(":")? 73 | (let key, let value) = (consume line).chop(i.usize()) 74 | key.>strip().lower_in_place() 75 | value.>shift()?.strip() 76 | _request._set_header(consume key, consume value) 77 | -------------------------------------------------------------------------------- /websocket/connection.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | 3 | actor WebSocketConnection 4 | """ 5 | A wrapper around a TCP connection, provides data-sending functionality. 6 | """ 7 | 8 | let _notify: WebSocketConnectionNotify 9 | let _tcp: TCPConnection 10 | var _closed: Bool = false 11 | let request: HandshakeRequest val 12 | 13 | new create( 14 | tcp: TCPConnection, 15 | notify: WebSocketConnectionNotify iso, 16 | request': HandshakeRequest val) 17 | => 18 | _notify = consume notify 19 | _tcp = tcp 20 | request = request' 21 | _notify.opened(this) 22 | 23 | fun send_text(text: String val) => 24 | """ 25 | Send text data (without fragmentation), text must be encoded in utf-8. 26 | """ 27 | if not _closed then 28 | _tcp.writev(Frame.text(text).build()) 29 | end 30 | 31 | be send_text_be(text: String val) => 32 | send_text(text) 33 | 34 | fun send_binary(data: Array[U8] val) => 35 | """ 36 | Send binary data (without fragmentation) 37 | """ 38 | if not _closed then 39 | _tcp.writev(Frame.binary(data).build()) 40 | end 41 | 42 | be send_binary_be(data: Array[U8] val) => 43 | send_binary(data) 44 | 45 | fun ref close(code: U16 = 1000) => 46 | """ 47 | Initiate closure, all data sending is ignored after this call. 48 | """ 49 | if not _closed then 50 | _tcp.writev(Frame.close(code).build()) 51 | _closed = true 52 | end 53 | 54 | be close_be(code: U16 = 1000) => 55 | close(code) 56 | 57 | be _send_ping(data: Array[U8] val = []) => 58 | """ 59 | Send a ping frame. 60 | """ 61 | if not _closed then 62 | _tcp.writev(Frame.ping(data).build()) 63 | end 64 | 65 | be _send_pong(data: Array[U8] val) => 66 | """ 67 | Send a pong frame. 68 | """ 69 | if not _closed then 70 | _tcp.writev(Frame.pong(data).build()) 71 | end 72 | 73 | be _close(code: U16 = 100) => 74 | """ 75 | Send a close frame and close the TCP connection, all data sending is 76 | ignored after this call. 77 | On client-initiated closure, send a close frame and close the connection. 78 | On server-initiated closure, close the connection without sending another 79 | close frame. 80 | """ 81 | if not _closed then 82 | _tcp.writev(Frame.close(code).build()) 83 | _closed = true 84 | end 85 | _tcp.dispose() 86 | 87 | be _text_received(text: String) => 88 | _notify.text_received(this, text) 89 | 90 | be _binary_received(data: Array[U8] val) => 91 | _notify.binary_received(this, data) 92 | 93 | be _notify_closed() => 94 | _notify.closed(this) 95 | -------------------------------------------------------------------------------- /websocket/connection_notify.pony: -------------------------------------------------------------------------------- 1 | interface WebSocketConnectionNotify 2 | 3 | fun ref opened(conn: WebSocketConnection ref) => 4 | None 5 | 6 | fun ref closed(conn: WebSocketConnection ref) => 7 | None 8 | 9 | fun ref text_received(conn: WebSocketConnection ref, text: String): None => 10 | None 11 | 12 | fun ref binary_received( 13 | conn: WebSocketConnection ref, 14 | data: Array[U8 val] val): None => None 15 | -------------------------------------------------------------------------------- /websocket/frame.pony: -------------------------------------------------------------------------------- 1 | use "buffered" 2 | 3 | primitive Continuation fun apply(): U8 => 0x00 4 | primitive Text fun apply(): U8 => 0x01 5 | primitive Binary fun apply(): U8 => 0x02 6 | primitive Close fun apply(): U8 => 0x08 7 | primitive Ping fun apply(): U8 => 0x09 8 | primitive Pong fun apply(): U8 => 0x0A 9 | type Opcode is (Continuation | Text | Binary | Ping | Pong | Close) 10 | 11 | class trn Frame 12 | var opcode: Opcode 13 | 14 | // Unmasked data. 15 | var data: (String val | Array[U8 val] val) 16 | 17 | new iso create(opcode': Opcode, data': (String val | Array[U8 val] iso)) => 18 | opcode = opcode' 19 | data = consume data' 20 | 21 | // Create an Text frame 22 | new iso text(data': String = "") => 23 | opcode = Text 24 | data = consume data' 25 | 26 | // Create an Text frame 27 | new iso ping(data': Array[U8 val] val) => 28 | opcode = Ping 29 | data = data' 30 | 31 | new iso pong(data': Array[U8 val] val) => 32 | opcode = Pong 33 | data = data' 34 | 35 | new iso binary(data': Array[U8 val] val) => 36 | opcode = Binary 37 | data = data' 38 | 39 | new iso close(code: U16 = 1000) => 40 | opcode = Close 41 | data = [U8.from[U16](code.shr(8)); U8.from[U16](code and 0xFF)] 42 | 43 | // Build a frame that the server can send to client, data is not masked 44 | fun val build(): Array[(String val | Array[U8 val] val)] iso^ => 45 | let writer: Writer = Writer 46 | 47 | match opcode 48 | | Text => writer.u8(0b1000_0001) 49 | | Binary => writer.u8(0b1000_0010) 50 | | Ping => writer.u8(0b1000_1001) 51 | | Pong => writer.u8(0b1000_1010) 52 | | Close => 53 | writer.u8(0b1000_1000) 54 | writer.u8(0x2) // two bytes for code 55 | writer.write(data) // status code 56 | return writer.done() 57 | end 58 | 59 | var payload_len = data.size() 60 | if payload_len < 126 then 61 | writer.u8(U8.from[USize](payload_len)) 62 | elseif payload_len < 65536 then 63 | writer.u8(126) 64 | writer.u16_be(U16.from[USize](payload_len)) 65 | else 66 | writer.u8(127) 67 | writer.u64_be(U64.from[USize](payload_len)) 68 | end 69 | writer.write(data) 70 | writer.done() 71 | -------------------------------------------------------------------------------- /websocket/handshake.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "encode/base64" 3 | use "crypto" 4 | 5 | class HandshakeRequest 6 | var method: String = "" 7 | var resource: String = "" 8 | let headers: Map[String, String] = headers.create(32) 9 | 10 | fun _handshake(): String ? => 11 | try 12 | let version = headers("sec-websocket-version")? 13 | let key = headers("sec-websocket-key")? 14 | let upgrade = headers("upgrade")? 15 | let connection = headers("connection")? 16 | 17 | if version.lower() != "13" then error end 18 | if upgrade.lower() != "websocket" then error end 19 | var conn_upgrade = false 20 | for s in connection.split_by(",").values() do 21 | if s.lower().>strip(" ") == "upgrade" then 22 | conn_upgrade = true 23 | break 24 | end 25 | end 26 | if not conn_upgrade then error end 27 | 28 | _response(_accept_key(key)) 29 | else 30 | error 31 | end 32 | 33 | fun _response(key: String): String => 34 | "HTTP/1.1 101 Switching Protocols\r\n" 35 | + "Upgrade: websocket\r\n" 36 | + "Connection: Upgrade\r\n" 37 | + "Sec-WebSocket-Accept:" + key 38 | + "\r\n\r\n" 39 | 40 | fun _accept_key(key: String): String => 41 | let c = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 42 | let digest = Digest.sha1() 43 | try 44 | digest.append(consume c)? 45 | end 46 | let d = digest.final() 47 | Base64.encode(d) 48 | 49 | fun ref _set_header(key: String, value: String) => 50 | headers(key) = value 51 | -------------------------------------------------------------------------------- /websocket/listen_notify.pony: -------------------------------------------------------------------------------- 1 | interface WebSocketListenNotify 2 | 3 | fun ref listening() => None 4 | 5 | fun ref not_listening() => None 6 | 7 | fun ref connected(): WebSocketConnectionNotify iso^ -------------------------------------------------------------------------------- /websocket/listener.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | use "buffered" 3 | use "net_ssl" 4 | 5 | actor WebSocketListener 6 | let _tcp_listner: TCPListener 7 | 8 | new create( 9 | auth: TCPListenAuth, 10 | notify: WebSocketListenNotify iso, 11 | host: String, 12 | service: String, 13 | limit: USize val = 0, 14 | read_buffer_size: USize val = 16384, 15 | yield_after_reading: USize val = 16384, 16 | yield_after_writing: USize val = 16384, 17 | ssl_context: (SSLContext | None) = None 18 | ) 19 | => 20 | _tcp_listner = TCPListener(auth, 21 | recover _TCPListenNotify(consume notify, ssl_context) end, 22 | host, 23 | service, 24 | limit, 25 | read_buffer_size, 26 | yield_after_reading, 27 | yield_after_writing) 28 | 29 | class _TCPListenNotify is TCPListenNotify 30 | var notify: WebSocketListenNotify iso 31 | let ssl_context: (SSLContext | None) 32 | 33 | new create(notify': WebSocketListenNotify iso, ssl_context': (SSLContext | None) = None) => 34 | notify = consume notify' 35 | ssl_context = ssl_context' 36 | 37 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ ? => 38 | let n = notify.connected() 39 | match ssl_context 40 | | let ctx: SSLContext => 41 | let ssl = ctx.server()? 42 | SSLConnection(_TCPConnectionNotify(consume n), consume ssl) 43 | else 44 | _TCPConnectionNotify(consume n) 45 | end 46 | 47 | fun ref not_listening(listen: TCPListener ref) => 48 | notify.not_listening() 49 | 50 | fun ref listening(listen: TCPListener ref) => 51 | notify.listening() 52 | 53 | primitive _Open 54 | primitive _Connecting 55 | primitive _Closed 56 | primitive _Error 57 | 58 | type State is (_Connecting | _Open | _Closed | _Error) 59 | 60 | class _TCPConnectionNotify is TCPConnectionNotify 61 | var _notify: (WebSocketConnectionNotify iso | None) 62 | var _http_parser: _HttpParser ref = _HttpParser 63 | let _buffer: Reader ref = Reader 64 | var _state: State = _Connecting 65 | var _frame_decoder: _FrameDecoder ref = _FrameDecoder 66 | var _connection: (WebSocketConnection | None) = None 67 | 68 | new iso create(notify: WebSocketConnectionNotify iso) => 69 | _notify = consume notify 70 | 71 | fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize) : Bool => 72 | // Should not handle any data when connection closed or error occured 73 | if (_state is _Error) or (_state is _Closed) then 74 | return false 75 | end 76 | 77 | _buffer.append(consume data) 78 | 79 | try 80 | match _state 81 | | _Connecting => 82 | while (_buffer.size() > 0) do 83 | _handle_handshake(conn, _buffer) 84 | end 85 | | _Open => _handle_frame(conn, _buffer)? 86 | end 87 | else 88 | _state = _Error 89 | end 90 | 91 | match _state 92 | | _Error => 93 | match _connection 94 | | let c: WebSocketConnection => 95 | c._close(_frame_decoder.status) 96 | end 97 | false 98 | | _Closed => false 99 | | _Open => true 100 | | _Connecting => true 101 | end 102 | 103 | fun ref connect_failed(conn: TCPConnection ref) => None 104 | 105 | fun ref _handle_handshake(conn: TCPConnection ref, buffer: Reader ref) => 106 | try 107 | match _http_parser.parse(_buffer)? 108 | | let req: HandshakeRequest val => 109 | let rep = req._handshake()? 110 | conn.write(rep) 111 | _state = _Open 112 | // Create connection 113 | match (_notify = None, _connection) 114 | | (let n: WebSocketConnectionNotify iso, None) => 115 | _connection = WebSocketConnection(conn, consume n, req) 116 | end 117 | conn.expect(2)? // expect minimal header 118 | end 119 | else 120 | conn.write("HTTP/1.1 400 BadRequest\r\n\r\n") 121 | conn.dispose() 122 | end 123 | 124 | fun ref _handle_frame(conn: TCPConnection ref, buffer: Reader ref)? => 125 | let frame = _frame_decoder.decode(_buffer)? 126 | match frame 127 | | let f: Frame val => 128 | match (_connection, f.opcode) 129 | | (None, Text) => error 130 | | (let c : WebSocketConnection, Text) => c._text_received(f.data as String) 131 | | (let c : WebSocketConnection, Binary) => c._binary_received(f.data as Array[U8] val) 132 | | (let c : WebSocketConnection, Ping) => c._send_pong(f.data as Array[U8] val) 133 | | (let c : WebSocketConnection, Close) => c._close(1000) 134 | end 135 | conn.expect(2)? // expect next header 136 | | let n: USize => 137 | // need more data to parse an frame 138 | // notice: if n > read_buffer_size, connection will be closed 139 | conn.expect(n)? 140 | end 141 | 142 | fun ref closed(conn: TCPConnection ref) => 143 | // When TCP connection is closed, enter CLOSED state. 144 | // See https://tools.ietf.org/html/rfc6455#section-7.1.4 145 | _state = _Closed 146 | match _connection 147 | | let c: WebSocketConnection => 148 | c._notify_closed() 149 | end 150 | -------------------------------------------------------------------------------- /websocket/simple_server.pony: -------------------------------------------------------------------------------- 1 | use "net" 2 | 3 | interface SimpleWebSocketNotify 4 | be opened(conn: WebSocketConnection tag) => 5 | None 6 | 7 | be closed(conn: WebSocketConnection tag) => 8 | None 9 | 10 | be text_received(conn: WebSocketConnection tag, text: String) => 11 | None 12 | 13 | be binary_received(conn: WebSocketConnection tag, data: Array[U8 val] val) => 14 | None 15 | 16 | be listening() => 17 | None 18 | 19 | be not_listening() => 20 | None 21 | 22 | class SimpleServer is WebSocketListenNotify 23 | let _notify: SimpleWebSocketNotify tag 24 | 25 | new create(notify: SimpleWebSocketNotify tag) => 26 | _notify = notify 27 | 28 | fun ref connected(): _ConnectionNotify iso^ => 29 | _ConnectionNotify(_notify) 30 | 31 | fun ref not_listening() => 32 | _notify.not_listening() 33 | 34 | class _ConnectionNotify is WebSocketConnectionNotify 35 | let _notify: SimpleWebSocketNotify tag 36 | 37 | new iso create(notify: SimpleWebSocketNotify tag) => 38 | _notify = notify 39 | 40 | fun ref opened(conn: WebSocketConnection tag) => 41 | _notify.opened(conn) 42 | 43 | fun ref text_received(conn: WebSocketConnection tag, text: String) => 44 | _notify.text_received(conn, text) 45 | 46 | fun ref binary_received(conn: WebSocketConnection tag, data: Array[U8] val) => 47 | _notify.binary_received(conn, data) 48 | 49 | fun ref closed(conn: WebSocketConnection tag) => 50 | _notify.closed(conn) 51 | -------------------------------------------------------------------------------- /websocket/utf8.pony: -------------------------------------------------------------------------------- 1 | 2 | primitive UTF8 3 | 4 | fun validate(data: Array[U8 val] iso, start_index: USize): Array[U8 val] iso^? => 5 | let buf = consume data 6 | let len = buf.size() 7 | var i = start_index 8 | var valid: Bool = true 9 | 10 | // TODO: multiline array 11 | let char_width: Array[U8] = [ 12 | 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1; 0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0; 0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0; 0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0; 0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0; 0;0;2;2;2;2;2;2;2;2;2;2;2;2;2;2; 2;2;2;2;2;2;2;2;2;2;2;2;2;2;2;2; 3;3;3;3;3;3;3;3;3;3;3;3;3;3;3;3;4;4;4;4;4;0;0;0;0;0;0;0;0;0;0;0 13 | ] 14 | 15 | while i < len do 16 | let first = buf(i)? 17 | if first > 128 then 18 | let w = char_width(USize.from[U8](first))? 19 | match w 20 | | 2 => 21 | if ((i + 1) == len) or _cont(buf(i + 1)?) then 22 | valid = false 23 | break 24 | else 25 | i = i + 2 26 | end 27 | | 3 => 28 | if ((i + 2) >= len) then 29 | valid = false 30 | break 31 | end 32 | if _cont(buf(i + 2)?) then 33 | valid = false 34 | break 35 | end 36 | 37 | let b2 = buf(i + 1)? 38 | if not ( 39 | ((first == 0xe0) and _in_range(b2, 0xA0, 0xbf)) 40 | or (_in_range(first, 0xe1, 0xec) and _in_range(b2, 0x80, 0xbf)) 41 | or ((first == 0xed) and _in_range(b2, 0x80, 0x9f)) 42 | or (_in_range(first, 0xee, 0xef) and _in_range(b2, 0x80, 0xbf)) 43 | ) then 44 | valid = false 45 | break 46 | end 47 | i = i + 3 48 | | 4 => 49 | if ((i + 3) >= len) then 50 | valid = false 51 | break 52 | end 53 | if _cont(buf(i + 2)?) or _cont(buf(i + 3)?) then 54 | valid = false 55 | break 56 | end 57 | 58 | let b2 = buf(i + 1)? 59 | if not( 60 | ((first == 0xf0) and _in_range(b2, 0x90, 0xbf)) 61 | or (_in_range(first, 0xf1, 0xf3) and _in_range(b2, 0x80, 0xbf)) 62 | or ((first == 0xf4) and _in_range(b2, 0x80, 0x8f)) 63 | ) then 64 | valid = false 65 | break 66 | end 67 | i = i + 4 68 | 69 | | let o: U8 => 70 | valid = false 71 | break 72 | end 73 | else 74 | if first == 128 then 75 | valid = false 76 | break 77 | end 78 | i = i + 1 79 | end 80 | end 81 | 82 | if valid then 83 | buf 84 | else 85 | error 86 | end 87 | 88 | fun _in_range(a: U8, b: U8, c: U8): Bool => 89 | (a >= b) and (a <= c) 90 | 91 | fun _cont(a: U8): Bool => 92 | (a and (not 0b0011_1111)) != 0b1000_0000 --------------------------------------------------------------------------------