webscoket implemented

This commit is contained in:
2014-08-13 11:44:29 +04:00
parent a7e8d5c2d9
commit 0e8ddca38c
17 changed files with 615 additions and 23 deletions

View File

@@ -0,0 +1,52 @@
package ru.m.armageddon.core;
#if haxe3
import haxe.crypto.BaseCode;
#else
import haxe.BaseCode;
#end
class Base64 {
private inline static var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static var codec:BaseCode;
public function new() {
}
static function getCodec():BaseCode {
if (codec == null) {
var bytes = haxe.io.Bytes.ofString(BASE64);
codec = new BaseCode(bytes);
}
return codec;
}
public static function encodeBase64(content:haxe.io.Bytes):String {
var suffix = switch (content.length % 3){
case 2: "=";
case 1: "==";
default: "";
};
var bytes = getCodec().encodeBytes(content);
return bytes.toString() + suffix;
}
private static function removeNullbits(s:String):String {
var len = s.length;
while (len > 0 && s.charAt(len - 1) == "=") {
len--;
if (len <= 0) {
return "";
}
}
return s.substr(0, len);
}
public static function decodeBase64(content:String):haxe.io.Bytes {
var bytes:haxe.io.Bytes = haxe.io.Bytes.ofString(removeNullbits(content));
return getCodec().decodeBytes(bytes);
}
}

View File

