diff --git a/Makefile b/Makefile index 367b4fb..c564c3b 100755 --- a/Makefile +++ b/Makefile @@ -40,9 +40,11 @@ endif PONYC := $(PONYC) $(SSL) SOURCE_FILES := $(shell find $(SRC_DIR) -name *.pony) -EXAMPLES := $(notdir $(shell find $(EXAMPLES_DIR)/* -type d)) -EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony) +EXAMPLES := $(notdir $(shell find $(EXAMPLES_DIR)/* -maxdepth 0 -type d -not -name websocket_echo_server)) +EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony -not -path '**/websocket_echo_server/*') EXAMPLES_BINARIES := $(addprefix $(BUILD_DIR)/,$(EXAMPLES)) +WEBSOCKET_EXAMPLE_SOURCE_FILES := $(EXAMPLES_DIR)/websocket_echo_server/main.pony +WEBSOCKET_EXAMPLES_BINARY := $(BUILD_DIR)/websocket_echo_server BENCH_SOURCE_FILES := $(shell find $(BENCH_DIR) -name *.pony) test: unit-tests build-examples @@ -54,12 +56,17 @@ $(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR) $(GET_DEPENDENCIES_WITH) $(PONYC) -o $(BUILD_DIR) $(SRC_DIR) -build-examples: $(EXAMPLES_BINARIES) +build-examples: $(EXAMPLES_BINARIES) $(WEBSOCKET_EXAMPLES_BINARY) $(EXAMPLES_BINARIES): $(BUILD_DIR)/%: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) $(GET_DEPENDENCIES_WITH) $(PONYC) -o $(BUILD_DIR) $(EXAMPLES_DIR)/$* +$(WEBSOCKET_EXAMPLES_BINARY): $(BUILD_DIR)/%: $(SOURCE_FILES) $(WEBSOCKET_EXAMPLE_SOURCE_FILES) | $(BUILD_DIR) + cd $(EXAMPLES_DIR)/$* && \ + $(GET_DEPENDENCIES_WITH) && \ + $(PONYC) -o ../../$(BUILD_DIR) $(EXAMPLES_DIR)/$* + clean: $(CLEAN_DEPENDENCIES_WITH) rm -rf $(BUILD_DIR) diff --git a/examples/websocket_echo_server/.gitignore b/examples/websocket_echo_server/.gitignore new file mode 100644 index 0000000..36ce88d --- /dev/null +++ b/examples/websocket_echo_server/.gitignore @@ -0,0 +1,2 @@ +/_corral/ +/_repos/ diff --git a/examples/websocket_echo_server/corral.json b/examples/websocket_echo_server/corral.json new file mode 100644 index 0000000..825516f --- /dev/null +++ b/examples/websocket_echo_server/corral.json @@ -0,0 +1,27 @@ +{ + "packages": [ + "websocket_echo_server" + ], + "deps": [ + { + "locator": "../.." + }, + { + "locator": "github.com/mfelsche/pony-websocket.git", + "version": "0.7.0", + "note": "forked from https://github.com/oraoto/pony-websocket" + }, + { + "locator": "github.com/ponylang/crypto.git", + "version": "1.2.2" + } + ], + "info": { + "description": "http server example websocket echo server", + "homepage": "https://github.com/ponylang/http_server", + "license": "bsd-2-clause", + "documentation_url": "https://ponylang.github.io/http_server/", + "version": "0.4.6", + "name": "websocket_echo_server" + } +} diff --git a/examples/websocket_echo_server/lock.json b/examples/websocket_echo_server/lock.json new file mode 100644 index 0000000..11ee16a --- /dev/null +++ b/examples/websocket_echo_server/lock.json @@ -0,0 +1,32 @@ +{ + "locks": [ + { + "locator": "../..", + "revision": "main" + }, + { + "locator": "github.com/ponylang/json.git", + "revision": "0.1.0" + }, + { + "locator": ".", + "revision": "main" + }, + { + "locator": "github.com/ponylang/valbytes.git", + "revision": "0.6.2" + }, + { + "locator": "github.com/mfelsche/pony-websocket.git", + "revision": "0.7.0" + }, + { + "locator": "github.com/ponylang/crypto.git", + "revision": "1.2.2" + }, + { + "locator": "github.com/ponylang/net_ssl.git", + "revision": "1.3.2" + } + ] +} diff --git a/examples/websocket_echo_server/main.pony b/examples/websocket_echo_server/main.pony new file mode 100644 index 0000000..33cf0d4 --- /dev/null +++ b/examples/websocket_echo_server/main.pony @@ -0,0 +1,124 @@ +use "crypto" +use "encode/base64" +use "net" + +use "http_server" +use ws = "websocket" + +actor Main + new create(env: Env) => + let host = "localhost" + let port = "9999" + let limit: USize = 9000 + let server = Server( + TCPListenAuth(env.root), + SimpleServerNotify, // notify for server lifecycle events + BackendMaker // factory for session-based application backend + where config = ServerConfig( // configuration of Server + where host' = host, + port' = port, + max_concurrent_connections' = limit) + ) + +class SimpleServerNotify + fun ref listening(server: Server ref) => + None + + fun ref not_listening(server: Server ref) => + None + + fun ref closed(server: Server ref) => + None + +class BackendMaker is HandlerFactory + fun apply(session: Session): Handler^ => + RequestHandler.create(session) + +class RequestHandler is Handler + let _session: Session + var _response_builder: ResponseBuilder + + new ref create(session: Session) => + _session = session + _response_builder = Responses.builder() + + fun ref apply(request: Request val, request_id: RequestID) => + if (request.method() isnt GET) or (request.version() < HTTP11) then + let body = "Invalid Method or HTTP version" + this._send_err(request_id, StatusBadRequest, body) + else + try + let upgrade_header = request.header("Upgrade") as String + if upgrade_header != "websocket" then + error + end + let conn_header = request.header("Connection") as String + if conn_header != "Upgrade" then + error + end + // calculate Sec-Websocket-Accept from Sec-Websocket-Key + let ws_key = request.header("Sec-WebSocket-Key") as String + let sha1_digest = Digest.sha1() + sha1_digest.append(ws_key)? + sha1_digest.append("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")? + let hash: Array[U8] val = sha1_digest.final() + let sec_ws_accept: String val = Base64.encode(hash) + + let ws_version = request.header("Sec-WebSocket-Version") as String + if ws_version.u64()? != 13 then + error + end + _session.send_raw( + _response_builder.set_status(StatusSwitchingProtocols) + .add_header("Upgrade", "websocket") + .add_header("Connection", "Upgrade") + .add_header("Sec-WebSocket-Accept", sec_ws_accept) + .finish_headers() + .build(), + request_id + ) + _session.send_finished(request_id) + _session.upgrade( + ws.WebsocketTCPConnectionNotify.open( + MyLittleWebSocketConnectionNotify.create() + ) + ) + else + this._send_err(request_id, StatusBadRequest, "Invalid Websocket Handshake Request") + end + end + + fun ref chunk(data: ByteSeq val, request_id: RequestID) => None + + fun ref finished(request_id: RequestID) => + _response_builder = _response_builder.reset() + + fun ref _send_err(request_id: RequestID, status: Status, body: String) => + _session.send_raw( + _response_builder.set_status(StatusBadRequest) + .add_header("Server", "Pony/http_server") + .add_header("Content-Length", body.size().string()) + .finish_headers() + .add_chunk(body.array()) + .build(), + request_id + ) + _session.send_finished(request_id) + +class iso MyLittleWebSocketConnectionNotify is ws.WebSocketConnectionNotify + + new iso create() => None + + fun ref opened(conn: ws.WebSocketConnection ref) => + None + + fun ref closed(conn: ws.WebSocketConnection ref) => + None + + fun ref text_received(conn: ws.WebSocketConnection ref, text: String) => + conn.send_text(text) + + fun ref binary_received( + conn: ws.WebSocketConnection ref, + data: Array[U8 val] val) => + conn.send_binary(data) diff --git a/http_server/_server_connection.pony b/http_server/_server_connection.pony index 258a5fd..af2f0ee 100644 --- a/http_server/_server_connection.pony +++ b/http_server/_server_connection.pony @@ -291,3 +291,6 @@ actor _ServerConnection is (Session & HTTP11RequestHandler) dispose() end + be upgrade(notify: TCPConnectionNotify iso) => + _conn.set_notify(consume notify) + diff --git a/http_server/request.pony b/http_server/request.pony index ce8ab59..4417f57 100644 --- a/http_server/request.pony +++ b/http_server/request.pony @@ -1,5 +1,5 @@ -interface val _Version is (Equatable[Version] & Stringable) +interface val _Version is (Equatable[Version] & Stringable & Comparable[Version]) fun to_bytes(): Array[U8] val primitive HTTP11 is _Version @@ -8,7 +8,15 @@ primitive HTTP11 is _Version """ fun string(): String iso^ => recover iso String(8).>append("HTTP/1.1") end fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '1'; '.'; '1'] + fun u64(): U64 => 'HTTP/1.1' fun eq(o: Version): Bool => o is this + fun lt(o: Version): Bool => + match o + | let _: HTTP11 => false + | let _: HTTP10 => false + | let _: HTTP09 => false + end + primitive HTTP10 is _Version """ @@ -16,7 +24,14 @@ primitive HTTP10 is _Version """ fun string(): String iso^ => recover iso String(8).>append("HTTP/1.0") end fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '1'; '.'; '0'] + fun u64(): U64 => 'HTTP/1.0' fun eq(o: Version): Bool => o is this + fun lt(o: Version): Bool => + match o + | let _: HTTP11 => true + | let _: HTTP10 => false + | let _: HTTP09 => false + end primitive HTTP09 is _Version """ @@ -24,7 +39,16 @@ primitive HTTP09 is _Version """ fun string(): String iso^ => recover iso String(8).>append("HTTP/0.9") end fun to_bytes(): Array[U8] val => [as U8: 'H'; 'T'; 'T'; 'P'; '/'; '0'; '.'; '9'] + fun u64(): U64 => 'HTTP/0.9' fun eq(o: Version): Bool => o is this + fun lt(o: Version): Bool => + match o + | let _: HTTP11 => true + | let _: HTTP10 => true + | let _: HTTP09 => false + end + + type Version is ((HTTP09 | HTTP10 | HTTP11) & _Version) """ diff --git a/http_server/session.pony b/http_server/session.pony index db6b9b2..efb7441 100644 --- a/http_server/session.pony +++ b/http_server/session.pony @@ -1,4 +1,5 @@ use "valbytes" +use "net" trait tag Session """ @@ -243,5 +244,10 @@ trait tag Session """ None + be upgrade(notify: TCPConnectionNotify iso) => + """ + Upgrade this TCP connection to another handler + """ + None