├── .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 | [](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 |
--------------------------------------------------------------------------------