@@ -10,8 +10,7 @@ class BaseConnection implements IConnection {
public var packetHandler(default,default):IPacketHandler;
public var connected(default, null):Bool;
public var queue(default, null):PacketQueue;
private var builder:IPacketBuilder;
public var builder(default, null):IPacketBuilder;
public function new(?handler:IConnectionHandler = null, ?packetHandler:IPacketHandler) {
this.builder = new PacketBuilder();
@@ -44,7 +43,7 @@ class BaseConnection implements IConnection {
L.d("Send", Type.getClassName(Type.getClass(packet)).split(".").pop());
}
private function receive(packet:Message):Void {
public function receive(packet:Message):Void {
L.d("Receive", Type.getClassName(Type.getClass(packet)).split(".").pop());
if (packetHandler == null) return;
var name = "on" + Type.getClassName(Type.getClass(packet)).split(".").pop();

View File

@@ -9,14 +9,14 @@ interface IConnection {
public var handler(default,default):IConnectionHandler;
public var packetHandler(default,default):IPacketHandler;
private var builder:IPacketBuilder;
public var builder(default,null):IPacketBuilder;
public function connect():ICallback<Dynamic>;
public function disconnect():Void;
public function send(packet:Message):Void;
public function pushData(bytes:Bytes):Void;
private function receive(packet:Message):Void;
public function receive(packet:Message):Void;
}
interface IConnectionHandler {

View File

@@ -0,0 +1,31 @@
package ru.m.armageddon.core.connect;
import haxe.io.Bytes;
import haxe.io.BytesOutput;
import ru.m.armageddon.core.connect.IConnection;
import protohx.Message;
class WebSocketTools {
public static function packet2string(packet:Message, builder:IPacketBuilder):String {
var meta = builder.packetMeta(packet);
var b = new BytesOutput();
packet.writeTo(b);
var data = b.getBytes();
var res = new BytesOutput();
res.writeByte(meta.family);
res.writeByte(meta.id);
//res.writeUInt16(data.length);
res.write(data);
return Base64.encodeBase64(res.getBytes());
}
public static function string2packet(data:String, builder:IPacketBuilder):Message {
var bytes = Base64.decodeBase64(data);
var family = bytes.get(0);
var id = bytes.get(1);
var packet = builder.buildPacket({family:family, id:id});
packet.mergeFrom(bytes.sub(2, bytes.length - 2));
return packet;
}
}

View File

@@ -0,0 +1,98 @@
package ru.m.armageddon.core.connect.js;
import ru.m.armageddon.core.Base64;
import haxework.net.callback.Callback;
import haxework.net.callback.ICallback;
import ru.m.armageddon.core.connect.IConnection.IConnectionHandler;
import protohx.Message;
import haxe.io.Bytes;
typedef WebSocket = {
var onopen:Dynamic->Void;
var onclose:Dynamic->Void;
var onmessage:Dynamic->Void;
var onerror:Dynamic->Void;
function send(message:String):Void;
function emit(a:String, b:String):Void;
function close():Void;
}
class JsConnection extends BaseConnection {
private var host:String;
private var port:Int;
private var socket:WebSocket;
private var callback:ICallback<Dynamic>;
public function new(host:String, port:Int, ?handler:IConnectionHandler = null) {
super(handler);
this.host = host;
this.port = port;
connected = false;
}
private function buildSocket(host:String, port:Int):WebSocket {
return untyped __js__("self.socket = new WebSocket('ws://'+host+':'+port); ");
}
override public function connect():ICallback<Dynamic> {
callback = Callback.build();
var self = this;
var decodeBytes = Base64.decodeBase64;
socket = buildSocket(host, port);
socket.onopen = this.onConnect;
socket.onclose = this.onClose;
socket.onerror = this.onError;
socket.onmessage = this.onSocketData;
return callback;
}
override public function disconnect():Void {
socket.close();
connected = false;
if (handler != null) handler.onDisconnected();
}
private function onError(event:Dynamic):Void {
socket.close();
connected = false;
if (handler != null) handler.onError(event);
if (callback != null) {
var c = callback;
callback = null;
c.callFail(event);
}
}
private function onConnect(_):Void {
connected = true;
if (handler != null) handler.onConnected();
if (callback != null) {
var c = callback;
callback = null;
c.callSuccess(null);
}
}
private function onClose(_):Void {
socket.close();
connected = false;
if (handler != null) handler.onDisconnected();
}
private function onSocketData(event:Dynamic):Void {
try {
var data:String = event.data;
var packet = WebSocketTools.string2packet(data, builder);
receive(packet);
} catch (error:Dynamic) {
handler.onError(error);
}
}
override public function send(packet:Message):Void {
super.send(packet);
socket.send(WebSocketTools.packet2string(packet, builder));
}
}

View File

@@ -8,7 +8,7 @@ import ru.m.armageddon.core.connect.IConnection;
class NekoConnection extends BaseConnection {
private var socket:Socket;
public var socket(default, null):Socket;
public function new(socket:Socket, ?handler:IConnectionHandler = null, ?packetHandler:IPacketHandler = null) {
super(handler, packetHandler);

View File

@@ -0,0 +1,198 @@
package ru.m.armageddon.core.connect.neko;
import haxe.crypto.BaseCode;
import haxe.crypto.Sha1;
import protohx.Message;
import haxe.io.Bytes;
import sys.net.Socket;
import ru.m.armageddon.core.connect.IConnection;
class NekoWebConnection extends NekoConnection {
private var opened:Bool;
public function new(socket:Socket, ?handler:IConnectionHandler = null, ?packetHandler:IPacketHandler = null) {
super(socket, handler, packetHandler);
opened = false;
}
override public function send(packet:Message):Void {
L.d("Send", Type.getClassName(Type.getClass(packet)).split(".").pop());
try {
var data = WebSocketTools.packet2string(packet, builder);
writeData(data, socket);
} catch (e:Dynamic) {
trace(e);
}
}
override public function pushData(bytes:Bytes):Void {
if (!opened) {
var str:String = bytes.getString(0, bytes.length);
if (StringTools.startsWith(str, "GET")) {
var r = ~/Sec-WebSocket-Key:\s*([A-z0-9=+\/]+)/;
r.match(str);
opened = true;
sendServerHandShake(socket, r.matched(1));
}
} else {
var data = parseData(bytes);
var packet = WebSocketTools.string2packet(data, builder);
receive(packet);
}
}
private function encodeBase64(content:String):String {
var suffix = switch (content.length % 3)
{
case 2: "=";
case 1: "==";
default: "";
};
return BaseCode.encode(content, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + suffix;
}
private function hex2data(hex:String):String {
var data = "";
for (i in 0...Std.int(hex.length / 2)) {
data += String.fromCharCode(Std.parseInt("0x" + hex.substr(i * 2, 2)));
}
return data;
}
private function sendServerHandShake(socket:sys.net.Socket, inpKey:String) {
var outKey = encodeBase64(hex2data(Sha1.encode(StringTools.trim(inpKey) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")));
var s = "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: " + outKey + "\r\n"
+ "\r\n";
socket.output.writeString(s);
}
private function writeData(data:String, socket:sys.net.Socket, isServer = true):Void {
socket.output.writeByte(0x81);
var len = 0;
if (data.length < 126) len = data.length;
else if (data.length < 65536) len = 126;
else len = 127;
socket.output.writeByte(len | (!isServer ? 0x80 : 0x00));
if (data.length >= 126)
{
if (data.length < 65536)
{
socket.output.writeByte((data.length >> 8) & 0xFF);
socket.output.writeByte(data.length & 0xFF);
}
else
{
socket.output.writeByte((data.length >> 24) & 0xFF);
socket.output.writeByte((data.length >> 16) & 0xFF);
socket.output.writeByte((data.length >> 8) & 0xFF);
socket.output.writeByte(data.length & 0xFF);
}
}
if (isServer)
{
socket.output.writeString(data);
}
else
{
var mask = [ Std.random(256), Std.random(256), Std.random(256), Std.random(256) ];
socket.output.writeByte(mask[0]);
socket.output.writeByte(mask[1]);
socket.output.writeByte(mask[2]);
socket.output.writeByte(mask[3]);
var maskedData = new StringBuf();
for (i in 0...data.length)
{
maskedData.addChar(data.charCodeAt(i) ^ mask[i % 4]);
}
socket.output.writeString(maskedData.toString());
}
}
private function parseData(bytes:Bytes):String {
var p = 0;
var opcode = bytes.get(p++);
if (opcode == 0x00)
{
var s = "";
var b : Int;
while ((b = bytes.get(p++)) != 0xFF)
{
s += String.fromCharCode(b);
}
return s;
}
if (opcode == 0x81) // 0x81 = fin & text
{
var len = bytes.get(p++);
if (len & 0x80 != 0) // mask
{
len &= 0x7F;
if (len == 126)
{
var b2 = bytes.get(p++);
var b3 = bytes.get(p++);
len = (b2 << 8) + b3;
}
else
if (len == 127)
{
var b2 = bytes.get(p++);
var b3 = bytes.get(p++);
var b4 = bytes.get(p++);
var b5 = bytes.get(p++);
len = (b2 << 24) + (b3 << 16) + (b4 << 8) + b5;
}
//Lib.println("len = " + len);
// direct array init not work corectly!
var mask = [];
mask.push(bytes.get(p++));
mask.push(bytes.get(p++));
mask.push(bytes.get(p++));
mask.push(bytes.get(p++));
//Lib.println("mask = " + mask);
var data = new StringBuf();
for (i in 0...len)
{
data.addChar(bytes.get(p++) ^ mask[i % 4]);
}
//Lib.println("readed = " + data.toString());
return data.toString();
}
else
{
throw "Expected masked data.";
}
}
if (opcode == 136) {
//socket.close();
return null;
}
else
{
throw "Unsupported websocket opcode: " + opcode;
}
return null;
}
}