├── .gitignore ├── .haxerc ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── dev.hxml ├── haxe_libraries ├── ansi.hxml ├── asys.hxml ├── http-status.hxml ├── hxnodejs.hxml ├── mime.hxml ├── tink_anon.hxml ├── tink_await.hxml ├── tink_chunk.hxml ├── tink_cli.hxml ├── tink_core.hxml ├── tink_http.hxml ├── tink_http_middleware.hxml ├── tink_io.hxml ├── tink_macro.hxml ├── tink_priority.hxml ├── tink_state.hxml ├── tink_streams.hxml ├── tink_stringly.hxml ├── tink_syntaxhub.hxml ├── tink_tcp.hxml ├── tink_testrunner.hxml ├── tink_unittest.hxml ├── tink_url.hxml ├── tink_websocket.hxml └── travix.hxml ├── haxelib.json ├── middleware_example.hxml ├── playground.hxml ├── src └── tink │ ├── http │ └── middleware │ │ └── WebSocket.hx │ └── websocket │ ├── Client.hx │ ├── ClientHandler.hx │ ├── Frame.hx │ ├── IncomingHandshakeRequestHeader.hx │ ├── IncomingHandshakeResponseHeader.hx │ ├── MaskingKey.hx │ ├── Message.hx │ ├── MessageStream.hx │ ├── OutgoingHandshakeRequestHeader.hx │ ├── OutgoingHandshakeResponseHeader.hx │ ├── Parser.hx │ ├── PongStream.hx │ ├── RawMessage.hx │ ├── RawMessageStream.hx │ ├── Server.hx │ ├── ServerHandler.hx │ ├── clients │ ├── HttpConnector.hx │ ├── JsConnector.hx │ └── TcpConnector.hx │ └── servers │ ├── NodeWsServer.hx │ └── TinkServer.hx ├── tests.hxml └── tests ├── ClientTest.hx ├── HeaderTest.hx ├── MiddlewareExample.hx ├── ParserTest.hx ├── Playground.hx ├── RunTests.hx ├── TcpAcceptorTest.hx └── TcpConnectorTest.hx /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules -------------------------------------------------------------------------------- /.haxerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.5", 3 | "resolveLibs": "scoped" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | stages: 5 | - test 6 | - deploy 7 | 8 | language: node_js 9 | node_js: 8 10 | 11 | os: 12 | - linux 13 | # - osx 14 | 15 | env: 16 | - HAXE_VERSION=3.4.7 17 | - HAXE_VERSION=nightly 18 | 19 | install: 20 | - npm i -g lix 21 | - lix install haxe $HAXE_VERSION 22 | - lix download 23 | 24 | script: 25 | - lix run travix node 26 | - lix run travix js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "haxe.displayConfigurations": [ 3 | ["dev.hxml"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "lix", 4 | "args": ["run","travix","node"], 5 | "problemMatcher": "$haxe", 6 | "group": { 7 | "kind": "build", 8 | "isDefault": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinkerbell WebSocket 2 | 3 | [![Build Status](https://travis-ci.org/haxetink/tink_websocket.svg)](https://travis-ci.org/haxetink/tink_websocket) 4 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?maxAge=2592000)](https://gitter.im/haxetink/public) 5 | 6 | > WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. 7 | 8 | ## Handler 9 | 10 | For each connection, there is a stream of incoming messages (chunk of bytes) and a stream of outgoing messages. 11 | So, a websocket handler is `RealStream->IdealStream` where the `RealStream` is the incoming stream 12 | and `IdealStream` is the outgoing stream. 13 | 14 | Getting chunks of bytes isn't very useful. So the `MessageStream` class comes to rescue: 15 | 16 | ```haxe 17 | abstract MessageStream(Stream) from Stream to Stream { 18 | @:from public static inline function ofChunkStream(s:Stream):MessageStream 19 | @:to public inline function toChunkStream():Stream 20 | } 21 | ``` 22 | 23 | So `MessageStream.ofChunkStream(chunkStream)` will give you a readily usable stream of websocket messages. 24 | On the other hand, given a stream of messages, calling `messageStream.toChunkStream()` will give you the 25 | stream of chunks that is ready to get piped into the tcp wire. 26 | 27 | ## Client 28 | 29 | ### With tink_tcp 30 | 31 | Use `Connector.wrap()` to transform a websocket handler to a tcp handler, then use it in a tcp connection. 32 | Visit documentation of tink_tcp for more details on setting up a tcp connection. 33 | 34 | ## Server 35 | 36 | ### With tink_tcp 37 | 38 | Use `Acceptor.wrap()` to transform a websocket handler to a tcp handler, then use it in a tcp server. 39 | Visit documentation of tink_tcp for more details on setting up a tcp server. 40 | 41 | ### With tink_http 42 | 43 | Use the `WebSocket` middleware to combine a websocket handler with a http handler into a single http handler, then use it in a http container. 44 | Visit documentation of tink_http for more details on setting up a http container. 45 | 46 | 47 | TODO: 48 | 49 | - Respond to ping-pong messages automatically 50 | - Handle connection-close messages 51 | -------------------------------------------------------------------------------- /dev.hxml: -------------------------------------------------------------------------------- 1 | tests.hxml 2 | 3 | -lib travix 4 | -lib tink_websocket 5 | -lib hxnodejs 6 | -js bin/node/tests.js -------------------------------------------------------------------------------- /haxe_libraries/ansi.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:ansi#1.0.0 into ansi/1.0.0/haxelib 2 | -D ansi=1.0.0 3 | -cp ${HAXESHIM_LIBCACHE}/ansi/1.0.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/asys.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "https://github.com/kevinresol/asys/archive/81d6331dd5cbf9f8913fe1a62b2134de3bd714d7.tar.gz" into asys/0.3.0/github/81d6331dd5cbf9f8913fe1a62b2134de3bd714d7 2 | -D asys=0.3.0 3 | -cp ${HAXESHIM_LIBCACHE}/asys/0.3.0/github/81d6331dd5cbf9f8913fe1a62b2134de3bd714d7/ 4 | 5 | -lib tink_core 6 | -lib tink_macro 7 | -lib tink_io 8 | -lib tink_streams -------------------------------------------------------------------------------- /haxe_libraries/http-status.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:http-status#1.3.1 into http-status/1.3.1/haxelib 2 | -D http-status=1.3.1 3 | -cp ${HAXESHIM_LIBCACHE}/http-status/1.3.1/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/hxnodejs.hxml: -------------------------------------------------------------------------------- 1 | -D hxnodejs=6.9.1 2 | # @install: lix --silent download "gh://github.com/haxefoundation/hxnodejs#38bdefd853f8d637ffb6e74c69ccaedc01985cac" into hxnodejs/6.9.1/github/38bdefd853f8d637ffb6e74c69ccaedc01985cac 3 | -cp ${HAXE_LIBCACHE}/hxnodejs/6.9.1/github/38bdefd853f8d637ffb6e74c69ccaedc01985cac/src 4 | --macro allowPackage('sys') 5 | # should behave like other target defines and not be defined in macro context 6 | --macro define('nodejs') 7 | -------------------------------------------------------------------------------- /haxe_libraries/mime.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download "haxelib:mime#0.0.1" into mime/0.0.1/haxelib 2 | -D mime=0.0.1 3 | -cp ${HAXESHIM_LIBCACHE}/mime/0.0.1/haxelib/ 4 | --macro mime.Mime.init() -------------------------------------------------------------------------------- /haxe_libraries/tink_anon.hxml: -------------------------------------------------------------------------------- 1 | -D tink_anon=0.3.1 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_anon#6594aded78443e4b6beea909fdc872034de67970" into tink_anon/0.3.1/github/6594aded78443e4b6beea909fdc872034de67970 3 | -lib tink_macro 4 | -cp ${HAXE_LIBCACHE}/tink_anon/0.3.1/github/6594aded78443e4b6beea909fdc872034de67970/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_await.hxml: -------------------------------------------------------------------------------- 1 | -D tink_await=0.4.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_await#e29b660ef0fef49b38b918f9e58759e7b7da3ac0" into tink_await/0.4.0/github/e29b660ef0fef49b38b918f9e58759e7b7da3ac0 3 | -lib tink_syntaxhub 4 | -lib tink_macro 5 | -lib tink_core 6 | -cp ${HAXE_LIBCACHE}/tink_await/0.4.0/github/e29b660ef0fef49b38b918f9e58759e7b7da3ac0/src 7 | --macro tink.await.Await.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_chunk.hxml: -------------------------------------------------------------------------------- 1 | -D tink_chunk=0.3.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_chunk#2887262a7709ee647faf46421970fae6ea5fb36f" into tink_chunk/0.3.0/github/2887262a7709ee647faf46421970fae6ea5fb36f 3 | -cp ${HAXE_LIBCACHE}/tink_chunk/0.3.0/github/2887262a7709ee647faf46421970fae6ea5fb36f/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_cli.hxml: -------------------------------------------------------------------------------- 1 | -D tink_cli=0.3.1 2 | # @install: lix --silent download "haxelib:/tink_cli#0.3.1" into tink_cli/0.3.1/haxelib 3 | -lib tink_io 4 | -lib tink_stringly 5 | -lib tink_macro 6 | -cp ${HAXE_LIBCACHE}/tink_cli/0.3.1/haxelib/src 7 | # Make sure docs are generated 8 | -D use-rtti-doc -------------------------------------------------------------------------------- /haxe_libraries/tink_core.hxml: -------------------------------------------------------------------------------- 1 | -D tink_core=1.24.0 2 | # @install: lix --silent download "haxelib:/tink_core#1.24.0" into tink_core/1.24.0/haxelib 3 | -cp ${HAXE_LIBCACHE}/tink_core/1.24.0/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_http.hxml: -------------------------------------------------------------------------------- 1 | -D tink_http=0.9.1 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_http#3a57b77c9655115bfaf3fd61af4ccc5fa84c9d12" into tink_http/0.9.1/github/3a57b77c9655115bfaf3fd61af4ccc5fa84c9d12 3 | -lib http-status 4 | -lib tink_anon 5 | -lib tink_io 6 | -lib tink_url 7 | -cp ${HAXE_LIBCACHE}/tink_http/0.9.1/github/3a57b77c9655115bfaf3fd61af4ccc5fa84c9d12/src 8 | -------------------------------------------------------------------------------- /haxe_libraries/tink_http_middleware.hxml: -------------------------------------------------------------------------------- 1 | -D tink_http_middleware=0.1.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_http_middleware#a2df34c74c0ad47252b286d9ecb76ee051cc8b0d" into tink_http_middleware/0.1.0/github/a2df34c74c0ad47252b286d9ecb76ee051cc8b0d 3 | -lib asys 4 | -lib mime 5 | -lib tink_http 6 | -cp ${HAXE_LIBCACHE}/tink_http_middleware/0.1.0/github/a2df34c74c0ad47252b286d9ecb76ee051cc8b0d/src 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_io.hxml: -------------------------------------------------------------------------------- 1 | -D tink_io=0.7.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_io#284d565f5a8375cb3d289d7f562af4cb89d9fc16" into tink_io/0.7.0/github/284d565f5a8375cb3d289d7f562af4cb89d9fc16 3 | -lib tink_chunk 4 | -lib tink_streams 5 | -cp ${HAXE_LIBCACHE}/tink_io/0.7.0/github/284d565f5a8375cb3d289d7f562af4cb89d9fc16/src 6 | -------------------------------------------------------------------------------- /haxe_libraries/tink_macro.hxml: -------------------------------------------------------------------------------- 1 | -D tink_macro=0.18.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_macro#b4fa5f4fe0310bd919866af42d403de06fdcf53c" into tink_macro/0.18.0/github/b4fa5f4fe0310bd919866af42d403de06fdcf53c 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_macro/0.18.0/github/b4fa5f4fe0310bd919866af42d403de06fdcf53c/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_priority.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:tink_priority#0.1.3 into tink_priority/0.1.3/haxelib 2 | -D tink_priority=0.1.3 3 | -cp ${HAXESHIM_LIBCACHE}/tink_priority/0.1.3/haxelib/src 4 | -------------------------------------------------------------------------------- /haxe_libraries/tink_state.hxml: -------------------------------------------------------------------------------- 1 | -D tink_state=0.11.0 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_state#14cb828db6ac95f8542063d04febf8b331e7b3ca" into tink_state/0.11.0/github/14cb828db6ac95f8542063d04febf8b331e7b3ca 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_state/0.11.0/github/14cb828db6ac95f8542063d04febf8b331e7b3ca/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_streams.hxml: -------------------------------------------------------------------------------- 1 | -D tink_streams=0.3.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_streams#01a2b3894ae7ee5eb583503282371e307c03f065" into tink_streams/0.3.2/github/01a2b3894ae7ee5eb583503282371e307c03f065 3 | -lib tink_core 4 | -cp ${HAXE_LIBCACHE}/tink_streams/0.3.2/github/01a2b3894ae7ee5eb583503282371e307c03f065/src 5 | # temp for development, delete this file when pure branch merged 6 | -D pure -------------------------------------------------------------------------------- /haxe_libraries/tink_stringly.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix download https://lib.haxe.org/p/tink_stringly/0.1.0/download/ as tink_stringly#0.1.0/haxelib 2 | -D tink_stringly=0.1.0 3 | -cp ${HAXESHIM_LIBCACHE}/tink_stringly/0.1.0/haxelib/src 4 | 5 | -lib tink_core -------------------------------------------------------------------------------- /haxe_libraries/tink_syntaxhub.hxml: -------------------------------------------------------------------------------- 1 | -D tink_syntaxhub=0.4.3 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_syntaxhub#8b928af11fb39170dcb7254d02923777cddcc678" into tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678 3 | -lib tink_priority 4 | -lib tink_macro 5 | -cp ${HAXE_LIBCACHE}/tink_syntaxhub/0.4.3/github/8b928af11fb39170dcb7254d02923777cddcc678/src 6 | --macro tink.SyntaxHub.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_tcp.hxml: -------------------------------------------------------------------------------- 1 | -D tink_tcp=0.1.1 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_tcp#b0961db4db515d6f695a23b09234412dbbbd09f7" into tink_tcp/0.1.1/github/b0961db4db515d6f695a23b09234412dbbbd09f7 3 | -lib tink_io 4 | -cp ${HAXE_LIBCACHE}/tink_tcp/0.1.1/github/b0961db4db515d6f695a23b09234412dbbbd09f7/src 5 | -------------------------------------------------------------------------------- /haxe_libraries/tink_testrunner.hxml: -------------------------------------------------------------------------------- 1 | -D tink_testrunner=0.7.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#5066b09848994f08c829ccecb6cfe0a46d157be5" into tink_testrunner/0.7.2/github/5066b09848994f08c829ccecb6cfe0a46d157be5 3 | -lib ansi 4 | -lib tink_macro 5 | -lib tink_streams 6 | -cp ${HAXE_LIBCACHE}/tink_testrunner/0.7.2/github/5066b09848994f08c829ccecb6cfe0a46d157be5/src 7 | -------------------------------------------------------------------------------- /haxe_libraries/tink_unittest.hxml: -------------------------------------------------------------------------------- 1 | -D tink_unittest=0.6.2 2 | # @install: lix --silent download "gh://github.com/haxetink/tink_unittest#b9df9a25bdac55d81e48d1e91d1aece419dfb9c9" into tink_unittest/0.6.2/github/b9df9a25bdac55d81e48d1e91d1aece419dfb9c9 3 | -lib tink_syntaxhub 4 | -lib tink_testrunner 5 | -cp ${HAXE_LIBCACHE}/tink_unittest/0.6.2/github/b9df9a25bdac55d81e48d1e91d1aece419dfb9c9/src 6 | --macro tink.unit.AssertionBufferInjector.use() -------------------------------------------------------------------------------- /haxe_libraries/tink_url.hxml: -------------------------------------------------------------------------------- 1 | # @install: lix --silent download haxelib:tink_url#0.3.1 into tink_url/0.3.1/haxelib 2 | -D tink_url=0.3.1 3 | -cp ${HAXESHIM_LIBCACHE}/tink_url/0.3.1/haxelib/src 4 | 5 | -lib tink_stringly -------------------------------------------------------------------------------- /haxe_libraries/tink_websocket.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -lib tink_http 3 | -lib tink_state -------------------------------------------------------------------------------- /haxe_libraries/travix.hxml: -------------------------------------------------------------------------------- 1 | -D travix=0.12.2 2 | # @install: lix --silent download "gh://github.com/back2dos/travix#dab32915d84f8aaa37a9e94685acd53d83afed97" into travix/0.12.2/github/dab32915d84f8aaa37a9e94685acd53d83afed97 3 | # @post-install: cd ${HAXE_LIBCACHE}/travix/0.12.2/github/dab32915d84f8aaa37a9e94685acd53d83afed97 && haxe -cp src --run travix.PostDownload 4 | # @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.12.2/github/dab32915d84f8aaa37a9e94685acd53d83afed97 5 | -lib tink_cli 6 | -cp ${HAXE_LIBCACHE}/travix/0.12.2/github/dab32915d84f8aaa37a9e94685acd53d83afed97/src 7 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tink_websocket", 3 | "license": "MIT", 4 | "tags": [ 5 | "websocket" 6 | ], 7 | "classPath": "src", 8 | "contributors": [ 9 | "kevinresol" 10 | ], 11 | "releasenote": "initial release", 12 | "version": "0.0.0", 13 | "dependencies": { 14 | "tink_http": "", 15 | "tink_state": "" 16 | } 17 | } -------------------------------------------------------------------------------- /middleware_example.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main MiddlewareExample 3 | -lib hxnodejs 4 | -lib tink_websocket 5 | -lib tink_http_middleware 6 | -js bin/middleware_example.js -------------------------------------------------------------------------------- /playground.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main Playground 3 | -lib hxnodejs 4 | -lib tink_websocket 5 | -lib tink_http_middleware 6 | -js bin/playground.js -------------------------------------------------------------------------------- /src/tink/http/middleware/WebSocket.hx: -------------------------------------------------------------------------------- 1 | package tink.http.middleware; 2 | 3 | import tink.websocket.*; 4 | import tink.streams.Stream; 5 | import tink.http.Middleware; 6 | import tink.http.Request; 7 | import tink.http.Response; 8 | 9 | using tink.io.Source; 10 | using tink.CoreApi; 11 | 12 | @:require(tink_http_middleware) 13 | class WebSocket implements MiddlewareObject { 14 | var ws:ServerHandler; 15 | 16 | public function new(ws, ?authenticator) { 17 | this.ws = ws; 18 | if(authenticator != null) authenticate = authenticator; 19 | } 20 | 21 | dynamic function authenticate(header:RequestHeader):Promise { 22 | return Noise; 23 | } 24 | 25 | public function apply(handler:tink.http.Handler):tink.http.Handler { 26 | return function(req:IncomingRequest):Future { 27 | var header:IncomingHandshakeRequestHeader = req.header; 28 | return switch [header.validate(), req.body] { 29 | case [Success(_), Plain(src)]: 30 | authenticate(req.header).flatMap(function(o) { 31 | return Future.sync(switch o { 32 | case Success(_): 33 | new OutgoingResponse( 34 | new OutgoingHandshakeResponseHeader(header.key), 35 | ws({ 36 | clientIp: req.clientIp, 37 | header: header, 38 | stream: src.parseStream(new Parser()), 39 | }).toUnmaskedChunkStream() 40 | ); 41 | case Failure(e): 42 | OutgoingResponse.reportError(e); 43 | }); 44 | }); 45 | default: 46 | handler.process(req); 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/tink/websocket/Client.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.state.*; 4 | import tink.streams.Stream; 5 | import tink.streams.RealStream; 6 | import tink.streams.IdealStream; 7 | import tink.Chunk; 8 | 9 | using tink.CoreApi; 10 | 11 | class Client { 12 | 13 | public var connected(get, null):Observable; 14 | public var messageReceived(get, null):Signal; 15 | 16 | var connector:Connector; 17 | 18 | var connectedState:State; 19 | var messageReceivedTrigger:SignalTrigger; 20 | 21 | var outgoingTrigger:SignalTrigger>; 22 | 23 | public function new(connector) { 24 | this.connector = connector; 25 | 26 | outgoingTrigger = Signal.trigger(); 27 | var outgoing = new SignalStream(outgoingTrigger); 28 | 29 | messageReceivedTrigger = Signal.trigger(); 30 | connectedState = new State(true); 31 | 32 | connector.connect(outgoing).forEach(function(message:RawMessage) { 33 | switch message { 34 | case Text(v): messageReceivedTrigger.trigger(Text(v)); 35 | case Binary(v): messageReceivedTrigger.trigger(Binary(v)); 36 | case _: // discard 37 | } 38 | return Resume; 39 | }).handle(function(o) switch o { 40 | case Depleted: close(); 41 | case Failed(err): close(); // TODO: signal the error 42 | case _: // should not happen 43 | }); 44 | } 45 | 46 | public function send(message:Message) { 47 | outgoingTrigger.trigger(Data(switch message { 48 | case Text(v): RawMessage.Text(v); 49 | case Binary(v): RawMessage.Binary(v); 50 | })); 51 | } 52 | 53 | public function close() { 54 | outgoingTrigger.trigger(End); 55 | connectedState.set(false); 56 | } 57 | 58 | inline function get_connected() 59 | return connectedState.observe(); 60 | 61 | inline function get_messageReceived() 62 | return messageReceivedTrigger.asSignal(); 63 | } 64 | 65 | 66 | interface Connector { 67 | /** 68 | * Create a new WebSocket connection. 69 | * Note that the connection will be closed when either stream is depleted, 70 | * so beware not to end the outgoing stream until you are done with the connection. 71 | * @param outgoing - Outgoing message stream 72 | * @return Incoming message stream 73 | */ 74 | function connect(outgoing:RawMessageStream):RawMessageStream; 75 | } 76 | -------------------------------------------------------------------------------- /src/tink/websocket/ClientHandler.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.streams.Stream; 4 | import tink.streams.IdealStream; 5 | import tink.streams.RealStream; 6 | import tink.io.StreamParser; 7 | import tink.Url; 8 | import tink.Chunk; 9 | 10 | using tink.CoreApi; 11 | using tink.io.Source; 12 | 13 | typedef ClientHandler = RawMessageStream->RawMessageStream; 14 | 15 | class ClientHandlerTools { 16 | #if tink_tcp 17 | public static function toTcpHandler(handler:ClientHandler, url:Url, ?onError:Error->RealStream):tink.tcp.Handler { 18 | if(onError == null) onError = function(_) return Empty.make(); 19 | 20 | return function(i:tink.tcp.Incoming):Future { 21 | return Future.sync({ 22 | stream: Generator.stream(function(step) { 23 | var header = new OutgoingHandshakeRequestHeader(url); 24 | var accept = header.accept; 25 | var promise = i.stream.parse(IncomingHandshakeResponseHeader.parser()) 26 | .next(function(o) return o.a.validate(accept).map(function(_) return o.b)) 27 | .next(function(rest):Stream return handler(RawMessageStream.ofChunkStream(rest.parseStream(new Parser()))).toMaskedChunkStream(MaskingKey.random)); 28 | step(Link((header.toString():Chunk), Stream.promise(promise).idealize(onError))); 29 | }), 30 | allowHalfOpen: true, 31 | }); 32 | } 33 | } 34 | #end 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/tink/websocket/Frame.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import haxe.ds.Option; 4 | import haxe.io.Bytes; 5 | import haxe.io.BytesBuffer; 6 | import tink.io.Source; 7 | import tink.websocket.RawMessage; 8 | import tink.Chunk; 9 | 10 | @:forward 11 | abstract Frame(FrameBase) from FrameBase to FrameBase { 12 | 13 | @:to 14 | public inline function toSource():IdealSource 15 | return toChunk(); 16 | 17 | @:to 18 | public inline function toChunk():Chunk 19 | return this.toChunk(); 20 | 21 | @:from 22 | public static inline function fromChunk(v:Chunk) 23 | return FrameBase.fromChunk(v); 24 | 25 | public static function ofMessage(message:RawMessage, ?maskingKey:MaskingKey):Frame { 26 | var opcode = 0; 27 | var payload = null; 28 | switch message { 29 | case Text(v): 30 | opcode = Text; 31 | payload = Bytes.ofString(v); 32 | case Binary(b): 33 | opcode = Binary; 34 | payload = b; 35 | case ConnectionClose: 36 | opcode = ConnectionClose; 37 | case Ping(b): 38 | opcode = Ping; 39 | payload = b; 40 | case Pong(b): 41 | opcode = Pong; 42 | payload = b; 43 | } 44 | if(maskingKey != null) payload = Masker.mask(payload, maskingKey); 45 | return new FrameBase(true, false, false, false, opcode, maskingKey == null ? Unmasked(payload) : Masked(payload, maskingKey)); 46 | } 47 | 48 | public function unmask():Frame { 49 | return switch this.payload { 50 | case Unmasked(_): this; 51 | case Masked(p, k): new FrameBase(this.fin, this.rsv1, this.rsv2, this.rsv3, this.opcode, Unmasked(Masker.unmask(p, k))); 52 | } 53 | } 54 | 55 | public function mask(key:MaskingKey):Frame { 56 | return switch this.payload { 57 | case Unmasked(p): new FrameBase(this.fin, this.rsv1, this.rsv2, this.rsv3, this.opcode, Masked(Masker.mask(p, key), key)); 58 | case Masked(_): this; 59 | } 60 | } 61 | } 62 | 63 | class FrameBase { 64 | public var fin(default, null):Bool; 65 | public var rsv1(default, null):Bool; 66 | public var rsv2(default, null):Bool; 67 | public var rsv3(default, null):Bool; 68 | public var opcode(default, null):Opcode; 69 | public var masked(get, never):Bool; 70 | public var payloadLength(get, never):Int; 71 | public var maskingKey(get, null):MaskingKey; 72 | public var payload(default, null):Payload; 73 | public var maskedPayload(get, null):Chunk; 74 | public var unmaskedPayload(get, null):Chunk; 75 | 76 | public function new(fin, rsv1, rsv2, rsv3, opcode, payload) { 77 | this.fin = fin; 78 | this.rsv1 = rsv1; 79 | this.rsv2 = rsv2; 80 | this.rsv3 = rsv3; 81 | this.opcode = opcode; 82 | this.payload = payload; 83 | } 84 | 85 | public static function fromChunk(chunk:Chunk):Frame { 86 | var bytes = chunk.toBytes(); 87 | var data = bytes.getData(); 88 | var length = bytes.length; 89 | var pos = 0; 90 | 91 | // first byte 92 | var c = Bytes.fastGet(data, pos++); 93 | var fin = (c >> 7 & 1) == 1; 94 | var rsv1 = (c >> 6 & 1) == 1; 95 | var rsv2 = (c >> 5 & 1) == 1; 96 | var rsv3 = (c >> 4 & 1) == 1; 97 | var opcode = c & 0xf; 98 | 99 | // second byte & length 100 | var c = Bytes.fastGet(data, pos++); 101 | var mask = c >> 7 == 1; 102 | var len = switch c & 127 { 103 | case 127: var l = 0; for(i in 0...8) l = l << 8 + Bytes.fastGet(data, pos++); l; 104 | case 126: var l = 0; for(i in 0...2) l = l << 8 + Bytes.fastGet(data, pos++); l; 105 | case v: v; 106 | } 107 | 108 | // masking key 109 | var maskingKey = 110 | if(mask) { 111 | var key = bytes.sub(pos, 4); 112 | pos += 4; 113 | MaskingKey.ofChunk(key); 114 | } else null; 115 | 116 | // payload 117 | var payload = bytes.sub(pos, length - pos); 118 | 119 | return new FrameBase(fin, rsv1, rsv2, rsv3, opcode, maskingKey == null ? Unmasked(payload) : Masked(payload, maskingKey)); 120 | } 121 | 122 | public function toChunk():Chunk { 123 | var out = new BytesBuffer(); 124 | 125 | // first byte: 126 | out.addByte( 127 | (this.fin ? 1 << 7 : 0) | 128 | (this.rsv1 ? 1 << 6 : 0) | 129 | (this.rsv2 ? 1 << 5 : 0) | 130 | (this.rsv3 ? 1 << 4 : 0) | 131 | this.opcode 132 | ); 133 | 134 | // second byte: 135 | out.addByte( 136 | (this.masked ? 1 << 7 : 0) | 137 | (this.payloadLength < 126 ? this.payloadLength : 126) // TODO: support 64-bit length 138 | ); 139 | 140 | // extended payload length: (TODO: support 64-bit length) 141 | if(this.payloadLength >= 126) { 142 | out.addByte(this.payloadLength >> 8 & 0xff); 143 | out.addByte(this.payloadLength & 0xff); 144 | } 145 | 146 | // masking key: 147 | if(this.masked) out.addBytes(this.maskingKey.toBytes(), 0, 4); 148 | 149 | // payload: 150 | var payload = this.maskedPayload; 151 | out.addBytes(payload, 0, payload.length); 152 | 153 | return out.getBytes(); 154 | } 155 | 156 | inline function get_maskingKey() 157 | return switch payload { 158 | case Masked(_, m): m; 159 | case Unmasked(_): null; 160 | } 161 | 162 | inline function get_masked() 163 | return payload.match(Masked(_)); 164 | 165 | inline function get_payloadLength() 166 | return switch payload { 167 | case Unmasked(p) | Masked(p, _): p.length; 168 | } 169 | 170 | function get_maskedPayload() { 171 | return switch payload { 172 | case Unmasked(p) | Masked(p, _): p; 173 | } 174 | } 175 | 176 | function get_unmaskedPayload() { 177 | return switch payload { 178 | case Unmasked(p): p; 179 | case Masked(p, k): Masker.unmask(p, k); 180 | } 181 | } 182 | } 183 | 184 | @:enum 185 | abstract Opcode(Int) from Int to Int { 186 | var Continuation = 0x0; 187 | var Text = 0x1; 188 | var Binary = 0x2; 189 | var ConnectionClose = 0x8; 190 | var Ping = 0x9; 191 | var Pong = 0xa; 192 | } 193 | 194 | enum Payload { 195 | Masked(masked:Chunk, key:MaskingKey); 196 | Unmasked(unmasked:Chunk); 197 | } 198 | 199 | class Masker { 200 | 201 | public static function mask(unmasked:Chunk, key:MaskingKey):Chunk { 202 | var masked = Bytes.alloc(unmasked.length); 203 | var data = unmasked.toBytes().getData(); 204 | var key = key.toBytes().getData(); 205 | for(i in 0...unmasked.length) masked.set(i, Bytes.fastGet(data, i) ^ Bytes.fastGet(key, i % 4)); 206 | return masked; 207 | } 208 | 209 | public static inline function unmask(masked:Chunk, key:MaskingKey):Chunk { 210 | return mask(masked, key); 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /src/tink/websocket/IncomingHandshakeRequestHeader.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.http.Request; 4 | import tink.io.StreamParser; 5 | 6 | using StringTools; 7 | using tink.CoreApi; 8 | 9 | abstract IncomingHandshakeRequestHeader(IncomingRequestHeader) from IncomingRequestHeader to IncomingRequestHeader { 10 | 11 | public var key(get, never):String; 12 | 13 | public function validate() { 14 | 15 | var errors = []; 16 | function ensureHeader(name:String, check:String->Bool) 17 | switch this.byName(name) { 18 | case Failure(f): errors.push('Header $name not found'); 19 | case Success(v) if(check(v)): // ok 20 | case Success(v): errors.push('Invalid header "$name: $v"'); 21 | } 22 | 23 | ensureHeader('upgrade', function(v) return v == 'websocket'); 24 | ensureHeader('connection', function(v) return v != null && [for(i in v.split(',')) i.trim().toLowerCase()].indexOf('upgrade') != -1); 25 | ensureHeader('sec-websocket-key', function(v) return v != null); 26 | ensureHeader('sec-websocket-version', function(v) return v == '13'); 27 | 28 | return 29 | if(errors.length > 0) 30 | Failure(Error.withData('Invalid request header', errors)); 31 | else 32 | Success(Noise); 33 | } 34 | 35 | inline function get_key() return this.byName('sec-websocket-key').sure(); 36 | 37 | public static inline function parser():StreamParser 38 | return IncomingRequestHeader.parser(); 39 | } -------------------------------------------------------------------------------- /src/tink/websocket/IncomingHandshakeResponseHeader.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.http.Response; 4 | import tink.io.StreamParser; 5 | 6 | using tink.CoreApi; 7 | 8 | abstract IncomingHandshakeResponseHeader(ResponseHeader) from ResponseHeader to ResponseHeader { 9 | 10 | public function validate(accept:String) { 11 | if(this.statusCode != 101) return Failure(new Error('Unexpected response status code')); 12 | return switch this.byName('sec-websocket-accept') { 13 | case Success(v) if(v == accept): Success(Noise); 14 | default: Failure(new Error('Invalid accept')); 15 | } 16 | } 17 | 18 | public static inline function parser():StreamParser 19 | return ResponseHeader.parser(); 20 | } -------------------------------------------------------------------------------- /src/tink/websocket/MaskingKey.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import haxe.io.Bytes; 4 | 5 | @:forward 6 | abstract MaskingKey(Chunk) { 7 | public function new(a, b, c, d) { 8 | var bytes = Bytes.alloc(4); 9 | bytes.set(0, a); 10 | bytes.set(1, b); 11 | bytes.set(2, c); 12 | bytes.set(3, d); 13 | this = bytes; 14 | } 15 | 16 | @:from 17 | public static function ofChunk(c:Chunk):MaskingKey { 18 | if(c.length != 4) throw 'Invalid key length, should be 4'; 19 | return cast c; 20 | } 21 | 22 | public static function random() { 23 | return new MaskingKey(Std.random(256), Std.random(256), Std.random(256), Std.random(256)); 24 | } 25 | } -------------------------------------------------------------------------------- /src/tink/websocket/Message.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | enum Message { 4 | Text(v:String); 5 | Binary(b:Chunk); 6 | } 7 | -------------------------------------------------------------------------------- /src/tink/websocket/MessageStream.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.streams.Stream; 4 | 5 | @:forward 6 | abstract MessageStream(Stream) from Stream to Stream { 7 | @:from 8 | public static function fromRawStream(raw:RawMessageStream):MessageStream 9 | return raw.regroup(function(m:Array):RegroupResult return Converted(switch m[0] { 10 | case Text(v): Stream.single(Message.Text(v)); 11 | case Binary(v): Stream.single(Message.Binary(v)); 12 | default: Empty.make(); 13 | })); 14 | 15 | @:to 16 | public function toRawStream():RawMessageStream 17 | return this.regroup(function(m:Array) return Converted(switch m[0] { 18 | case Text(v): Stream.single(RawMessage.Text(v)); 19 | case Binary(v): Stream.single(RawMessage.Binary(v)); 20 | })); 21 | } -------------------------------------------------------------------------------- /src/tink/websocket/OutgoingHandshakeRequestHeader.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import haxe.io.Bytes; 4 | import haxe.crypto.*; 5 | import tink.http.Request; 6 | import tink.http.Header; 7 | import tink.Url; 8 | 9 | class OutgoingHandshakeRequestHeader extends OutgoingRequestHeader { 10 | 11 | public var key(default, null):String; 12 | public var accept(default, null):String; 13 | 14 | public function new(url:Url, ?key, ?fields) { 15 | super(GET, url, null, fields); 16 | 17 | this.key = switch key { 18 | case null: Base64.encode(Sha1.make(Bytes.ofString(Std.string(Math.random())))); 19 | default: key; 20 | } 21 | accept = Base64.encode(Sha1.make(Bytes.ofString(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); 22 | 23 | function fillHeader(name:String, value:String) { 24 | switch byName(name) { 25 | case Failure(_): this.fields.push(new HeaderField(name, value)); 26 | default: 27 | } 28 | } 29 | 30 | fillHeader('host', url.host); 31 | fillHeader('upgrade', 'websocket'); 32 | fillHeader('connection', 'upgrade'); 33 | fillHeader('sec-websocket-key', this.key); 34 | fillHeader('sec-websocket-version', '13'); 35 | } 36 | } -------------------------------------------------------------------------------- /src/tink/websocket/OutgoingHandshakeResponseHeader.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import haxe.io.Bytes; 4 | import haxe.crypto.*; 5 | import tink.http.Response; 6 | import tink.http.Header; 7 | 8 | class OutgoingHandshakeResponseHeader extends ResponseHeaderBase { 9 | 10 | public var key(default, null):String; 11 | public var accept(default, null):String; 12 | 13 | public function new(key, ?fields) { 14 | super(101, 'Switching Protocols', fields); 15 | 16 | accept = Base64.encode(Sha1.make(Bytes.ofString(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); 17 | 18 | function fillHeader(name:String, value:String) { 19 | switch byName(name) { 20 | case Failure(_): this.fields.push(new HeaderField(name, value)); 21 | default: 22 | } 23 | } 24 | 25 | fillHeader('upgrade', 'websocket'); 26 | fillHeader('connection', 'upgrade'); 27 | fillHeader('sec-websocket-accept', accept); 28 | } 29 | } -------------------------------------------------------------------------------- /src/tink/websocket/Parser.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import haxe.ds.Option; 4 | import tink.io.StreamParser; 5 | import tink.Chunk; 6 | import tink.chunk.*; 7 | 8 | using tink.CoreApi; 9 | 10 | class Parser implements StreamParserObject { 11 | var mask:Bool; // mask bit of the frame 12 | var length = 0; // total length of frame 13 | var required = 0; // required length for next read 14 | var out:Chunk; 15 | 16 | public function new() { 17 | reset(); 18 | } 19 | 20 | public function eof(rest:ChunkCursor) { 21 | return switch progress(rest) { 22 | case Done(result): Success(result); 23 | case Progressed: Failure(new Error('Unexpected end of input')); 24 | case Failed(e): Failure(e); 25 | } 26 | } 27 | 28 | public function progress(cursor:ChunkCursor) { 29 | if(cursor.length < required) return Progressed; 30 | return switch length { 31 | case 0: 32 | cursor.next(); 33 | var secondByte = cursor.currentByte; 34 | mask = secondByte >> 7 == 1; 35 | required = switch secondByte & 127 { 36 | case 127: length = -2; 8; 37 | case 126: length = -1; 2; 38 | case len: length = len + 2 + (mask ? 4 : 0); length - 2; 39 | } 40 | cursor.next(); 41 | out = out & cursor.left(); 42 | Progressed; 43 | 44 | case -1 | -2: 45 | length = 0; 46 | for(i in 0...required) { 47 | length = (length << 8) | cursor.currentByte; 48 | cursor.next(); 49 | } 50 | length += 2 + required + (mask ? 4 : 0); 51 | required = length - 2 - required; 52 | out = out & cursor.left(); 53 | Progressed; 54 | 55 | default: 56 | var ret = Done(out & cursor.right().slice(0, required)); 57 | cursor.moveBy(required); 58 | reset(); 59 | ret; 60 | } 61 | } 62 | 63 | function reset() { 64 | out = Chunk.EMPTY; 65 | length = 0; 66 | required = 2; 67 | } 68 | } -------------------------------------------------------------------------------- /src/tink/websocket/PongStream.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.websocket.RawMessage; 4 | import tink.streams.Stream; 5 | 6 | @:forward 7 | abstract PongStream(RawMessageStream) to RawMessageStream { 8 | public function new(raw:RawMessageStream) 9 | this = raw.regroup(function(m) return Converted(switch m[0] { 10 | case Ping(v): Stream.single(Pong(v)); 11 | default: Empty.make(); 12 | })); 13 | } -------------------------------------------------------------------------------- /src/tink/websocket/RawMessage.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.Chunk; 4 | 5 | enum RawMessage { 6 | Text(v:String); 7 | Binary(b:Chunk); 8 | ConnectionClose; 9 | Ping(b:Chunk); 10 | Pong(b:Chunk); 11 | } 12 | -------------------------------------------------------------------------------- /src/tink/websocket/RawMessageStream.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.Chunk; 4 | import tink.streams.Stream; 5 | 6 | using tink.CoreApi; 7 | 8 | @:forward 9 | abstract RawMessageStream(Stream) to Stream { 10 | 11 | public inline function append(other:RawMessageStream):RawMessageStream 12 | return this.append(other); 13 | 14 | public inline function prepend(other:RawMessageStream):RawMessageStream 15 | return this.prepend(other); 16 | 17 | public inline function filter(f:Filter):RawMessageStream 18 | return this.filter(f); 19 | 20 | public inline function blend(other:RawMessageStream):RawMessageStream 21 | return this.blend(other); 22 | 23 | @:from // `@:from` has higher priority than `from`, this prevents the value being inferred as chunk/frame stream 24 | public static inline function lift(s:Stream):RawMessageStream 25 | return cast s; // FIXME: the `cast` is to mitigate "Recursive implicit cast" 26 | 27 | @:from 28 | public static inline function ofChunkStream(s:Stream):RawMessageStream 29 | return ofFrameStream(s.map(Frame.fromChunk)); 30 | 31 | @:from 32 | public static inline function ofFrameStream(s:Stream):RawMessageStream 33 | return s.regroup(MessageRegrouper.get()); 34 | 35 | public inline function toUnmaskedChunkStream():Stream 36 | return toMaskedChunkStream(function() return null); 37 | 38 | public inline function toUnmaskedFrameStream():Stream 39 | return toMaskedFrameStream(function() return null); 40 | 41 | public function toMaskedChunkStream(key:Void->MaskingKey):Stream 42 | return toMaskedFrameStream(key).map(function(f:Frame) return f.toChunk()); 43 | 44 | public function toMaskedFrameStream(key:Void->MaskingKey):Stream 45 | return this.map(function(message) return Frame.ofMessage(message, key())); 46 | } 47 | 48 | class MessageRegrouper { 49 | public static function transform(s:Stream) 50 | return s.map(Frame.fromChunk).regroup(get()); 51 | 52 | public static function get():Regrouper 53 | return cast inst; 54 | 55 | static var inst:Regrouper = 56 | function(frames:Array, s) { 57 | var last = frames[frames.length - 1]; 58 | if(!last.fin) return Untouched; 59 | 60 | function mergeBytes() { 61 | var out = Chunk.EMPTY; 62 | for(frame in frames) out = out & frame.unmaskedPayload; 63 | return out; 64 | } 65 | 66 | return Converted(Stream.single(switch frames[0].opcode { 67 | case Continuation: 68 | throw 'Unreachable'; // technically 69 | case Text: 70 | RawMessage.Text(mergeBytes().toString()); 71 | case Binary: 72 | RawMessage.Binary(mergeBytes()); 73 | case ConnectionClose: 74 | RawMessage.ConnectionClose; 75 | case Ping: 76 | RawMessage.Ping(mergeBytes()); 77 | case Pong: 78 | RawMessage.Pong(mergeBytes()); 79 | })); 80 | } 81 | } -------------------------------------------------------------------------------- /src/tink/websocket/Server.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.http.Request; 4 | import tink.websocket.Message; 5 | 6 | using tink.CoreApi; 7 | 8 | interface Server { 9 | var clientConnected(default, null):Signal; 10 | var errors(default, null):Signal; 11 | function close():Future; 12 | } 13 | 14 | interface ConnectedClient { 15 | var clientIp(default, null):String; 16 | var header(default, null):IncomingRequestHeader; 17 | var closed(default, null):Future; 18 | var messageReceived(default, null):Signal; 19 | function send(message:Message):Void; 20 | function close():Void; 21 | } 22 | -------------------------------------------------------------------------------- /src/tink/websocket/ServerHandler.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket; 2 | 3 | import tink.streams.Stream; 4 | import tink.streams.IdealStream; 5 | import tink.streams.RealStream; 6 | import tink.io.StreamParser; 7 | import tink.http.Request; 8 | import tink.http.Response; 9 | import tink.Url; 10 | import tink.Chunk; 11 | 12 | using tink.CoreApi; 13 | using tink.io.Source; 14 | 15 | typedef Incoming = { 16 | clientIp:String, 17 | header:IncomingHandshakeRequestHeader, 18 | stream:RawMessageStream, 19 | } 20 | typedef ServerHandler = Incoming->RawMessageStream; 21 | 22 | class ServerHandlerTools { 23 | #if tink_tcp 24 | public static function toTcpHandler(handler:ServerHandler, ?onError:Error->Void):tink.tcp.Handler { 25 | if(onError == null) onError = function(e) trace(e); 26 | 27 | return function(i:tink.tcp.Incoming):Future { 28 | return Future.sync({ 29 | stream: Generator.stream(function(step) { 30 | i.stream.parse(IncomingHandshakeRequestHeader.parser()) 31 | .handle(function(o) switch o { 32 | case Success({a: header, b: rest}): 33 | switch header.validate() { 34 | case Success(_): // ok 35 | case Failure(e): onError(e); 36 | } 37 | var reponseHeader = new OutgoingHandshakeResponseHeader(header.key); 38 | step(Link((reponseHeader.toString():Chunk), handler({ 39 | clientIp: i.from.toString(), 40 | header: header, 41 | stream: rest.parseStream(new Parser()), 42 | }).toUnmaskedChunkStream())); 43 | case Failure(e): 44 | onError(e); 45 | }); 46 | }), 47 | allowHalfOpen: true, 48 | }); 49 | } 50 | } 51 | #end 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/tink/websocket/clients/HttpConnector.hx: -------------------------------------------------------------------------------- 1 | 2 | package tink.websocket.clients; 3 | 4 | import haxe.io.Bytes; 5 | import tink.Url; 6 | import tink.websocket.Client; 7 | import tink.websocket.RawMessage; 8 | import tink.websocket.MaskingKey; 9 | import tink.websocket.IncomingHandshakeResponseHeader; 10 | import tink.websocket.OutgoingHandshakeRequestHeader; 11 | import tink.http.Request; 12 | import tink.http.Response; 13 | import tink.streams.Stream; 14 | import tink.streams.IdealStream; 15 | import tink.streams.RealStream; 16 | import tink.Url; 17 | import js.html.*; 18 | 19 | using tink.io.Source; 20 | using tink.CoreApi; 21 | 22 | /** 23 | * Only works if the http client supports streaming 24 | */ 25 | class HttpConnector implements Connector { 26 | 27 | var url:Url; 28 | var client:tink.http.Client; 29 | 30 | public function new(url, client) { 31 | this.url = url; 32 | this.client = client; 33 | } 34 | 35 | public function connect(outgoing:RawMessageStream):RawMessageStream { 36 | 37 | var requestHeader = new OutgoingHandshakeRequestHeader(url); 38 | var promise = client.request(new OutgoingRequest( 39 | requestHeader, 40 | RawMessageStream.lift(outgoing).toMaskedChunkStream(MaskingKey.random) 41 | )) 42 | .next(function(res) { 43 | return Promise.lift((res.header:IncomingHandshakeResponseHeader).validate(requestHeader.accept)) 44 | .next(function(_) return RawMessageStream.ofChunkStream(res.body.parseStream(new Parser()))); 45 | }); 46 | 47 | return Stream.promise(promise); 48 | } 49 | } -------------------------------------------------------------------------------- /src/tink/websocket/clients/JsConnector.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket.clients; 2 | 3 | import haxe.io.Bytes; 4 | import tink.Url; 5 | import tink.websocket.Client; 6 | import tink.websocket.RawMessage; 7 | import tink.streams.Stream; 8 | import tink.streams.IdealStream; 9 | import tink.streams.RealStream; 10 | import js.html.*; 11 | 12 | using tink.CoreApi; 13 | 14 | class JsConnector implements Connector { 15 | 16 | var url:String; 17 | 18 | public function new(url) 19 | this.url = url; 20 | 21 | public function connect(outgoing:RawMessageStream):RawMessageStream { 22 | var ws = new WebSocket(url); 23 | ws.binaryType = BinaryType.ARRAYBUFFER; 24 | 25 | var trigger = Signal.trigger(); 26 | var incoming = new SignalStream(trigger.asSignal()); 27 | 28 | var opened = Future.async(function(cb) ws.onopen = cb.bind(Noise)); 29 | ws.onclose = function() trigger.trigger(End); 30 | ws.onerror = function(e) trigger.trigger(Fail(Error.withData('WebSocket Error', e))); 31 | ws.onmessage = function(m:{data:Any}) trigger.trigger(Data( 32 | if(Std.is(m.data, String)) Text(m.data); 33 | else Binary(Bytes.ofData(m.data)) 34 | )); 35 | 36 | opened.handle(function(_) { 37 | outgoing.forEach(function(message) { 38 | switch message { 39 | case Text(v): ws.send(v); 40 | case Binary(v): ws.send(v.toBytes().getData()); 41 | case _: // js client cannot handle raw messages 42 | } 43 | return Resume; 44 | }).handle(function(o) switch o { 45 | case Depleted: ws.close(); 46 | case Halted(_): throw 'unreachable'; 47 | }); 48 | }); 49 | 50 | return incoming; 51 | } 52 | } -------------------------------------------------------------------------------- /src/tink/websocket/clients/TcpConnector.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket.clients; 2 | 3 | import haxe.io.Bytes; 4 | import tink.Url; 5 | import tink.websocket.Client; 6 | import tink.websocket.RawMessage; 7 | import tink.websocket.MaskingKey; 8 | import tink.streams.Stream; 9 | import tink.streams.IdealStream; 10 | import tink.streams.RealStream; 11 | import tink.Url; 12 | import js.html.*; 13 | 14 | using tink.websocket.ClientHandler; 15 | using tink.CoreApi; 16 | 17 | class TcpConnector implements Connector { 18 | 19 | var url:Url; 20 | 21 | public function new(url) 22 | this.url = url; 23 | 24 | public function connect(outgoing:RawMessageStream):RawMessageStream { 25 | var stream = Stream.promise(Future.async(function(cb) { 26 | var handler:ClientHandler = function(stream) { 27 | cb(Success(stream)); 28 | var pong = new PongStream(stream).idealize(function(e) return Empty.make()); 29 | return RawMessageStream.lift(outgoing).blend(pong); 30 | } 31 | 32 | var port = switch [url.host.port, url.scheme] { 33 | case [null, 'wss' | 'https']: 443; 34 | case [null, _]: 80; 35 | case [v, _]: v; 36 | } 37 | 38 | tink.tcp.nodejs.NodejsConnector.connect({host: url.host.name, port: port}, handler.toTcpHandler(url)) 39 | .eager(); 40 | })); 41 | 42 | return stream; 43 | } 44 | } -------------------------------------------------------------------------------- /src/tink/websocket/servers/NodeWsServer.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket.servers; 2 | 3 | #if nodejs 4 | import tink.http.Request; 5 | import tink.websocket.Message; 6 | import tink.websocket.Server; 7 | import haxe.Constraints; 8 | import haxe.extern.EitherType; 9 | import js.html.ArrayBuffer; 10 | import js.node.http.IncomingMessage; 11 | 12 | using tink.CoreApi; 13 | 14 | class NodeWsServer implements Server { 15 | public var clientConnected(default, null):Signal; 16 | public var errors(default, null):Signal; 17 | 18 | var server:NativeServer; 19 | 20 | public function new(opt) { 21 | server = new NativeServer(opt); 22 | errors = Signal.generate(function(trigger) { 23 | server.on('error', function(err) trigger(Error.ofJsError(err))); 24 | }); 25 | 26 | clientConnected = Signal.generate(function(trigger) { 27 | server.on('connection', function(socket, request:IncomingMessage) { 28 | trigger((new NodeWsConnectedClient( 29 | request.connection.remoteAddress, 30 | IncomingRequestHeader.fromIncomingMessage(request), 31 | socket 32 | ):ConnectedClient)); 33 | }); 34 | }); 35 | } 36 | 37 | public function close():Future { 38 | return Future.async(function(cb) { 39 | server.close(cb.bind(Noise)); 40 | }); 41 | } 42 | } 43 | 44 | class NodeWsConnectedClient implements ConnectedClient { 45 | 46 | public var clientIp(default, null):String; 47 | public var header(default, null):IncomingRequestHeader; 48 | public var closed(default, null):Future; 49 | public var messageReceived(default, null):Signal; 50 | 51 | var socket:NativeSocket; 52 | 53 | public function new(clientIp, header, socket) { 54 | this.clientIp = clientIp; 55 | this.header = header; 56 | this.socket = socket; 57 | 58 | closed = Future.async(function(cb) { 59 | socket.once('close', cb.bind(Noise)); 60 | }); 61 | 62 | messageReceived = Signal.generate(function(trigger) { 63 | socket.on('message', function(message) { 64 | trigger(Text(message)); 65 | }); 66 | }); 67 | } 68 | 69 | public function send(message:Message):Void { 70 | socket.send(switch message { 71 | case Text(v): v; 72 | case Binary(v): v.toBytes().getData(); 73 | }); 74 | } 75 | public function close():Void { 76 | socket.close(); 77 | } 78 | } 79 | 80 | @:jsRequire('ws', 'Server') 81 | private extern class NativeServer { 82 | function new(opt:{}); 83 | function on(event:String, f:Function):Void; 84 | function close(f:Function):Void; 85 | } 86 | 87 | extern class NativeSocket { 88 | function on(event:String, f:Function):Void; 89 | function once(event:String, f:Function):Void; 90 | function send(data:EitherType):Void; 91 | function close():Void; 92 | } 93 | #end -------------------------------------------------------------------------------- /src/tink/websocket/servers/TinkServer.hx: -------------------------------------------------------------------------------- 1 | package tink.websocket.servers; 2 | 3 | import tink.http.Request; 4 | import tink.websocket.Server; 5 | import tink.websocket.RawMessage; 6 | import tink.websocket.RawMessageStream; 7 | import tink.streams.Stream; 8 | import tink.streams.RealStream; 9 | import tink.streams.IdealStream; 10 | import tink.Chunk; 11 | 12 | using tink.CoreApi; 13 | 14 | class TinkServer implements Server { 15 | 16 | public var clients(default, null):Array; 17 | public var clientConnected(default, null):Signal; 18 | public var errors(default, null):Signal; 19 | 20 | var connectedTrigger:SignalTrigger; 21 | var errorsTrigger:SignalTrigger; 22 | 23 | public function new() { 24 | clients = []; 25 | clientConnected = connectedTrigger = Signal.trigger(); 26 | errors = errorsTrigger = Signal.trigger(); 27 | } 28 | 29 | public function close():Future { 30 | throw 'not implemented'; 31 | } 32 | 33 | public function handle(i):RawMessageStream { 34 | var client = new TinkConnectedClient(i.clientIp, i.header, i.stream); 35 | clients.push(client); 36 | client.closed.handle(function() clients.remove(client)); 37 | connectedTrigger.trigger(client); 38 | client.listen(); 39 | return client.outgoing; 40 | } 41 | } 42 | 43 | @:allow(tink.websocket) 44 | class TinkConnectedClient implements ConnectedClient { 45 | public var clientIp(default, null):String; 46 | public var header(default, null):IncomingRequestHeader; 47 | 48 | public var closed(default, null):Future; 49 | public var messageReceived(default, null):Signal; 50 | var closedTrigger:FutureTrigger; 51 | var messageReceivedTrigger:SignalTrigger; 52 | 53 | var outgoingTrigger:SignalTrigger>; 54 | var outgoing:RawMessageStream; 55 | 56 | var incoming:RawMessageStream; 57 | 58 | public function new(clientIp, header, incoming) { 59 | 60 | this.clientIp = clientIp; 61 | this.header = header; 62 | this.incoming = incoming; 63 | 64 | closed = closedTrigger = Future.trigger(); 65 | messageReceived = messageReceivedTrigger = Signal.trigger(); 66 | 67 | outgoingTrigger = Signal.trigger(); 68 | outgoing = new SignalStream(outgoingTrigger); 69 | } 70 | 71 | function listen() { 72 | incoming.forEach(function(message:RawMessage) { 73 | switch message { 74 | case Text(v): messageReceivedTrigger.trigger(Text(v)); 75 | case Binary(v): messageReceivedTrigger.trigger(Binary(v)); 76 | case Ping(v): outgoingTrigger.trigger(Data(Pong(v))); 77 | case Pong(_): // do nothing; 78 | case ConnectionClose: 79 | close(); 80 | return Finish; 81 | } 82 | return Resume; 83 | }).eager(); 84 | } 85 | 86 | public function send(message:Message):Void { 87 | outgoingTrigger.trigger(Data(switch message { 88 | case Text(v): Text(v); 89 | case Binary(v): Binary(v); 90 | })); 91 | } 92 | 93 | public function close() { 94 | outgoingTrigger.trigger(End); 95 | closedTrigger.trigger(Noise); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | -cp tests 2 | -main RunTests 3 | -dce full 4 | 5 | -lib tink_unittest 6 | -lib tink_tcp 7 | -lib tink_http_middleware 8 | -------------------------------------------------------------------------------- /tests/ClientTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.io.Bytes; 4 | import tink.unit.*; 5 | import tink.streams.Stream; 6 | import tink.websocket.*; 7 | import tink.websocket.Client; 8 | import tink.websocket.clients.*; 9 | import tink.Chunk; 10 | 11 | using tink.CoreApi; 12 | using tink.io.Source; 13 | 14 | @:asserts 15 | @:timeout(10000) 16 | class ClientTest { 17 | var url = 'ws://echo.websocket.org'; 18 | 19 | public function new() {} 20 | 21 | #if nodejs 22 | public function tcp() return run(asserts, new TcpConnector(url)); 23 | // public function http() return run(asserts, new HttpConnector(url, new tink.http.clients.NodeClient())); // FIXME: no res in http client request? 24 | #elseif js 25 | public function js() return run(asserts, new JsConnector(url)); 26 | #end 27 | 28 | function run(asserts:AssertionBuffer, connector:Connector) { 29 | var c = 0; 30 | var n = 7; 31 | var sender = Signal.trigger(); 32 | connector.connect(new SignalStream(sender)).forEach(function(message:RawMessage) { 33 | switch message { 34 | case Text(v): asserts.assert(v == 'payload' + c++); 35 | default: asserts.fail('Unexpected message'); 36 | } 37 | return if(c < n) { 38 | sender.trigger(Data(RawMessage.Text('payload$c'))); 39 | Resume; 40 | } else { 41 | sender.trigger(End); 42 | asserts.done(); 43 | Finish; 44 | } 45 | }).handle(function(o) trace(Std.string(o))); 46 | sender.trigger(Data(RawMessage.Text('payload$c'))); 47 | return asserts; 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /tests/HeaderTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.websocket.*; 4 | import tink.http.Request; 5 | import tink.http.Header; 6 | 7 | @:asserts 8 | class HeaderTest { 9 | public function new() {} 10 | 11 | public function connection() { 12 | var header:IncomingHandshakeRequestHeader = new IncomingRequestHeader(null, null, [ 13 | new HeaderField('upgrade', 'websocket'), 14 | new HeaderField('connection', 'keep-alive, upgrade'), 15 | new HeaderField('sec-websocket-key', ''), 16 | new HeaderField('sec-websocket-version', '13'), 17 | ]); 18 | asserts.assert(header.validate()); 19 | return asserts.done(); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/MiddlewareExample.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.websocket.*; 4 | import tink.http.containers.*; 5 | import tink.http.middleware.*; 6 | import tink.http.Response; 7 | import tink.http.Request; 8 | import tink.http.Handler; 9 | import tink.websocket.ServerHandler; 10 | import tink.websocket.servers.TinkServer; 11 | import tink.streams.Stream.Empty; 12 | 13 | using tink.CoreApi; 14 | 15 | class MiddlewareExample { 16 | static function main() { 17 | var server = new TinkServer(); 18 | server.clientConnected.handle(function(client) { 19 | trace('client connected from ${client.clientIp}'); 20 | client.messageReceived.handle(function(message) trace(message)); 21 | client.send(Text('Hello!')); 22 | }); 23 | 24 | var handler:Handler = req -> Future.sync(('Done':OutgoingResponse)); 25 | 26 | handler = handler.applyMiddleware(new WebSocket( 27 | // Choose either one from the following 2 lines: 28 | // websocketHandler 29 | server.handle 30 | )); 31 | 32 | var container = new NodeContainer(8081, {upgradable: true}); 33 | container.run(handler).eager(); 34 | } 35 | 36 | static function websocketHandler(incoming:Incoming):RawMessageStream { 37 | trace(incoming.clientIp); 38 | var source = incoming.stream; 39 | return source.idealize(_ -> Empty.make()); 40 | } 41 | } 42 | 43 | /* 44 | Run this in browser: 45 | var ws = new WebSocket('ws://localhost:8081'); 46 | ws.onopen = function() { setInterval(function() { ws.send(new Date().toString()); }, 1000); } 47 | ws.onmessage = console.log; 48 | */ 49 | -------------------------------------------------------------------------------- /tests/ParserTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.io.Bytes; 4 | import tink.streams.Stream; 5 | import tink.websocket.*; 6 | import tink.Chunk; 7 | 8 | using tink.CoreApi; 9 | using tink.io.Source; 10 | 11 | @:asserts 12 | class ParserTest { 13 | public function new() {} 14 | 15 | @:variant((this.arrayToBytes([129, 131, 61]):tink.io.Source.IdealSource).append(this.arrayToBytes([84, 35, 6, 112, 16, 109])), '81833d54230670106d', '3d542306', '70106d', 'MDN') 16 | @:variant(this.arrayToBytes([129, 131, 61, 84, 35, 6, 112, 16, 109]), '81833d54230670106d', '3d542306', '70106d', 'MDN') 17 | @:variant(tink.Chunk.ofHex('818823fb87c8539afea44c9ae3f9'), '818823fb87c8539afea44c9ae3f9', '23fb87c8', '539afea44c9ae3f9', 'payload1') 18 | public function parseSingleFrame(source:IdealSource, whole:String, key:String, masked:String, unmasked:String) { 19 | source.parseStream(new Parser()).forEach(function(chunk:Chunk) { 20 | var frame:Frame = chunk; 21 | asserts.assert(chunk.toBytes().toHex() == whole); 22 | asserts.assert(frame.fin == true); 23 | asserts.assert(frame.opcode == 1); 24 | asserts.assert(frame.masked == true); 25 | asserts.assert(frame.maskingKey.toHex() == key); 26 | asserts.assert(frame.maskedPayload.toHex() == masked); 27 | asserts.assert(frame.unmaskedPayload.toString() == unmasked); 28 | return Resume; 29 | }).handle(function(o) { 30 | asserts.assert(o == Depleted); 31 | asserts.done(); 32 | }); 33 | return asserts; 34 | } 35 | 36 | public function parseConsecutiveFrame() { 37 | var frame = [129, 131, 61, 84, 35, 6, 112, 16, 109]; 38 | var source:IdealSource = arrayToBytes(frame.concat(frame).concat(frame)); 39 | var num = 0; 40 | source.parseStream(new Parser()).forEach(function(chunk:Chunk) { 41 | asserts.assert(chunk.toBytes().toHex() == '81833d54230670106d'); 42 | var frame:Frame = chunk; 43 | asserts.assert(frame.fin == true); 44 | asserts.assert(frame.opcode == 1); 45 | asserts.assert(frame.masked == true); 46 | asserts.assert(frame.maskingKey.toHex() == '3d542306'); 47 | asserts.assert(frame.maskedPayload.toHex() == '70106d'); 48 | asserts.assert(frame.unmaskedPayload.toString() == 'MDN'); 49 | num++; 50 | return Resume; 51 | }).handle(function(o) { 52 | asserts.assert(o == Depleted); 53 | asserts.assert(num == 3); 54 | asserts.done(); 55 | }); 56 | return asserts; 57 | } 58 | 59 | function arrayToBytes(a:Array):Chunk { 60 | var bytes = Bytes.alloc(a.length); 61 | for(i in 0...a.length) bytes.set(i, a[i]); 62 | return bytes; 63 | } 64 | } -------------------------------------------------------------------------------- /tests/Playground.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.websocket.*; 4 | import tink.streams.Stream; 5 | import tink.streams.Accumulator; 6 | 7 | using tink.CoreApi; 8 | 9 | class Playground { 10 | static function main() { 11 | var handler = Acceptor.wrap(function(stream) { 12 | var s = RawMessageStream.ofChunkStream(stream); 13 | s.forEach(function(m) { 14 | trace(Date.now().toString() + ': Received:' + Std.string(m)); 15 | return Resume; 16 | }).handle(function(_) {}); 17 | 18 | var pings = new Accumulator(); 19 | 20 | var out = s.filter(function(i:RawMessage) return i.match(RawMessage.Text(_))).blend(pings); 21 | 22 | out.forEach(function(m) { 23 | trace(Date.now().toString() + ': Sending:' + Std.string(m)); 24 | return Resume; 25 | }).handle(function(_) {}); 26 | 27 | var timer = new haxe.Timer(1000); 28 | timer.run = function() pings.yield(Data(RawMessage.Ping(tink.Chunk.EMPTY))); 29 | 30 | return out.toUnmaskedChunkStream().idealize(function(o) { 31 | trace(o); 32 | return Empty.make(); 33 | }); 34 | }); 35 | 36 | tink.tcp.nodejs.NodejsAcceptor.inst.bind(18088).handle(function(o) switch o { 37 | case Success(openPort): 38 | openPort.setHandler(handler); 39 | trace('running'); 40 | 41 | case Failure(e): 42 | trace('failed: $e'); 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/RunTests.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import tink.testrunner.*; 4 | import tink.unit.*; 5 | 6 | 7 | class RunTests { 8 | static function main() { 9 | Runner.run(TestBatch.make([ 10 | #if nodejs new TcpConnectorTest(), #end 11 | #if nodejs new TcpAcceptorTest(), #end 12 | new ParserTest(), 13 | new ClientTest(), 14 | new HeaderTest(), 15 | ])).handle(Runner.exit); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/TcpAcceptorTest.hx: -------------------------------------------------------------------------------- 1 | 2 | package; 3 | 4 | import haxe.io.Bytes; 5 | import tink.streams.Stream; 6 | import tink.websocket.*; 7 | import tink.Chunk; 8 | 9 | using tink.websocket.ServerHandler; 10 | using tink.CoreApi; 11 | using tink.io.Source; 12 | 13 | @:asserts 14 | class TcpAcceptorTest extends TcpConnectorTest { 15 | 16 | public function tcpServer() { 17 | 18 | var handler:ServerHandler = function(i) { 19 | // sends back the same frame unmasked 20 | return i.stream.idealize(function(_) return Empty.make()); 21 | } 22 | 23 | tink.tcp.nodejs.NodejsAcceptor.inst.bind(18088).handle(function(o) switch o { 24 | case Success(openPort): 25 | openPort.setHandler(handler.toTcpHandler()); 26 | _echo('http://localhost:18088', 'localhost', 18088, asserts).handle(function(_) { 27 | openPort.shutdown(true).handle(asserts.done); 28 | }); 29 | 30 | case Failure(e): 31 | asserts.fail(e); 32 | }); 33 | 34 | return asserts; 35 | } 36 | 37 | function http(port:Int, container:tink.http.Container, asserts:tink.unit.AssertionBuffer) { 38 | var handler:tink.http.Handler = function(req) return Future.sync(('done':tink.http.Response.OutgoingResponse)); 39 | handler = handler.applyMiddleware(new tink.http.middleware.WebSocket(function(i) return i.stream.idealize(function(_) return Empty.make()))); 40 | 41 | container.run(handler).handle(function(o) switch o { 42 | case Running(r): 43 | _echo('http://localhost:$port', 'localhost', port, asserts).handle(function(_) { 44 | // trace('shutting down'); 45 | // r.shutdown(false).handle(asserts.done); 46 | asserts.done(); 47 | }); 48 | 49 | case Shutdown: 50 | trace('shutdown'); 51 | 52 | case Failed(e): 53 | asserts.fail(e); 54 | }); 55 | 56 | 57 | return asserts; 58 | 59 | } 60 | 61 | // FIXME: timeout? 62 | // public function tcpContainer() { 63 | // var container = new tink.http.containers.TcpContainer(tink.tcp.nodejs.NodejsAcceptor.inst.bind.bind(18090)); 64 | // return http(18090, container, asserts); 65 | // } 66 | 67 | // public function nodeContainer() { 68 | // var container = new tink.http.containers.NodeContainer(18089); 69 | // return http(18089, container, asserts); 70 | // } 71 | } -------------------------------------------------------------------------------- /tests/TcpConnectorTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.io.Bytes; 4 | import tink.streams.Stream; 5 | import tink.websocket.*; 6 | import tink.Chunk; 7 | 8 | using tink.websocket.ClientHandler; 9 | using tink.CoreApi; 10 | using tink.io.Source; 11 | 12 | @:asserts 13 | class TcpConnectorTest { 14 | public function new() {} 15 | 16 | 17 | public function echo() { 18 | _echo('ws://echo.websocket.org', 'echo.websocket.org', 80, asserts).handle(function(_) asserts.done()); 19 | return asserts; 20 | } 21 | 22 | function _echo(url, host, port, asserts:tink.unit.AssertionBuffer) { 23 | return Future.async(function(cb) { 24 | var c = 0; 25 | var n = 7; 26 | var sender = Signal.trigger(); 27 | var outgoing = new SignalStream(sender); 28 | var handler:ClientHandler = function(stream) { 29 | stream.forEach(function(message:RawMessage) { 30 | switch message { 31 | case Text(v): asserts.assert(v == 'payload' + ++c); 32 | default: asserts.fail('Unexpected message'); 33 | } 34 | if(c == n) cb(Noise); 35 | return c < n ? Resume : Finish; 36 | }); 37 | 38 | return RawMessageStream.lift(outgoing); 39 | } 40 | 41 | tink.tcp.nodejs.NodejsConnector.connect({host: host, port: port}, handler.toTcpHandler(url)).eager(); 42 | 43 | for(i in 0...n) sender.trigger(Data(RawMessage.Text('payload' + (i + 1)))); 44 | sender.trigger(End); 45 | }); 46 | } 47 | } --------------------------------------------------------------------------------