├── .gitignore ├── .munit ├── .travis.yml ├── README.md ├── example ├── client │ ├── build.hxml │ ├── index.html │ └── src │ │ └── ExampleClient.hx └── server │ ├── build.hxml │ └── src │ └── ExampleServer.hx ├── haxelib.json ├── src └── com │ └── thomasuster │ └── ws │ ├── FrameReader.hx │ ├── FrameWriter.hx │ ├── HandShaker.hx │ ├── input │ ├── ByteStringer.hx │ ├── BytesInputMock.hx │ ├── BytesInputProxy.hx │ └── BytesInputReal.hx │ └── output │ ├── BytesOutputMock.hx │ ├── BytesOutputProxy.hx │ └── BytesOutputReal.hx ├── test.hxml └── test ├── TestMain.hx ├── TestSuite.hx ├── com └── thomasuster │ └── ws │ ├── ByteStringerTest.hx │ ├── FrameReaderTest.hx │ ├── FrameWriterTest.hx │ └── HandShakerTest.hx └── resources ├── chrome.txt └── safari.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | example/client/main-javascript.js 4 | example/server/Build.n -------------------------------------------------------------------------------- /.munit: -------------------------------------------------------------------------------- 1 | version=2.1.2 2 | src=test 3 | bin=build 4 | report=build/report 5 | hxml=test.hxml 6 | classPaths=src 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | install: 4 | - brew install neko haxe 5 | - cd .. 6 | - mkdir haxelib 7 | - haxelib setup haxelib 8 | 9 | before_script: 10 | - cd $TRAVIS_BUILD_DIR 11 | - haxelib install munit 12 | - haxelib install hamcrest 13 | - haxelib dev haxe-websocket-server . 14 | 15 | script: 16 | - haxe test.hxml 17 | - haxelib run munit test 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/thomasuster/haxe-websocket-server.svg?branch=master)](https://travis-ci.org/thomasuster/haxe-websocket-server) 2 | 3 | ## Haxe Websocket Server (UNDER DEVELOPMENT) 4 | 5 | A haxe to neko server implementation of websockets. 6 | 7 | ### Run the tests 8 | ``` 9 | git clone git@github.com:thomasuster/haxe-websocket-server.git 10 | haxelib dev haxe-websocket-server haxe-websocket-server 11 | cd haxe-websocket-server 12 | haxelib run munit test 13 | ``` 14 | 15 | ### Run the example 16 | 17 | 1. Run the example server 18 | ``` 19 | cd haxe-websocket-server/example/server 20 | haxe build.hxml 21 | neko Build.n 22 | ``` 23 | 24 | 2. In a new console build the example client 25 | ``` 26 | cd haxe-websocket-server/example/client 27 | haxe build.hxml 28 | open index.html 29 | ``` 30 | 31 | 3. Show the javascript console (alt+command+i for osx chrome) 32 | 4. You should see something like this... 33 | ``` 34 | ping 35 | pong 36 | ping 37 | pong 38 | ... 39 | ``` 40 | 41 | 5. Open haxe-websocket-server/example/ and see how it works! -------------------------------------------------------------------------------- /example/client/build.hxml: -------------------------------------------------------------------------------- 1 | -cp src 2 | -main ExampleClient 3 | -js main-javascript.js 4 | -D js-flatten 5 | -dce full -------------------------------------------------------------------------------- /example/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/client/src/ExampleClient.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import js.html.Uint8Array; 3 | import js.html.ArrayBuffer; 4 | import js.html.FileReader; 5 | import js.html.Event; 6 | import js.html.MessageEvent; 7 | import js.html.WebSocket; 8 | class ExampleClient { 9 | 10 | public var ws:WebSocket; 11 | 12 | static function main() { 13 | new ExampleClient(); 14 | } 15 | 16 | public function new():Void { 17 | connect(); 18 | loop(); 19 | } 20 | 21 | function loop():Void { 22 | var window:Dynamic = js.Browser.window; 23 | var rqf:Dynamic = window.requestAnimationFrame || 24 | window.webkitRequestAnimationFrame || 25 | window.mozRequestAnimationFrame; 26 | update(); 27 | rqf(loop); 28 | } 29 | 30 | function connect():Void { 31 | trace("connecting..."); 32 | ws = new WebSocket("ws://localhost:4000"); 33 | ws.onopen = handleJSOpen; 34 | ws.onclose = handleJSClose; 35 | ws.onmessage = cast handleJSMessage; 36 | ws.onerror = cast handleJSError; 37 | } 38 | 39 | function update():Void { 40 | if(ws.readyState == WebSocket.OPEN) { 41 | trace("ping"); 42 | ws.send('ping'); 43 | } 44 | } 45 | 46 | function handleJSOpen(evt:Event) { 47 | trace('connection open\n'); 48 | } 49 | 50 | function handleJSClose(evt:Event) { 51 | trace("connection closed\n"); 52 | } 53 | 54 | function handleJSMessage(evt:MessageEvent) { 55 | var fileReader:FileReader = new FileReader(); 56 | fileReader.onload = function() { 57 | var buffer:ArrayBuffer = fileReader.result; 58 | var view:Uint8Array = new Uint8Array(buffer); 59 | var s:String = ''; 60 | for (i in 0...view.length) { 61 | var char:Int = view[i]; 62 | s += String.fromCharCode(char); 63 | } 64 | trace(s); 65 | }; 66 | fileReader.readAsArrayBuffer(evt.data); 67 | } 68 | 69 | function handleJSError(evt:MessageEvent) { 70 | trace("Error: " + evt.data + '\n'); 71 | } 72 | } -------------------------------------------------------------------------------- /example/server/build.hxml: -------------------------------------------------------------------------------- 1 | -lib haxe-websocket-server 2 | -cp src 3 | -main ExampleServer 4 | -neko Build.n -------------------------------------------------------------------------------- /example/server/src/ExampleServer.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import com.thomasuster.ws.FrameWriter; 3 | import haxe.io.Bytes; 4 | import com.thomasuster.ws.FrameReader; 5 | import com.thomasuster.ws.HandShaker; 6 | import com.thomasuster.ws.output.BytesOutputReal; 7 | import com.thomasuster.ws.input.BytesInputReal; 8 | import sys.net.Host; 9 | import sys.net.Socket; 10 | class ExampleServer { 11 | 12 | var socket:Socket; 13 | var s:Socket; 14 | 15 | public static function main() { 16 | new ExampleServer(); 17 | } 18 | 19 | public function new():Void { 20 | start(); 21 | while(true) 22 | update(); 23 | } 24 | 25 | public function start():Void { 26 | socket = new Socket(); 27 | socket.setBlocking(true); 28 | socket.setTimeout(60); 29 | socket.bind(new Host('localhost'),4000); 30 | socket.listen(1); 31 | } 32 | 33 | public function update():Void { 34 | s = socket.accept(); 35 | 36 | var input:BytesInputReal = new BytesInputReal(); 37 | input.input = s.input; 38 | var output:BytesOutputReal = new BytesOutputReal(); 39 | output.output = s.output; 40 | 41 | var shaker:HandShaker = new HandShaker(); 42 | shaker.input = input; 43 | shaker.output = output; 44 | shaker.shake(); 45 | 46 | var reader:FrameReader = new FrameReader(); 47 | reader.input = input; 48 | 49 | while(true) { 50 | var toServer:Bytes = reader.read(); 51 | 52 | if(toServer.toString() == 'ping') { 53 | var writer:FrameWriter = new FrameWriter(); 54 | writer.output = output; 55 | writer.payload = Bytes.ofString('pong'); 56 | writer.write(); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haxe-websocket-server", 3 | "url": "", 4 | "classPath": "src/", 5 | "license": "MIT", 6 | "tags": [], 7 | "description": "", 8 | "version": "0.0.1", 9 | "releasenote": "", 10 | "contributors": [ "" ] 11 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/FrameReader.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import com.thomasuster.ws.input.BytesInputProxy; 3 | import haxe.io.BytesInput; 4 | import haxe.io.BytesBuffer; 5 | import haxe.io.Bytes; 6 | class FrameReader { 7 | 8 | public var input:BytesInputProxy; 9 | 10 | var payloadLength:Int; 11 | var hasMask:Bool; 12 | var mask:Bytes; 13 | var fin:Bool; 14 | var op:Int; 15 | var maxFrameIntroSize:Int; 16 | var bytes:Bytes; 17 | 18 | public function new():Void { 19 | maxFrameIntroSize = 14; 20 | bytes = Bytes.alloc(maxFrameIntroSize); 21 | mask = Bytes.alloc(4); 22 | } 23 | 24 | public function read():Bytes { 25 | bytes.fill(0,bytes.length,0); 26 | readFullBytes(2); 27 | parseFirstByte(); 28 | parseSecondByte(); 29 | if(payloadLength == 126) 30 | parseExtendedPayloadLength(); 31 | readFullBytes(4); 32 | parseMask(); 33 | return parseData(); 34 | } 35 | 36 | function parseFirstByte():Void { 37 | fin = parseInt(bytes.get(0),0,0) == 1; 38 | op = parseInt(bytes.get(0), 4,7); 39 | } 40 | 41 | function parseSecondByte():Void { 42 | hasMask = parseInt(bytes.get(1),0,0) == 1; 43 | payloadLength = parseInt(bytes.get(1), 1,7); 44 | } 45 | 46 | function parseExtendedPayloadLength():Void { 47 | readFullBytes(2); 48 | payloadLength = (bytes.get(0) << 8) | bytes.get(1); 49 | } 50 | 51 | function readFullBytes(len:Int ):Void { 52 | input.readFullBytes(bytes, 0, len); 53 | } 54 | 55 | function parseMask():Void { 56 | mask.blit(0, bytes, 0, 4); 57 | } 58 | 59 | function parseData():Bytes { 60 | var encoded:Bytes = Bytes.alloc(payloadLength); 61 | input.readFullBytes(encoded,0,payloadLength); 62 | var decoded:Bytes = encoded; 63 | for (i in 0...payloadLength) { 64 | var frame:Int = encoded.get(i); 65 | decoded.set(i, frame ^ mask.get(i % 4)); 66 | } 67 | return decoded; 68 | } 69 | 70 | function parseInt(byte:Int,start:Int, end:Int):Int { 71 | var length:Int = end-start+1; 72 | var lShift:Int = 24+start; 73 | var leftTrimmed:Int = (byte << lShift); 74 | return leftTrimmed >>> (lShift+(7-end)); 75 | } 76 | 77 | /* 78 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 79 | 0 1 2 3 //dec 80 | 0 1 2 3 //bytes 81 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 82 | +-+-+-+-+-------+-+-------------+-------------------------------+ 83 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 84 | |I|S|S|S| (4) |A| (7) | (16/64) | 85 | |N|V|V|V| |S| | (if payload len==126/127) | 86 | | |1|2|3| |K| | | 87 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 88 | | Extended payload length continued, if payload len == 127 | 89 | + - - - - - - - - - - - - - - - +-------------------------------+ 90 | | |Masking-key, if MASK set to 1 | 91 | +-------------------------------+-------------------------------+ 92 | | Masking-key (continued) | Payload Data | 93 | +-------------------------------- - - - - - - - - - - - - - - - + 94 | : Payload Data continued ... : 95 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 96 | | Payload Data continued ... | 97 | +---------------------------------------------------------------+ 98 | */ 99 | } 100 | -------------------------------------------------------------------------------- /src/com/thomasuster/ws/FrameWriter.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import com.thomasuster.ws.output.BytesOutputProxy; 3 | import haxe.io.Bytes; 4 | class FrameWriter { 5 | 6 | public var output:BytesOutputProxy; 7 | public var payload:Bytes; 8 | 9 | var mask:Bytes; 10 | var header:Bytes; 11 | 12 | public function new():Void { 13 | mask = Bytes.alloc(4); 14 | header = Bytes.alloc(14); 15 | } 16 | 17 | public function write():Void { 18 | header.fill(0,header.length,0); 19 | var numUsed:Int = 2; 20 | 21 | var b0:Int = 0; 22 | b0 |= 0x80; //FIN 23 | b0 |= 0x02; //BINARY OP 24 | header.set(0,b0); 25 | 26 | var b1:Int = 0; 27 | b1 |= 0x00; //MASK 28 | if(payload.length >= 126) { 29 | if(payload.length >= 65535) { 30 | numUsed = 10; 31 | b1 |= 127; 32 | } 33 | else { 34 | numUsed = 4; 35 | b1 |= 126; 36 | } 37 | } 38 | else 39 | b1 |= payload.length; 40 | header.set(1,b1); 41 | 42 | if(numUsed == 4) { 43 | var bExtended:Int = 0; 44 | bExtended |= payload.length; 45 | header.set(2, bExtended >>> 8); 46 | header.set(3, bExtended & 0x00FF); 47 | } 48 | else if(numUsed == 10) { 49 | var bExtended:Int = 0; 50 | bExtended |= payload.length; 51 | header.set(6, bExtended >>> 24); 52 | header.set(7, bExtended >>> 16); 53 | header.set(8, bExtended >>> 8); 54 | header.set(9, bExtended); 55 | } 56 | 57 | output.writeFullBytes(header, 0, numUsed); 58 | 59 | output.writeFullBytes(payload, 0, payload.length); 60 | } 61 | // 62 | // /* 63 | // https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 64 | // 0 1 2 3 //dec 65 | // 0 1 2 3 //bytes 66 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 67 | // +-+-+-+-+-------+-+-------------+-------------------------------+ 68 | // |F|R|R|R| opcode|M| Payload len | Extended payload length | 69 | // |I|S|S|S| (4) |A| (7) | (16/64) | 70 | // |N|V|V|V| |S| | (if payload len==126/127) | 71 | // | |1|2|3| |K| | | 72 | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 73 | // | Extended payload length continued, if payload len == 127 | 74 | // + - - - - - - - - - - - - - - - +-------------------------------+ 75 | // | |Masking-key, if MASK set to 1 | 76 | // +-------------------------------+-------------------------------+ 77 | // | Masking-key (continued) | Payload Data | 78 | // +-------------------------------- - - - - - - - - - - - - - - - + 79 | // : Payload Data continued ... : 80 | // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 81 | // | Payload Data continued ... | 82 | // +---------------------------------------------------------------+ 83 | // */ 84 | } 85 | -------------------------------------------------------------------------------- /src/com/thomasuster/ws/HandShaker.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import haxe.io.Eof; 3 | import com.thomasuster.ws.input.BytesInputProxy; 4 | import com.thomasuster.ws.output.BytesOutputProxy; 5 | import haxe.io.Output; 6 | import haxe.io.Bytes; 7 | import haxe.crypto.Sha1; 8 | import haxe.crypto.Base64; 9 | class HandShaker { 10 | 11 | public var input:BytesInputProxy; 12 | public var output:BytesOutputProxy; 13 | 14 | var key:String = null; 15 | 16 | public function new() {} 17 | 18 | public function shake():Void { 19 | parseKey(); 20 | writeShake(); 21 | } 22 | 23 | function parseKey():Void { 24 | key = null; 25 | try { 26 | while(true) { 27 | var s:String = input.readLine(); 28 | var index:Int = s.indexOf('Sec-WebSocket-Key:'); 29 | if(index >= 0) { 30 | key = s.substring(index+'Sec-WebSocket-Key:'.length+1,s.length-index+1); 31 | } 32 | if(s.length == 0) 33 | break; 34 | } 35 | } 36 | catch(eof:Eof) { 37 | 38 | } 39 | } 40 | function writeShake():Void { 41 | var shake:String = hash(key); 42 | var header:String = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $shake\r\n\r\n'; 43 | writeString(header); 44 | } 45 | 46 | function writeString(string:String):Void { 47 | var b = Bytes.ofString(string); 48 | output.writeFullBytes(b,0,b.length); 49 | } 50 | 51 | public function hash(key:String):String { 52 | var magic:String = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 53 | var concated:Bytes = Bytes.ofString(key+magic); 54 | return Base64.encode(Sha1.make(concated)); 55 | } 56 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/input/ByteStringer.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.input; 2 | import haxe.io.Bytes; 3 | class ByteStringer { 4 | 5 | public function new():Void {} 6 | 7 | public function makeInput(data:String):Bytes { 8 | var raw:String = data.split(' ').join(''); 9 | var b:Bytes = Bytes.alloc(Math.floor(raw.length/8)); 10 | var current:Int = 0; 11 | var pos:Int = 0; 12 | for (i in 0...raw.length) { 13 | var r:Int = i % 8; 14 | if(Std.parseInt(raw.charAt(i)) == 1) 15 | current |= (1 << (7-r)); 16 | if(r == 7) { 17 | b.set(pos, current); 18 | current = 0; 19 | pos++; 20 | } 21 | } 22 | return b; 23 | } 24 | 25 | public function printInput(bytes:Bytes):String { 26 | var bits:Array = []; 27 | for (i in 0...bytes.length) { 28 | for (j in 0...8) { 29 | var byte:Int = bytes.get(i); 30 | byte = byte << j; 31 | byte = byte & 0xFF; 32 | byte = byte >> 7; 33 | if(byte == 1) 34 | bits.push(1); 35 | else 36 | bits.push(0); 37 | } 38 | } 39 | return bits.join(' '); 40 | } 41 | 42 | public function assertEquals(actual:Bytes, expected:String):String { 43 | var out:String = printInput(actual); 44 | var s:String = assertLinedUp(out, expected); 45 | return s; 46 | } 47 | 48 | public function assertLinedUp(actual:String, expected:String):String { 49 | var s:String = ''; 50 | if(actual != expected) { 51 | s+='\nExpected: ' + expected + '\n'; 52 | s+=' But was: ' + actual + '\n'; 53 | } 54 | return s; 55 | } 56 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/input/BytesInputMock.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.input; 2 | import haxe.io.Bytes; 3 | import haxe.io.BytesInput; 4 | class BytesInputMock implements BytesInputProxy { 5 | 6 | public var bytes:BytesInput; 7 | 8 | public function new():Void {} 9 | 10 | public function readLine():String { 11 | return bytes.readLine(); 12 | } 13 | 14 | public function readFullBytes(buf:Bytes, pos:Int, len:Int):Void { 15 | return bytes.readFullBytes(buf, pos, len); 16 | } 17 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/input/BytesInputProxy.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.input; 2 | import haxe.io.Bytes; 3 | interface BytesInputProxy { 4 | function readLine() : String; 5 | function readFullBytes( buf : Bytes, pos:Int, len:Int ) : Void; 6 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/input/BytesInputReal.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.input; 2 | import haxe.io.Input; 3 | import haxe.io.Bytes; 4 | class BytesInputReal implements BytesInputProxy { 5 | 6 | public var input:Input; 7 | 8 | public function new():Void {} 9 | 10 | public function readLine() : String { 11 | return input.readLine(); 12 | } 13 | 14 | public function readFullBytes(buf:Bytes, pos:Int, len:Int):Void { 15 | input.readBytes(buf,pos,len); 16 | } 17 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/output/BytesOutputMock.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.output; 2 | import haxe.io.BytesBuffer; 3 | import haxe.io.Bytes; 4 | class BytesOutputMock implements BytesOutputProxy { 5 | 6 | public var buffer:BytesBuffer; 7 | 8 | public function new():Void { 9 | buffer = new BytesBuffer(); 10 | } 11 | 12 | public function writeFullBytes(s:Bytes, pos:Int, len:Int):Void { 13 | buffer.addBytes(s,pos,len); 14 | } 15 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/output/BytesOutputProxy.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.output; 2 | import haxe.io.Bytes; 3 | interface BytesOutputProxy { 4 | function writeFullBytes( s : Bytes, pos : Int, len : Int ) : Void; 5 | } -------------------------------------------------------------------------------- /src/com/thomasuster/ws/output/BytesOutputReal.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws.output; 2 | import haxe.io.Bytes; 3 | import haxe.io.Output; 4 | class BytesOutputReal implements BytesOutputProxy { 5 | 6 | public var output:Output; 7 | 8 | public function new():Void {} 9 | 10 | public function writeFullBytes(s:Bytes, pos:Int, len:Int):Void { 11 | output.writeFullBytes(s, pos, len); 12 | } 13 | } -------------------------------------------------------------------------------- /test.hxml: -------------------------------------------------------------------------------- 1 | ## Neko 2 | -main TestMain 3 | -lib munit 4 | -lib hamcrest 5 | -cp src 6 | 7 | -cp test 8 | -cp ../shared/src 9 | -neko build/neko_test.n 10 | 11 | -resource test/resources/chrome.txt@chrome 12 | -resource test/resources/safari.txt@safari -------------------------------------------------------------------------------- /test/TestMain.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import massive.munit.client.PrintClient; 3 | import massive.munit.client.RichPrintClient; 4 | import massive.munit.client.HTTPClient; 5 | import massive.munit.client.JUnitReportClient; 6 | import massive.munit.client.SummaryReportClient; 7 | import massive.munit.TestRunner; 8 | 9 | #if js 10 | import js.Lib; 11 | #end 12 | 13 | /** 14 | * Auto generated Test Application. 15 | * Refer to munit command line tool for more information (haxelib run munit) 16 | */ 17 | class TestMain 18 | { 19 | static function main(){ new TestMain(); } 20 | 21 | public function new() 22 | { 23 | var suites = new Array>(); 24 | suites.push(TestSuite); 25 | 26 | #if MCOVER 27 | var client = new mcover.coverage.munit.client.MCoverPrintClient(); 28 | var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient()); 29 | #else 30 | var client = new RichPrintClient(); 31 | var httpClient = new HTTPClient(new SummaryReportClient()); 32 | #end 33 | 34 | var runner:TestRunner = new TestRunner(client); 35 | runner.addResultClient(httpClient); 36 | //runner.addResultClient(new HTTPClient(new JUnitReportClient())); 37 | 38 | runner.completionHandler = completionHandler; 39 | 40 | #if js 41 | var seconds = 0; // edit here to add some startup delay 42 | function delayStartup() 43 | { 44 | if (seconds > 0) { 45 | seconds--; 46 | js.Browser.document.getElementById("munit").innerHTML = 47 | "Tests will start in " + seconds + "s..."; 48 | haxe.Timer.delay(delayStartup, 1000); 49 | } 50 | else { 51 | js.Browser.document.getElementById("munit").innerHTML = ""; 52 | runner.run(suites); 53 | } 54 | } 55 | delayStartup(); 56 | #else 57 | runner.run(suites); 58 | #end 59 | } 60 | 61 | /* 62 | updates the background color and closes the current browser 63 | for flash and html targets (useful for continous integration servers) 64 | */ 65 | function completionHandler(successful:Bool):Void 66 | { 67 | try 68 | { 69 | #if flash 70 | flash.external.ExternalInterface.call("testResult", successful); 71 | #elseif js 72 | js.Lib.eval("testResult(" + successful + ");"); 73 | #elseif sys 74 | Sys.exit(0); 75 | #end 76 | } 77 | // if run from outside browser can get error which we can ignore 78 | catch (e:Dynamic) 79 | { 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/TestSuite.hx: -------------------------------------------------------------------------------- 1 | import massive.munit.TestSuite; 2 | 3 | import com.thomasuster.ws.ByteStringerTest; 4 | import com.thomasuster.ws.FrameReaderTest; 5 | import com.thomasuster.ws.FrameWriterTest; 6 | import com.thomasuster.ws.HandShakerTest; 7 | 8 | /** 9 | * Auto generated Test Suite for MassiveUnit. 10 | * Refer to munit command line tool for more information (haxelib run munit) 11 | */ 12 | 13 | class TestSuite extends massive.munit.TestSuite 14 | { 15 | 16 | public function new() 17 | { 18 | super(); 19 | 20 | add(com.thomasuster.ws.ByteStringerTest); 21 | add(com.thomasuster.ws.FrameReaderTest); 22 | add(com.thomasuster.ws.FrameWriterTest); 23 | add(com.thomasuster.ws.HandShakerTest); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/com/thomasuster/ws/ByteStringerTest.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import com.thomasuster.ws.input.ByteStringer; 3 | import haxe.io.BytesBuffer; 4 | import haxe.io.BytesBuffer; 5 | import haxe.io.Int32Array; 6 | import haxe.io.BytesData; 7 | import massive.munit.Assert; 8 | import haxe.io.Bytes; 9 | import org.hamcrest.core.IsEqual; 10 | import org.hamcrest.MatcherAssert; 11 | class ByteStringerTest { 12 | 13 | var factory:ByteStringer; 14 | 15 | public function new() { 16 | } 17 | 18 | @Before 19 | public function before():Void { 20 | factory = new ByteStringer(); 21 | } 22 | 23 | @Test 24 | public function test1Byte():Void { 25 | var data:String = '1 0 0 0 0 0 0 0'; 26 | /* 0 1 2 3 4 5 6 7 */ 27 | var bytes:Bytes = factory.makeInput(data); 28 | MatcherAssert.assertThat(bytes.length, IsEqual.equalTo(1)); 29 | MatcherAssert.assertThat(bytes.get(0), IsEqual.equalTo(128)); 30 | } 31 | 32 | @Test 33 | public function test1ByteInAndOut():Void { 34 | var data:String = '1 0 0 1 0 0 0 0'; 35 | /* 0 1 2 3 4 5 6 7 */ 36 | var actual:Bytes = factory.makeInput(data); 37 | var t:String = factory.assertEquals(actual, data); 38 | if(t != '') 39 | Assert.fail(t); 40 | } 41 | } -------------------------------------------------------------------------------- /test/com/thomasuster/ws/FrameReaderTest.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import haxe.io.BytesInput; 3 | import com.thomasuster.ws.input.BytesInputMock; 4 | import com.thomasuster.ws.input.ByteStringer; 5 | import haxe.io.BytesData; 6 | import haxe.io.BytesBuffer; 7 | import massive.munit.Assert; 8 | import haxe.io.Bytes; 9 | import com.thomasuster.ws.FrameReader; 10 | import org.hamcrest.core.IsEqual; 11 | import org.hamcrest.MatcherAssert; 12 | class FrameReaderTest { 13 | 14 | var frameInput:ByteStringer; 15 | var frame:FrameReader; 16 | var mock:BytesInputMock; 17 | 18 | public function new() { 19 | } 20 | 21 | @Before 22 | public function before():Void { 23 | frame = new FrameReader(); 24 | mock = new BytesInputMock(); 25 | frame.input = mock; 26 | 27 | frameInput = new ByteStringer(); 28 | } 29 | 30 | @Test 31 | public function test1ByteInput():Void { 32 | var data:String = '1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 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 1'; 33 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 34 | mock.bytes = new BytesInput(frameInput.makeInput(data)); 35 | var out:Bytes = frame.read(); 36 | MatcherAssert.assertThat(out.length, IsEqual.equalTo(1)); 37 | MatcherAssert.assertThat(out.get(0), IsEqual.equalTo(1)); 38 | } 39 | 40 | @Test 41 | public function test1ByteMasked():Void { 42 | var data:String = '1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 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'; 43 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 44 | mock.bytes = new BytesInput(frameInput.makeInput(data)); 45 | var out:Bytes = frame.read(); 46 | MatcherAssert.assertThat(out.get(0), IsEqual.equalTo(1)); 47 | } 48 | 49 | @Test 50 | public function test2ByteInput():Void { 51 | var data:String = '1 0 0 0 0 0 1 0 1 0 0 0 0 0 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 1 0 0 0 0 0 0 0 1'; 52 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 53 | mock.bytes = new BytesInput(frameInput.makeInput(data)); 54 | var out:Bytes = frame.read(); 55 | MatcherAssert.assertThat(out.length, IsEqual.equalTo(2)); 56 | var t:String = frameInput.assertEquals(out, '0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1'); 57 | if(t != '') 58 | Assert.fail(t); 59 | } 60 | 61 | @Test 62 | public function test126Payload():Void { 63 | var data:String = '1 0 0 0 0 0 1 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 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'; 64 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 65 | var intro:Bytes = frameInput.makeInput(data); 66 | var bytes:BytesBuffer = new BytesBuffer(); 67 | bytes.addBytes(intro,0,8); 68 | bytes.addBytes(Bytes.alloc(125),0,125); 69 | bytes.addByte(0x01); 70 | mock.bytes = new BytesInput(bytes.getBytes()); 71 | var out:Bytes = frame.read(); 72 | MatcherAssert.assertThat(out.length, IsEqual.equalTo(126)); 73 | MatcherAssert.assertThat(out.get(125), IsEqual.equalTo(1)); 74 | } 75 | /* 76 | 0 1 2 3 //dec 77 | 0 1 2 3 //bytes 78 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 79 | +-+-+-+-+-------+-+-------------+-------------------------------+ 80 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 81 | |I|S|S|S| (4) |A| (7) | (16/64) | 82 | |N|V|V|V| |S| | (if payload len==126/127) | 83 | | |1|2|3| |K| | | 84 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 85 | | Extended payload length continued, if payload len == 127 | 86 | + - - - - - - - - - - - - - - - +-------------------------------+ 87 | | |Masking-key, if MASK set to 1 | 88 | +-------------------------------+-------------------------------+ 89 | | Masking-key (continued) | Payload Data | 90 | +-------------------------------- - - - - - - - - - - - - - - - + 91 | : Payload Data continued ... : 92 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 93 | | Payload Data continued ... | 94 | +---------------------------------------------------------------+ 95 | 96 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 97 | */ 98 | } -------------------------------------------------------------------------------- /test/com/thomasuster/ws/FrameWriterTest.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import org.hamcrest.MatcherAssert; 3 | import org.hamcrest.core.IsEqual; 4 | import massive.munit.Assert; 5 | import String; 6 | import haxe.io.Bytes; 7 | import com.thomasuster.ws.input.ByteStringer; 8 | import com.thomasuster.ws.output.BytesOutputMock; 9 | import com.thomasuster.ws.FrameWriter; 10 | class FrameWriterTest { 11 | 12 | var writer:FrameWriter; 13 | var mock:BytesOutputMock; 14 | var byteStringer:ByteStringer; 15 | 16 | public function new() { 17 | } 18 | 19 | @Before 20 | public function before():Void { 21 | writer = new FrameWriter(); 22 | mock = new BytesOutputMock(); 23 | writer.output = mock; 24 | byteStringer = new ByteStringer(); 25 | } 26 | 27 | @Test 28 | public function testOneByte():Void { 29 | writer.payload = Bytes.alloc(1); 30 | writer.payload.set(0,1); 31 | writer.write(); 32 | var expected:String = '1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1'; 33 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 34 | var t:String = byteStringer.assertEquals(mock.buffer.getBytes(), expected); 35 | if(t != '') 36 | Assert.fail(t); 37 | } 38 | 39 | @Test 40 | public function test126Length():Void { 41 | writer.payload = Bytes.alloc(126); 42 | writer.payload.set(125,1); 43 | writer.write(); 44 | var expected:String = '1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0'; 45 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 46 | var datum:Array = [expected]; 47 | for (i in 0...126) { 48 | if(i == 125) 49 | datum.push('0 0 0 0 0 0 0 1'); 50 | else 51 | datum.push('0 0 0 0 0 0 0 0'); 52 | } 53 | expected = datum.join(' '); 54 | var t:String = byteStringer.assertEquals(mock.buffer.getBytes(), expected); 55 | if(t != '') 56 | Assert.fail(t); 57 | } 58 | 59 | @Test 60 | public function test127Length():Void { 61 | writer.payload = Bytes.alloc(65536); 62 | writer.write(); 63 | var first32:String = '1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0'; 64 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 65 | var second32:String ='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 1'; 66 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 67 | var third16:String = '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0'; 68 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 */ 69 | var out:String = byteStringer.printInput(mock.buffer.getBytes()); 70 | var t:String = byteStringer.assertLinedUp(out.substr(0, 159), first32+' '+second32+' '+third16); 71 | if(t != '') 72 | Assert.fail(t); 73 | } 74 | 75 | @Test 76 | public function tryMultiByteSpawnForLargerPayload():Void { 77 | writer.payload = Bytes.alloc(66052); 78 | writer.write(); 79 | var first32:String = '1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0'; 80 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 81 | var second32:String ='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 1'; 82 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 83 | var third16:String = '0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0'; 84 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 */ 85 | var out:String = byteStringer.printInput(mock.buffer.getBytes()); 86 | var t:String = byteStringer.assertLinedUp(out.substr(0, 159), first32+' '+second32+' '+third16); 87 | if(t != '') 88 | Assert.fail(t); 89 | } 90 | 91 | @Test 92 | public function test266UseCase():Void { 93 | writer.payload = Bytes.alloc(266); 94 | writer.payload.set(265,1); 95 | writer.write(); 96 | var first32:String = '1 0 0 0 0 0 1 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0'; 97 | /* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 */ 98 | var out:String = byteStringer.printInput(mock.buffer.getBytes()); 99 | var t:String = byteStringer.assertLinedUp(out.substr(0, 63), first32); 100 | if(t != '') 101 | Assert.fail(t); 102 | } 103 | /* 104 | 0 1 2 3 //dec 105 | 0 1 2 3 //bytes 106 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 107 | +-+-+-+-+-------+-+-------------+-------------------------------+ 108 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 109 | |I|S|S|S| (4) |A| (7) | (16/64) | 110 | |N|V|V|V| |S| | (if payload len==126/127) | 111 | | |1|2|3| |K| | | 112 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 113 | | Extended payload length continued, if payload len == 127 | 114 | + - - - - - - - - - - - - - - - +-------------------------------+ 115 | | |Masking-key, if MASK set to 1 | 116 | +-------------------------------+-------------------------------+ 117 | | Masking-key (continued) | Payload Data | 118 | +-------------------------------- - - - - - - - - - - - - - - - + 119 | : Payload Data continued ... : 120 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 121 | | Payload Data continued ... | 122 | +---------------------------------------------------------------+ 123 | 124 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 125 | */ 126 | } -------------------------------------------------------------------------------- /test/com/thomasuster/ws/HandShakerTest.hx: -------------------------------------------------------------------------------- 1 | package com.thomasuster.ws; 2 | import haxe.io.BytesInput; 3 | import haxe.io.Bytes; 4 | import com.thomasuster.ws.output.BytesOutputMock; 5 | import com.thomasuster.ws.input.BytesInputMock; 6 | import com.thomasuster.ws.input.ByteStringer; 7 | import com.thomasuster.ws.HandShaker; 8 | import org.hamcrest.core.IsEqual; 9 | import org.hamcrest.MatcherAssert; 10 | import haxe.io.BytesBuffer; 11 | class HandShakerTest { 12 | 13 | var frameInput:ByteStringer; 14 | var shaker:HandShaker; 15 | var input:BytesInputMock; 16 | var output:BytesOutputMock; 17 | 18 | public function new() { 19 | } 20 | 21 | @Before 22 | public function before():Void { 23 | shaker = new HandShaker(); 24 | input = new BytesInputMock(); 25 | output = new BytesOutputMock(); 26 | shaker.input = input; 27 | shaker.output = output; 28 | } 29 | 30 | @Test 31 | public function testExample():Void { 32 | var shaker:HandShaker = new HandShaker(); 33 | MatcherAssert.assertThat(shaker.hash('dGhlIHNhbXBsZSBub25jZQ=='), IsEqual.equalTo('s3pPLMBiTxaQ9kYGzzhZRbK+xOo=')); 34 | } 35 | 36 | @Test 37 | public function testChromeStyleHeader():Void { 38 | var bb:BytesBuffer = new BytesBuffer(); 39 | bb.add(Bytes.ofString(haxe.Resource.getString('chrome'))); 40 | input.bytes = new BytesInput(bb.getBytes()); 41 | 42 | shaker.shake(); 43 | 44 | var output:String = output.buffer.getBytes().toString(); 45 | MatcherAssert.assertThat(output.split('\r\n')[3], IsEqual.equalTo('Sec-WebSocket-Accept: 4fsrxpYQajC65Rb0+L7yR+IWNxA=')); 46 | } 47 | 48 | @Test 49 | public function testSafariStyleHeader():Void { 50 | var bb:BytesBuffer = new BytesBuffer(); 51 | bb.add(Bytes.ofString(haxe.Resource.getString('safari'))); 52 | input.bytes = new BytesInput(bb.getBytes()); 53 | 54 | shaker.shake(); 55 | 56 | var output:String = output.buffer.getBytes().toString(); 57 | MatcherAssert.assertThat(output, IsEqual.equalTo('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: bP7qsdqdjmOA1Stow2peTeRiYOA=\r\n\r\n')); 58 | } 59 | } -------------------------------------------------------------------------------- /test/resources/chrome.txt: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host: localhost:4000 3 | Connection: Upgrade 4 | Pragma: no-cache 5 | Cache-Control: no-cache 6 | Upgrade: websocket 7 | Origin: file:// 8 | Sec-WebSocket-Version: 13 9 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 10 | Accept-Encoding: gzip, deflate, sdch, br 11 | Accept-Language: en-US,en;q=0.8 12 | Sec-WebSocket-Key: btVPuCtfAJ5uTRH2uvvHdg== 13 | Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 14 | -------------------------------------------------------------------------------- /test/resources/safari.txt: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Upgrade: websocket 3 | Connection: Upgrade 4 | Host: localhost:4000 5 | Origin: file:// 6 | Pragma: no-cache 7 | Cache-Control: no-cache 8 | Sec-WebSocket-Key: 0q8SdRexpp8eEOdNRs8yfw== 9 | Sec-WebSocket-Version: 13 10 | Sec-WebSocket-Extensions: x-webkit-deflate-frame 11 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7 12 | 13 | --------------------------------------------------------------------------------