diff --git a/gen.sh b/gen.sh index 8b4551e..ad60a80 100755 --- a/gen.sh +++ b/gen.sh @@ -1,2 +1,2 @@ -#!/bin/sh +#!/bin/bash haxelib run protohx generate protohx.json \ No newline at end of file diff --git a/project.xml b/project.xml index 3d6853b..7e02c8a 100755 --- a/project.xml +++ b/project.xml @@ -6,7 +6,9 @@ + + diff --git a/protohx.json b/protohx.json index 208f678..a9c1128 100755 --- a/protohx.json +++ b/protohx.json @@ -1,9 +1,9 @@ -{ - "protoPath": ".", - "protoFiles": [ - "proto/base.proto" - ], - "cleanOut": true, - "haxeOut": "src-gen/haxe", - "javaOut": null +{ + "protoPath": ".", + "protoFiles": [ + "proto/base.proto" + ], + "cleanOut": true, + "haxeOut": "src-gen/haxe", + "javaOut": null } \ No newline at end of file diff --git a/server.sh b/server.sh index 4430202..54b7688 100755 --- a/server.sh +++ b/server.sh @@ -1,2 +1,2 @@ -#!/bin/sh +#!/bin/bash haxe server.hxml && neko target/server.n diff --git a/src/client/haxe/ru/m/armageddon/client/Client.hx b/src/client/haxe/ru/m/armageddon/client/Client.hx index 1930999..1c22251 100755 --- a/src/client/haxe/ru/m/armageddon/client/Client.hx +++ b/src/client/haxe/ru/m/armageddon/client/Client.hx @@ -1,5 +1,9 @@ package ru.m.armageddon.client; +import flash.text.TextFieldType; +import flash.Lib; +import flash.text.TextField; +import haxework.log.JSLogger; import ru.m.armageddon.client.data.GameData; import haxework.frame.IFrameSwitcher; import haxework.provider.Provider; @@ -10,6 +14,7 @@ import haxework.gui.GuiBuilder; import haxe.Json; import openfl.Assets; import ru.m.armageddon.core.connect.flash.FlashConnection; +import ru.m.armageddon.core.connect.js.JsConnection; import ru.m.armageddon.core.connect.IConnection; import haxework.log.TraceLogger; @@ -19,6 +24,7 @@ class Client implements IConnectionHandler { public static function main() { L.push(new TraceLogger()); + L.push(new JSLogger()); L.d(TAG, Meta.getVersion()); new Client(); } @@ -34,9 +40,20 @@ class Client implements IConnectionHandler { Provider.setFactory(GameData, GameData); Provider.set(IFrameSwitcher, switcher); - Provider.set(IConnection, new FlashConnection("localhost", 5000, this)); + //#if flash + //Provider.set(IConnection, new FlashConnection("localhost", 5001, this)); + //#elseif html5 + Provider.set(IConnection, new JsConnection("localhost", 5001, this)); + //#end switcher.change(AuthFrame.ID); + + /*var tf = new TextField(); + tf.type = TextFieldType.INPUT; + tf.text = "Azaza"; + tf.border = true; + tf.borderColor = 0xff0000; + Lib.current.addChild(tf);*/ } public function onConnected():Void {} diff --git a/src/client/haxe/ru/m/armageddon/client/frames/AuthFrame.hx b/src/client/haxe/ru/m/armageddon/client/frames/AuthFrame.hx index 8b9e6b4..65ef2a1 100755 --- a/src/client/haxe/ru/m/armageddon/client/frames/AuthFrame.hx +++ b/src/client/haxe/ru/m/armageddon/client/frames/AuthFrame.hx @@ -47,6 +47,9 @@ class AuthFrame extends VGroupView implements IPacketHandler { loginInput.text = so.data.login; passwordInput.text = so.data.password; onPress(null); + } else { + loginInput.text = "shmyga"; + passwordInput.text = "xkbp8jh9z2"; } } @@ -56,6 +59,7 @@ class AuthFrame extends VGroupView implements IPacketHandler { var connection:IConnection = Provider.get(IConnection); connection.connect() .success(function(_) { + L.d(TAG, "Connected"); connection.send(new LoginRequest().setLogin(login).setPassword(password)); }) .fail(function(error) { diff --git a/src/client/webapp/index.html b/src/client/webapp/index.html new file mode 100644 index 0000000..aeb3a98 --- /dev/null +++ b/src/client/webapp/index.html @@ -0,0 +1,26 @@ + + + + + + + + + +
+ + + + + + diff --git a/src/client/webapp/swf.js b/src/client/webapp/swf.js new file mode 100644 index 0000000..62c1328 --- /dev/null +++ b/src/client/webapp/swf.js @@ -0,0 +1,148 @@ +(function(window) { + + if (!window.Swf) { + + window.Swf = function(eid, src, listener, params) { + this.id = "adman" + window.Swf.i++; + this.listener = listener; + this.embed(eid, src, params); + + window.SWF_CALLBACKS = window.SWF_CALLBACKS || {}; + window.SWF_CALLBACKS[this.id] = this.onSwfEvent(); + }; + + window.Swf.i = 0; + + window.Swf.prototype = { + + dispatch: function(type) { + if (this.listener && this.listener["on_" + type]) { + var args = [].slice.call(arguments, 1); + this.listener["on_" + type].apply(this.listener, args); + } + }, + + onSwfEvent: function() { + var self = this; + return function(type, arg1, arg2) { + self.dispatch(type, arg1, arg2); + }; + }, + + insertAfter :function(elem, refElem) { + var parent = refElem.parentNode; + var next = refElem.nextSibling; + if (next) { + return parent.insertBefore(elem, next); + } else { + return parent.appendChild(elem); + } + }, + + + embed:function(target, src, params) { + params = params || {}; + params["callback"] = "SWF_CALLBACKS." + this.id; + var flashData = { + params: { + id: this.id, + quality: "high", + allowScriptAccess: "always", + allowFullScreen: true, + wMode: "opaque", + base: null, + swLiveConnect: true + }, + properties: { + name: this.id, + width: "100%", + height: "100%" + }, + vars: params + }; + this.swf = this.buildFlashElement(src, flashData); + this.swf.style["position"] = "absolute"; + this.swf.style["left"] = 0; + this.swf.style["top"] = 0; + //this.swf.style["visibility"] = "hidden"; + if (target) { + target = document.getElementById(target); + this.insertAfter(this.swf, target); + } else { + document.body.appendChild(this.swf); + } + }, + + typeOf: function (item) { + if (item == null) return "null"; + if (item.nodeName) { + if (item.nodeType == 1) return "element"; + if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? "textnode" : "whitespace"; + } else if (typeof item.length == "number") { + if (item.callee) return "arguments"; + } + return typeof item; + }, + + toFlashVars: function (object, base) { + var queryString = []; + for (var key in object) { + var value = object[key]; + if (base) key = base + ":" + key; + var result; + switch (this.typeOf(value)) { + case "object": + result = this.toFlashVars(value, key); + break; + case "array": + var qs = {}; + value.each(function (val, i) { + qs[i] = val; + }); + result = this.toFlashVars(qs, key); + break; + default: + result = key + "=" + encodeURIComponent(value); + } + if (value != null) queryString.push(result); + } + return queryString.join("&"); + }, + + buildFlashElement: function (path, options) { + var params = options.params; + var vars = options.vars; + var properties = options.properties; + + params.flashVars = this.toFlashVars(vars); + var isIE = /*@cc_on!@*/false; + if (isIE) { + properties.classid = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; + params.movie = path; + } else { + properties.type = "application/x-shockwave-flash"; + } + properties.data = path; + + var build = ""; + } + build += ""; + + var div = document.createElement("div"); + div.innerHTML = build; + return div.firstChild; + }, + + dispose: function() { + if (this.swf) { + this.swf.dispose(); + this.swf.parentNode.removeChild(this.swf); + } + } + }; + } +})(window); \ No newline at end of file diff --git a/src/common/haxe/ru/m/armageddon/core/Base64.hx b/src/common/haxe/ru/m/armageddon/core/Base64.hx new file mode 100644 index 0000000..2df7eaa --- /dev/null +++ b/src/common/haxe/ru/m/armageddon/core/Base64.hx @@ -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); + } + +} diff --git a/src/common/haxe/ru/m/armageddon/core/connect/BaseConnection.hx b/src/common/haxe/ru/m/armageddon/core/connect/BaseConnection.hx index b0a6bb1..132c35b 100755 --- a/src/common/haxe/ru/m/armageddon/core/connect/BaseConnection.hx +++ b/src/common/haxe/ru/m/armageddon/core/connect/BaseConnection.hx @@ -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(); diff --git a/src/common/haxe/ru/m/armageddon/core/connect/IConnection.hx b/src/common/haxe/ru/m/armageddon/core/connect/IConnection.hx index 84bcf44..5fe9dfc 100755 --- a/src/common/haxe/ru/m/armageddon/core/connect/IConnection.hx +++ b/src/common/haxe/ru/m/armageddon/core/connect/IConnection.hx @@ -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; 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 { diff --git a/src/common/haxe/ru/m/armageddon/core/connect/WebSocketTools.hx b/src/common/haxe/ru/m/armageddon/core/connect/WebSocketTools.hx new file mode 100644 index 0000000..b84665d --- /dev/null +++ b/src/common/haxe/ru/m/armageddon/core/connect/WebSocketTools.hx @@ -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; + } +} \ No newline at end of file diff --git a/src/common/haxe/ru/m/armageddon/core/connect/js/JsConnection.hx b/src/common/haxe/ru/m/armageddon/core/connect/js/JsConnection.hx new file mode 100644 index 0000000..3b16704 --- /dev/null +++ b/src/common/haxe/ru/m/armageddon/core/connect/js/JsConnection.hx @@ -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; + + 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 { + 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)); + } +} \ No newline at end of file diff --git a/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoConnection.hx b/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoConnection.hx index 5425130..c2c5805 100755 --- a/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoConnection.hx +++ b/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoConnection.hx @@ -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); diff --git a/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoWebConnection.hx b/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoWebConnection.hx new file mode 100644 index 0000000..ce3a472 --- /dev/null +++ b/src/common/haxe/ru/m/armageddon/core/connect/neko/NekoWebConnection.hx @@ -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; + } +} \ No newline at end of file diff --git a/src/server/haxe/ru/m/armageddon/server/Server.hx b/src/server/haxe/ru/m/armageddon/server/Server.hx index f544974..e02e948 100755 --- a/src/server/haxe/ru/m/armageddon/server/Server.hx +++ b/src/server/haxe/ru/m/armageddon/server/Server.hx @@ -1,21 +1,34 @@ package ru.m.armageddon.server; +import ru.m.armageddon.core.connect.neko.NekoWebConnection; +import ru.m.armageddon.core.connect.neko.NekoConnection; import haxework.log.TraceLogger; import ru.m.armageddon.server.session.Session; import haxe.io.Bytes; import sys.net.Socket; import neko.net.ThreadServer; +enum ServerMode { + NORMAL_SOCKET; + WEB_SOCKET; +} + class Server extends ThreadServer { private static inline var TAG = "Server"; - public function new() { + private var mode:ServerMode; + + public function new(mode:ServerMode) { super(); + this.mode = mode; } override function clientConnected(s:Socket):Session { - var session = new Session(s); + var session = new Session(s, switch(mode) { + case ServerMode.NORMAL_SOCKET: NekoConnection; + case ServerMode.WEB_SOCKET: NekoWebConnection; + }); L.d(TAG, "Client connected"); return session; } @@ -35,7 +48,9 @@ class Server extends ThreadServer { public static function main() { L.push(new TraceLogger()); L.d(TAG, "Running"); - var server = new Server(); - server.run("localhost", 5000); + //var server = new Server(ServerMode.NORMAL_SOCKET); + //server.run("localhost", 5001); + var wserver = new Server(ServerMode.WEB_SOCKET); + wserver.run("localhost", 5001); } } \ No newline at end of file diff --git a/src/server/haxe/ru/m/armageddon/server/session/Session.hx b/src/server/haxe/ru/m/armageddon/server/session/Session.hx index de0ee9f..e9d0208 100755 --- a/src/server/haxe/ru/m/armageddon/server/session/Session.hx +++ b/src/server/haxe/ru/m/armageddon/server/session/Session.hx @@ -16,9 +16,11 @@ class Session implements IConnectionHandler implements IPacketHandler { public var account(default, null):Account; public var connection(default, null):IConnection; + public var opened:Bool; - public function new(socket:Socket) { - connection = new NekoConnection(socket, this, this); + public function new(socket:Socket, connectionFactory:Class) { + opened = false; + connection = Type.createInstance(connectionFactory, [socket, this, this]); } public function onConnected():Void {