0.1.1 release
This commit is contained in:
parent
65dcf4bd86
commit
57bc85c0dc
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
package-lock.json
|
||||
node_modules/
|
||||
*.ini
|
||||
|
113
README.md
113
README.md
@ -1,26 +1,99 @@
|
||||
CHUNICHUNIMatch
|
||||
===============
|
||||
ShadowTenpo
|
||||
===========
|
||||
|
||||
Remote LAN multiplayer matching by virtualizing members into the local LAN.
|
||||
Remote SEGA cab-to-cab multiplayer by virtualizing members into the local LAN.
|
||||
|
||||
*No VPN required*
|
||||
Not a VPN. No need for a VPN.*
|
||||
|
||||
Instructions
|
||||
Installation
|
||||
------------
|
||||
|
||||
***Project is currently in experimentation***
|
||||
You need a server to connect and forward traffic between clients. You can deploy
|
||||
this on any machine that has a public IP address and can accept HTTP/Websockets
|
||||
connection on a port of your choice.
|
||||
|
||||
* 配置IP别名。提权cmd运行以下指令在主网卡上新增192.168.139.11-14(如果必要,替换Ethernet为网卡名)
|
||||
```
|
||||
netsh interface ipv4 set interface interface="Ethernet" dhcpstaticipcoexistence=enabled
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.11 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.12 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.13 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address "Ethernet" 192.168.139.14 255.255.255.0 store=active
|
||||
```
|
||||
* 确保使用的segatools更新到至少forcebind
|
||||
* 修改segatools.ini添加下面的新section以使用CCM在连接时自动生成的INI
|
||||
```
|
||||
[override]
|
||||
netenv="D:\path\to\ccm\segatools-override.ini"
|
||||
```
|
||||
On the client:
|
||||
|
||||
1. Install NodeJS (and NPM) if not installed already.
|
||||
2. Open `install.bat`, or run command `npm i` to install node dependencies
|
||||
3. Edit `config.js` and change the server address and port to your server.
|
||||
4. If necessary, change other settings in `config.js`. The default values are
|
||||
for SDBT. Other SEGA games may use different ports, or may not work.
|
||||
|
||||
To deploy a server:
|
||||
|
||||
1. Clone this same repo and have a NodeJS installation ready.
|
||||
2. `npm i` for dependencies
|
||||
3. Edit `config.js` and change `server_port` to any port of your choice for
|
||||
incoming HTTP/Websocket connections.
|
||||
4. Run `node server` to start the server. Using a screen/tmux is recommended as
|
||||
the server will not fork into background by itself.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
*Note: this project may be in active development. Usage instructions may change
|
||||
frequently. If you pull from git or manually update, make sure to revisit the
|
||||
usage every time.*
|
||||
|
||||
1. Open `netif.bat` with administrative privileges to setup IP aliases.
|
||||
* IP aliases are valid until reboot by default.
|
||||
* `NETIF` in the script may be set to `loopback` if the default fails.
|
||||
* Using your physical network interface is discouraged and may cause loss of
|
||||
network. See https://superuser.com/a/1337075 if you really need to.
|
||||
* Using a virtual interface (like VPN adaptor) may cause undefined behavior
|
||||
with the game.
|
||||
2. Open `start.bat` or run `node .` to start the client. Verify that it can
|
||||
connect to the server and obtain party IP addresses.
|
||||
3. Configure `segatools.ini` of the game to use the subnet and address. The
|
||||
client will create a `segatools-override.ini` in the same directory. Use
|
||||
that as a reference.
|
||||
4. Start the game. If IP ADDRESS BAD, start the game as administrator.
|
||||
|
||||
Note that the client cannot be started if the game is running. If the client
|
||||
is closed or crashed, the game must be stopped before you can restart the
|
||||
client.
|
||||
|
||||
Game Settings
|
||||
-------------
|
||||
|
||||
- Members must be on the exact same version. This includes game, ICF, options
|
||||
and data.conf.
|
||||
- By default, standard and netdelivery ports are not forwarded. Every member
|
||||
should be their own standard and netdelivery server.
|
||||
- Members must set to the same cab-to-cab group.
|
||||
- Members may use different title servers or even no aime service at all.
|
||||
- No additional executable patching is required.
|
||||
|
||||
Security Warning
|
||||
----------------
|
||||
|
||||
This software has the ability to send arbitrary network traffic to your local
|
||||
network. Your firewall may be bypassed if an adversary took control over parts
|
||||
of the system, including but not limited to, a malicious server, a malicious
|
||||
client on the same server, a middleman attack on any connection to the server.
|
||||
By running the software you acknowledge such security risks on your system, and
|
||||
agree to hold the developers innocent against any potential loss or damages,
|
||||
even if caused by a flaw in the software.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
All rights reserved.
|
||||
|
||||
The authors informally permits acceptable modifications and redistributions
|
||||
within closed communities for the purpose of enhancement and further
|
||||
development. The authors reserves the full authority to determine if any
|
||||
particular usage is acceptable. Specifically, reproduction in any form to any
|
||||
publicly accessible location, including but not limited to GitHub.com, is
|
||||
prohibited.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
61
cccc.js
Normal file
61
cccc.js
Normal file
@ -0,0 +1,61 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
var CHUNICHUNICHUNIC = {};
|
||||
|
||||
const cccc_key = Buffer.from('CHUNICHUNICHUNIC', 'ascii');
|
||||
|
||||
CHUNICHUNICHUNIC.encrypt = function (data) {
|
||||
var length = data.length;
|
||||
if (length % 16 != 0) {
|
||||
var padlen = 16-(length%16);
|
||||
data = Buffer.concat([data, Buffer.alloc(padlen)]);
|
||||
console.warn("Trying to encrypt non-padded data!");
|
||||
length += padlen;
|
||||
}
|
||||
var cipher = crypto.createCipheriv('aes-128-ecb', cccc_key, null);
|
||||
cipher.setAutoPadding(false);
|
||||
var msg0 = Buffer.alloc(4);
|
||||
msg0.writeUInt32LE(length+4, 0);
|
||||
var msg1 = cipher.update(data);
|
||||
var msg2 = cipher.final();
|
||||
return Buffer.concat([msg0, msg1, msg2]);
|
||||
}
|
||||
|
||||
CHUNICHUNICHUNIC.decrypt = function(msg) {
|
||||
if (msg.length % 16 != 0 ) {
|
||||
console.warn("Bad decrypt length: " + msg.length);
|
||||
return;
|
||||
}
|
||||
var decipher = crypto.createDecipheriv('aes-128-ecb', cccc_key, null);
|
||||
decipher.setAutoPadding(false);
|
||||
var data1 = decipher.update(msg);
|
||||
var data2 = decipher.final();
|
||||
return Buffer.concat([data1, data2]);
|
||||
}
|
||||
|
||||
CHUNICHUNICHUNIC.deframe = function(buf, ctx) {
|
||||
var data, pending, msg;
|
||||
if (!ctx.length) {
|
||||
ctx.length = buf.readUInt32LE(0) - 4;
|
||||
ctx.current = buf.slice(4);
|
||||
if (ctx.length % 16 != 0) {
|
||||
console.warn("Received mispadded packet: ", buf.toString('hex'));
|
||||
}
|
||||
} else {
|
||||
ctx.current = Buffer.concat([ctx.current, buf]);
|
||||
}
|
||||
if (ctx.current.length > ctx.length) {
|
||||
pending = ctx.current.slice(ctx.length);
|
||||
msg = ctx.current.slice(0, ctx.length);
|
||||
} else if (ctx.current.length == ctx.length) {
|
||||
msg = ctx.current;
|
||||
}
|
||||
if (msg) {
|
||||
ctx.length = 0;
|
||||
data = CHUNICHUNICHUNIC.decrypt(msg);
|
||||
ctx.current = null;
|
||||
}
|
||||
return {data: data, pending: pending};
|
||||
}
|
||||
|
||||
module.exports = CHUNICHUNICHUNIC;
|
41
ccencap.js
41
ccencap.js
@ -1,7 +1,10 @@
|
||||
var CCEncap = {};
|
||||
|
||||
const CCM_NET_SENDTCP = 0x01;
|
||||
const CCM_NET_SENDUDP = 0x02;
|
||||
const CCM_NET_TCPSEND = 0x01;
|
||||
const CCM_NET_UDPSEND = 0x02;
|
||||
const CCM_NET_TCPFIN = 0x03;
|
||||
const CCM_NET_TCPCLOSE = 0x04;
|
||||
const CCM_SYS_MESSAGE = 0x20;
|
||||
const CCM_PARTY_CREATE = 0x30;
|
||||
const CCM_PARTY_HELLO = 0x31;
|
||||
const CCM_PARTY_UPDATE = 0x32;
|
||||
@ -26,9 +29,15 @@ CCEncap.encodePartyUpdate = function(req) {
|
||||
|
||||
CCEncap.encodePacket = function(src, dst, sport, dport, type, data) {
|
||||
if (type == 'tcp') {
|
||||
var header = CCM_NET_SENDTCP;
|
||||
var header = CCM_NET_TCPSEND;
|
||||
} else if (type == 'udp') {
|
||||
var header = CCM_NET_SENDUDP;
|
||||
var header = CCM_NET_UDPSEND;
|
||||
} else if (type == 'tcpfin') {
|
||||
var header = CCM_NET_TCPFIN;
|
||||
data = Buffer.alloc(0);
|
||||
} else if (type == 'tcpclose') {
|
||||
var header = CCM_NET_TCPCLOSE;
|
||||
data = Buffer.alloc(0);
|
||||
} else {
|
||||
throw new Error('Invalid network protocol: ' + type);
|
||||
}
|
||||
@ -39,10 +48,13 @@ CCEncap.encodePacket = function(src, dst, sport, dport, type, data) {
|
||||
msg.writeUInt16LE(dport, 4);
|
||||
msg[6] = src;
|
||||
data.copy(msg, 7);
|
||||
console.log(type + "_send "+src+':'+sport+'->'+dst+':'+dport);
|
||||
return msg;
|
||||
}
|
||||
|
||||
CCEncap.encodeTextMessage = function(text) {
|
||||
return Buffer.from(' ' + text); // space is 0x20
|
||||
}
|
||||
|
||||
CCEncap.decode = function(msg) {
|
||||
var opcode = msg[0], data = msg;
|
||||
var ret = {op: '?'};
|
||||
@ -50,8 +62,11 @@ CCEncap.decode = function(msg) {
|
||||
case CCM_PARTY_CREATE: ret.op = 'party_create'; break;
|
||||
case CCM_PARTY_HELLO: ret.op = 'party_hello'; break;
|
||||
case CCM_PARTY_UPDATE: ret.op = 'party_update'; break;
|
||||
case CCM_NET_SENDTCP: ret.op = 'net_tcp'; break;
|
||||
case CCM_NET_SENDUDP: ret.op = 'net_udp'; break;
|
||||
case CCM_NET_TCPSEND: ret.op = 'net_tcp'; break;
|
||||
case CCM_NET_UDPSEND: ret.op = 'net_udp'; break;
|
||||
case CCM_NET_TCPFIN: ret.op = 'net_tcpfin'; break;
|
||||
case CCM_NET_TCPCLOSE: ret.op = 'net_tcpclose'; break;
|
||||
case CCM_SYS_MESSAGE: ret.op = 'sys_msg'; break;
|
||||
}
|
||||
switch(opcode) {
|
||||
case CCM_PARTY_CREATE:
|
||||
@ -60,14 +75,18 @@ CCEncap.decode = function(msg) {
|
||||
msg[0] = 0x7b; // '{'
|
||||
Object.assign(ret, JSON.parse(msg));
|
||||
break;
|
||||
case CCM_NET_SENDTCP:
|
||||
case CCM_NET_SENDUDP:
|
||||
case CCM_NET_TCPSEND:
|
||||
case CCM_NET_UDPSEND:
|
||||
case CCM_NET_TCPFIN:
|
||||
case CCM_NET_TCPCLOSE:
|
||||
ret.dst = msg[1];
|
||||
ret.src = msg[6];
|
||||
ret.sport = msg.readUInt16LE(2);
|
||||
ret.dport = msg.readUInt16LE(4);
|
||||
// ret.msg = msg.slice(7);
|
||||
console.log(ret.op+" "+ret.src+':'+ret.sport+'->'+ret.dst+':'+ret.dport);
|
||||
// console.log(ret.op+' '+ret.src+':'+ret.sport+'->'+ret.dst+':'+ret.dport);
|
||||
break;
|
||||
case CCM_SYS_MESSAGE:
|
||||
ret.msg = msg.subarray(1).toString('utf-8');
|
||||
break;
|
||||
default:
|
||||
console.warn('Received unknown opcode: ' + opcode.toString(16));
|
||||
|
7
cchub.js
7
cchub.js
@ -25,10 +25,15 @@ function CCHub(url) {
|
||||
break;
|
||||
case 'net_tcp':
|
||||
case 'net_udp':
|
||||
case 'net_tcpfin':
|
||||
case 'net_tcpclose':
|
||||
var type = msg.op.slice(4);
|
||||
var payload = data.slice(7);
|
||||
var payload = data.subarray(7);
|
||||
this.onNetForward(msg.src, msg.dst, msg.sport, msg.dport, type, payload);
|
||||
break;
|
||||
case 'sys_msg':
|
||||
console.log('Hub message: ' + msg.msg);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unhandled packed: ', msg)
|
||||
}
|
||||
|
61
ccrelay.js
61
ccrelay.js
@ -4,7 +4,7 @@ const dgram = require('dgram');
|
||||
// Note: src and dst addresses in this file are full IP strings only.
|
||||
|
||||
function CCRelay(host) {
|
||||
console.log("Relay started for " + host);
|
||||
console.info("Relay started for " + host);
|
||||
this.host = host;
|
||||
this.sockets = {};
|
||||
this.servers = {};
|
||||
@ -51,18 +51,29 @@ CCRelay.prototype.sendUDP = function(dst, sport, dport, msg) {
|
||||
});
|
||||
};
|
||||
|
||||
CCRelay.prototype._setupTCPSocket = function(socket, sport, dport) {
|
||||
CCRelay.prototype._setupTCPSocket = function(socket, gameport, relayport) {
|
||||
socket.on('data', (msg) => {
|
||||
this.onReceive(sport, dport, 'tcp', msg);
|
||||
this.onReceive(gameport, relayport, 'tcp', msg);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
if (this.sockets['tcp'+gameport+'-'+relayport]) {
|
||||
console.info('tcp'+gameport+'-'+relayport + ' fin\'ed');
|
||||
this.onReceive(gameport, relayport, 'tcpfin');
|
||||
}
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
if (this.sockets['tcp'+gameport+'-'+relayport]) {
|
||||
console.info('tcp'+gameport+'-'+relayport + ' closed');
|
||||
this.sockets['tcp'+gameport+'-'+relayport] = null;
|
||||
this.onReceive(gameport, relayport, 'tcpclose');
|
||||
}
|
||||
});
|
||||
socket.on('error', e => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = null;
|
||||
if (this.sockets['tcp'+gameport+'-'+relayport]) {
|
||||
console.info('tcp'+gameport+'-'+relayport + 'error', e);
|
||||
this.sockets['tcp'+gameport+'-'+relayport] = null;
|
||||
this.onReceive(gameport, relayport, 'tcpclose');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -79,8 +90,8 @@ CCRelay.prototype.listenTCP = function(port) {
|
||||
this.listenTCP(port);
|
||||
});
|
||||
server.on('connection', (socket) => {
|
||||
console.info('tcp'+socket.remotePort+'-'+port + ' created by accept');
|
||||
this._setupTCPSocket(socket, socket.remotePort, port);
|
||||
console.log("Adding passive TCP socket ", 'tcp'+socket.remotePort+'-'+port)
|
||||
this.sockets['tcp'+socket.remotePort+'-'+port] = socket;
|
||||
});
|
||||
var listenpromise = new Promise((resolve) => {
|
||||
@ -94,18 +105,21 @@ CCRelay.prototype.listenTCP = function(port) {
|
||||
};
|
||||
|
||||
CCRelay.prototype.sendTCP = function(dst, sport, dport, msg) {
|
||||
var socket = this.sockets['tcp'+sport+'-'+dport];
|
||||
var socket = this.sockets['tcp'+dport+'-'+sport];
|
||||
if (!socket) {
|
||||
socket = new Promise((resolve) => {
|
||||
console.log("Adding active TCP socket", 'tcp'+sport+'-'+dport);
|
||||
console.info('tcp'+dport+'-'+sport + 'created by connect');
|
||||
var newsocket = new net.Socket();
|
||||
this._setupTCPSocket(newsocket, sport, dport);
|
||||
newsocket.connect(dport, dst, () => {
|
||||
this.sockets['tcp'+sport+'-'+dport] = newsocket;
|
||||
this._setupTCPSocket(newsocket, dport, sport);
|
||||
newsocket.connect({
|
||||
port: dport, host: dst,
|
||||
localPort: sport, localAddress: this.host
|
||||
}, () => {
|
||||
this.sockets['tcp'+dport+'-'+sport] = newsocket;
|
||||
resolve(newsocket);
|
||||
});
|
||||
});
|
||||
this.sockets['tcp'+sport+'-'+dport] = socket;
|
||||
this.sockets['tcp'+dport+'-'+sport] = socket;
|
||||
}
|
||||
if (socket instanceof Promise) {
|
||||
return socket.then(() => {
|
||||
@ -117,4 +131,25 @@ CCRelay.prototype.sendTCP = function(dst, sport, dport, msg) {
|
||||
});
|
||||
};
|
||||
|
||||
CCRelay.prototype.endTCP = function(sport, dport) {
|
||||
var socket = this.sockets['tcp'+dport+'-'+sport];
|
||||
if (!socket) {
|
||||
console.warn('Requested to end nonexistent socket ' + 'tcp'+dport+'-'+sport);
|
||||
return;
|
||||
}
|
||||
console.info('FIN on' + 'tcp'+dport+'-'+sport);
|
||||
socket.end();
|
||||
};
|
||||
|
||||
CCRelay.prototype.closeTCP = function(sport, dport) {
|
||||
var socket = this.sockets['tcp'+dport+'-'+sport];
|
||||
if (!socket) {
|
||||
console.warn('Requested to close nonexistent socket ' + 'tcp'+dport+'-'+sport);
|
||||
return;
|
||||
}
|
||||
this.sockets['tcp'+dport+'-'+sport] = null;
|
||||
console.info('Close on' + 'tcp'+dport+'-'+sport);
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
module.exports = CCRelay;
|
||||
|
15
config.js
15
config.js
@ -1,6 +1,13 @@
|
||||
module.exports = {
|
||||
"server": "gcp.naominet.com",
|
||||
"server_port": 61820,
|
||||
"login": {},
|
||||
"write_override_ini": "segatools-override.ini"
|
||||
"version": "0.1.1",
|
||||
"server": "example.com",
|
||||
"server_port": 37703,
|
||||
"login": {
|
||||
pref_ip: 11
|
||||
},
|
||||
"write_override_ini": "segatools-override.ini",
|
||||
"tcp_ports": [50000],
|
||||
"udp_ports": [50000],
|
||||
"verbosity": {"log": 1, "warn": 1, "error": 1, "info": 1, "debug": 0},
|
||||
"debug_tcpdump": 0,
|
||||
};
|
||||
|
85
index.js
85
index.js
@ -4,7 +4,10 @@ const fs = require('fs');
|
||||
|
||||
const CCRelay = require('./ccrelay');
|
||||
const CCHub = require('./cchub');
|
||||
const WebMgr = require('./webmgr');
|
||||
const cccc = require('./cccc');
|
||||
|
||||
console.log('ShadowTenpo ver. ' + cfg.version);
|
||||
|
||||
var hub = new CCHub('ws://' + cfg.server + ':' + cfg.server_port);
|
||||
var relays = {};
|
||||
@ -12,20 +15,20 @@ var suffix = 0;
|
||||
var subnet = "";
|
||||
|
||||
hub.ready.then(() => {
|
||||
console.log('Connecting to CCM Hub ' + cfg.server + '...');
|
||||
hub.hello(cfg.login || {});
|
||||
console.log('Connecting to ShadowTenpo Route ' + cfg.server + '...');
|
||||
var login = cfg.login || {};
|
||||
login.version = cfg.version;
|
||||
hub.hello(login);
|
||||
});
|
||||
|
||||
hub.onPartyJoin = function(mysubnet, mysuffix) {
|
||||
suffix = mysuffix;
|
||||
subnet = mysubnet;
|
||||
console.log('Join successful. Game IP should be ' + subnet+suffix);
|
||||
console.log('Please wait until all match members have joined before starting game');
|
||||
if (cfg.write_override_ini) {
|
||||
var override =
|
||||
'[netenv]\n' +
|
||||
'enable=1\n' +
|
||||
'forcebind=1\n' +
|
||||
'addrSuffix=' + suffix + '\n' +
|
||||
'[keychip]\n' +
|
||||
'subnet=' + subnet + '0\n';
|
||||
@ -35,11 +38,19 @@ hub.onPartyJoin = function(mysubnet, mysuffix) {
|
||||
|
||||
hub.onPartyUpdate = function(members) {
|
||||
members.forEach(dst => {
|
||||
console.log(subnet+dst + ' joined virtual party.');
|
||||
console.log('Virtual cab: ' + subnet+dst);
|
||||
var relay = new CCRelay(subnet+dst);
|
||||
relay.bindUDP(50200);
|
||||
relay.listenTCP(50200);
|
||||
cfg.udp_ports.forEach(port => relay.bindUDP(port));
|
||||
cfg.tcp_ports.forEach(port => relay.listenTCP(port));
|
||||
relay.txctx = {};
|
||||
relay.rxctx = {}
|
||||
relay.onReceive = function(sport, dport, type, msg) {
|
||||
var desc = type+' TX: ' + suffix+':'+sport + '->' + dst+':'+dport;
|
||||
if (cfg.debug_tcpdump) {
|
||||
decodeStream(msg, relay.rxctx, desc);
|
||||
} else {
|
||||
console.debug(desc);
|
||||
}
|
||||
hub.sendNetForward(suffix, dst, sport, dport, type, msg);
|
||||
}
|
||||
relays[dst] = relay;
|
||||
@ -53,8 +64,66 @@ hub.onNetForward = function(src, dst, sport, dport, type, msg) {
|
||||
return;
|
||||
}
|
||||
if (type == 'tcp') {
|
||||
relay.sendTCP(subnet+dst, sport, dport, msg)
|
||||
relay.sendTCP(subnet+dst, sport, dport, msg);
|
||||
var desc = 'tcp RX: ' + src+':'+sport + '->' + dst+':'+dport;
|
||||
if (cfg.debug_tcpdump) {
|
||||
decodeStream(msg, relay.txctx, desc);
|
||||
} else {
|
||||
console.debug(desc);
|
||||
}
|
||||
} else if (type == 'udp') {
|
||||
var desc = 'udp RX: ' + src+':'+sport + '->' + dst+':'+dport;
|
||||
console.debug(desc);
|
||||
relay.sendUDP(subnet+dst, sport, dport, msg);
|
||||
} else if (type == 'tcpfin') {
|
||||
relay.closeTCP(sport, dport);
|
||||
}
|
||||
};
|
||||
|
||||
function formatbinary(data) {
|
||||
var ret = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var c = data[i];
|
||||
if (c==0)
|
||||
ret += ' .';
|
||||
else if (c < 16)
|
||||
ret += '0'+c.toString(16);
|
||||
else
|
||||
ret += c.toString(16);
|
||||
|
||||
ret += ' ';
|
||||
if (i % 4 == 3) ret += ' ';
|
||||
if (i % 16 == 15) ret += '\n';
|
||||
}
|
||||
if (data.length % 16 != 0) ret += '\n';
|
||||
return ret;b
|
||||
}
|
||||
|
||||
function decodeStream(data, ctx, desc) {
|
||||
do {
|
||||
var frame = cccc.deframe(data, ctx);
|
||||
var msg = frame.data;
|
||||
if (msg) {
|
||||
switch(msg[8]) {
|
||||
case 2: console.debug(desc + ' ping');break;
|
||||
case 3: console.debug(desc + ' pong');break;
|
||||
default:
|
||||
console.debug(desc);
|
||||
console.debug(formatbinary(msg));
|
||||
}
|
||||
}
|
||||
} while (ctx.pending);
|
||||
}
|
||||
|
||||
// Web interface
|
||||
var webmgr = new WebMgr(8000);
|
||||
function logfork(log) {
|
||||
return function() {
|
||||
webmgr.log(Array.from(arguments).join(' '));
|
||||
log.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
["log", "warn", "error", "info", "debug"].forEach(stream => {
|
||||
console[stream] = cfg.verbosity[stream] ? logfork(console[stream]) : () => {};
|
||||
});
|
||||
|
3
install.bat
Normal file
3
install.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
npm i
|
||||
pause
|
80
mime.js
Normal file
80
mime.js
Normal file
@ -0,0 +1,80 @@
|
||||
module.exports = {
|
||||
".aac": "audio/aac",
|
||||
".abw": "application/x-abiword",
|
||||
".arc": "application/x-freearc",
|
||||
".avif": "image/avif",
|
||||
".avi": "video/x-msvideo",
|
||||
".azw": "application/vnd.amazon.ebook",
|
||||
".bin": "application/octet-stream",
|
||||
".bmp": "image/bmp",
|
||||
".bz": "application/x-bzip",
|
||||
".bz2": "application/x-bzip2",
|
||||
".cda": "application/x-cdf",
|
||||
".csh": "application/x-csh",
|
||||
".css": "text/css",
|
||||
".csv": "text/csv",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".epub": "application/epub+zip",
|
||||
".gz": "application/gzip",
|
||||
".gif": "image/gif",
|
||||
".htm": "text/html",
|
||||
".html": "text/html",
|
||||
".ico": "image/vnd.microsoft.icon",
|
||||
".ics": "text/calendar",
|
||||
".jar": "application/java-archive",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".js": "text/javascript",
|
||||
".json": "application/json",
|
||||
".jsonld": "application/ld+json",
|
||||
".mid": "audio/midi",
|
||||
".midi": "audio/midi",
|
||||
".mjs": "text/javascript",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".mpg": "video/mpeg",
|
||||
".mpeg": "video/mpeg",
|
||||
".mpkg": "application/vnd.apple.installer+xml",
|
||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".oga": "audio/ogg",
|
||||
".ogv": "video/ogg",
|
||||
".ogx": "application/ogg",
|
||||
".opus": "audio/opus",
|
||||
".otf": "font/otf",
|
||||
".png": "image/png",
|
||||
".pdf": "application/pdf",
|
||||
".php": "application/x-httpd-php",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".rar": "application/vnd.rar",
|
||||
".rtf": "application/rtf",
|
||||
".sh": "application/x-sh",
|
||||
".svg": "image/svg+xml",
|
||||
".swf": "application/x-shockwave-flash",
|
||||
".tar": "application/x-tar",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".ts": "video/mp2t",
|
||||
".ttf": "font/ttf",
|
||||
".txt": "text/plain",
|
||||
".vsd": "application/vnd.visio",
|
||||
".wav": "audio/wav",
|
||||
".weba": "audio/webm",
|
||||
".webm": "video/webm",
|
||||
".webp": "image/webp",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".xhtml": "application/xhtml+xml",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xml": "application/xml",
|
||||
".xul": "application/vnd.mozilla.xul+xml",
|
||||
".zip": "application/zip",
|
||||
".3gp": "video/3gpp;",
|
||||
".3g2": "video/3gpp2;",
|
||||
".7z": "application/x-7z-compressed",
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
set NETIF="Ethernet"
|
||||
netsh interface ipv4 set interface interface=%NETIF% dhcpstaticipcoexistence=enabled
|
||||
set NETIF="Loopback Pseudo-Interface 1"
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.11 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.12 255.255.255.0 store=active
|
||||
netsh interface ipv4 add address %NETIF% 192.168.139.13 255.255.255.0 store=active
|
||||
|
40
package-lock.json
generated
40
package-lock.json
generated
@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "chunichunimatch",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
76
server.js
76
server.js
@ -2,6 +2,8 @@ const cfg = require('./config');
|
||||
const ws = require('ws');
|
||||
const enc = require('./ccencap');
|
||||
|
||||
console.log('ShadowTenpo Route server ver. ' + cfg.version);
|
||||
|
||||
var server = new ws.WebSocketServer({
|
||||
port: cfg.server_port,
|
||||
perMessageDeflate: false
|
||||
@ -12,8 +14,16 @@ var occupancy = [0, 0, 0, 0];
|
||||
var suffixes = [11, 12, 13, 14];
|
||||
var clients = {};
|
||||
|
||||
function allocateClient() {
|
||||
var slot = -1;
|
||||
function allocateClient(req) {
|
||||
var slot;
|
||||
if (req.pref_ip) {
|
||||
slot = suffixes.indexOf(req.pref_ip);
|
||||
if (slot >= 0 && !occupancy[slot]) {
|
||||
occupancy[slot] = 1;
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
slot = -1;
|
||||
for (var i = 0; i < occupancy.length; i++) {
|
||||
if (!occupancy[i]) {
|
||||
slot = i;
|
||||
@ -25,46 +35,68 @@ function allocateClient() {
|
||||
}
|
||||
|
||||
server.on('connection', function(conn, req) {
|
||||
var slot = allocateClient();
|
||||
var realip = req.socket.remoteAddress;
|
||||
if (slot < 0) {
|
||||
console.log('Dropping ' + realip + ' due to no party vacancy');
|
||||
conn.close();
|
||||
return;
|
||||
}
|
||||
var suffix = suffixes[slot];
|
||||
clients[suffix] = conn;
|
||||
console.log("[Client #" + slot + "] Allocated. Welcome " + realip);
|
||||
var login = null;
|
||||
conn.on('message', function(data) {
|
||||
var msg = enc.decode(data);
|
||||
switch(msg.op) {
|
||||
case 'party_hello':
|
||||
sendMemberChange();
|
||||
login = registerUser(conn, req, msg);
|
||||
break;
|
||||
case 'net_tcp':
|
||||
case 'net_udp':
|
||||
case 'net_tcpfin':
|
||||
case 'net_tcpclose':
|
||||
var dstconn = clients[msg.dst];
|
||||
if (!dstconn) {
|
||||
console.warn('Invalid dst: ' + msg.src + '->' + msg.dst);
|
||||
} else {
|
||||
if (dstconn) {
|
||||
dstconn.send(data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
conn.on('close', function() {
|
||||
console.log("[Client #" + slot + "] Dropped.");
|
||||
delete clients[suffix];
|
||||
occupancy[slot] = 0;
|
||||
if (login) {
|
||||
console.log("[Client #" + login.slot + "] Bye.");
|
||||
delete clients[login.suffix];
|
||||
occupancy[login.slot] = 0;
|
||||
}
|
||||
});
|
||||
conn.on('error', function(e) {
|
||||
console.log("[Client #" + slot + "] Error ", e);
|
||||
conn.close();
|
||||
delete clients[suffix];
|
||||
occupancy[slot] = 0;
|
||||
if (login) {
|
||||
console.log("[Client #" + login.slot + "] Error. Dropped.");
|
||||
delete clients[login.suffix];
|
||||
occupancy[login.slot] = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function registerUser(conn, req, msg) {
|
||||
// Validate login
|
||||
if (msg.version != cfg.version) {
|
||||
// DROP!
|
||||
conn.send(enc.encodeTextMessage("内鬼禁止. Server is at " + cfg.version), () => {
|
||||
conn.close();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allocate User
|
||||
var slot = allocateClient(req);
|
||||
var realip = req.socket.remoteAddress;
|
||||
if (slot < 0) {
|
||||
console.log('Dropping ' + realip + ' due to no party vacancy');
|
||||
conn.send(enc.encodeTextMessage("Sorry, party is full."), () => {
|
||||
conn.close();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
var suffix = suffixes[slot];
|
||||
clients[suffix] = conn;
|
||||
console.log("[Client #" + slot + "] Allocated. Welcome " + realip);
|
||||
sendMemberChange();
|
||||
return {suffix, slot};
|
||||
}
|
||||
|
||||
function sendMemberChange() {
|
||||
// var members = suffixes.filter((suffix, slot) => occupancy[slot]);
|
||||
// Pre-allocate all members on the client to prevent late joiner crash
|
||||
|
43
test/client.js
Normal file
43
test/client.js
Normal file
@ -0,0 +1,43 @@
|
||||
var net = require('net');
|
||||
var dgram = require('dgram');
|
||||
|
||||
var myip = "192.168.139.12";
|
||||
|
||||
var udp50200 = dgram.createSocket('udp4');
|
||||
udp50200.on('message', function(msg, rinfo) {
|
||||
var baseip = msg.toString('ascii');
|
||||
if (!tcpclient) {
|
||||
console.log("Sending TCP Hello to " + baseip);
|
||||
tcphello(baseip);
|
||||
}
|
||||
});
|
||||
udp50200.on('error', function(err) {
|
||||
console.log("Err: ", err);
|
||||
});
|
||||
udp50200.on('listening', function() {
|
||||
console.log('udp50200 listening');
|
||||
})
|
||||
udp50200.bind({port:50200,address:myip});
|
||||
|
||||
var tcpclient = null;
|
||||
function tcphello(host) {
|
||||
if (tcpclient) {
|
||||
console.log("TCP client in use.");
|
||||
return;
|
||||
}
|
||||
tcpclient = new net.Socket();
|
||||
tcpclient.on('data', function(msg) {
|
||||
console.log("tcpclient RX: ", msg.toString('ascii'));
|
||||
});
|
||||
tcpclient.on('end', function() {
|
||||
console.log('tcpclient end');
|
||||
});
|
||||
tcpclient.on('close', function() {
|
||||
console.log('tcpclient close');
|
||||
tcpclient = null;
|
||||
});
|
||||
tcpclient.connect(50200, host, function() {
|
||||
console.log("tcpclient connected");
|
||||
tcpclient.write("FNNC");
|
||||
});
|
||||
}
|
43
test/server.js
Normal file
43
test/server.js
Normal file
@ -0,0 +1,43 @@
|
||||
// Set base configuration here
|
||||
var net = require('net');
|
||||
var dgram = require('dgram');
|
||||
|
||||
var myip = "192.168.139.11";
|
||||
|
||||
// UDP50200 Announce
|
||||
var udpbc = dgram.createSocket('udp4');
|
||||
udpbc.bind(function() {
|
||||
udpbc.setBroadcast(true);
|
||||
console.log('Start udp50200 broadcast');
|
||||
setInterval(function() {
|
||||
// 50200 UDP Broadcast
|
||||
udpbc.send(myip, 50200, '192.168.139.255');
|
||||
// console.log('announce');
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// TCP50200 Base Server
|
||||
var tcp50200 = net.createServer();
|
||||
tcp50200.on('connection', function(conn) {
|
||||
console.log('tcp50200 accept ' + conn.remoteAddress);
|
||||
conn.on('data', function(buf) {
|
||||
console.log("tcp50200("+conn.remoteAddress+"): " + buf.toString('ascii'));
|
||||
for (var i = 0; i < buf.length; i++)
|
||||
buf[i]++;
|
||||
conn.write(buf);
|
||||
});
|
||||
conn.on('close', function() {
|
||||
console.log("tcp50200("+conn.remoteAddress+") bye");
|
||||
if (ping_interval) {
|
||||
clearInterval(ping_interval);
|
||||
ping_interval = null;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
tcp50200.listen({host: myip, port: 50200}, function(e) {
|
||||
if (e) {
|
||||
console.log('tcp50200 listen:', e);
|
||||
}
|
||||
console.log('tcp50200 listen');
|
||||
});
|
94
webmgr.js
Normal file
94
webmgr.js
Normal file
@ -0,0 +1,94 @@
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
const fs = require('fs');
|
||||
const ws = require('ws');
|
||||
|
||||
const mimetypes = require('./mime');
|
||||
|
||||
function WebManager(port) {
|
||||
const server = http.createServer();
|
||||
const wss = new ws.WebSocketServer({ noServer: true });
|
||||
|
||||
this.clients = [];
|
||||
this.clientcnt = 0;
|
||||
|
||||
wss.on('connection', (ws, request) => {
|
||||
var clientid = this.clientcnt;
|
||||
this.clientcnt++;
|
||||
this.clients.push(ws);
|
||||
ws.on('message', msg => {
|
||||
console.log(msg.toString('utf-8'));
|
||||
});
|
||||
ws.on('close', () => {
|
||||
this.clients[clientid] = false;
|
||||
});
|
||||
ws.on('error', () => {
|
||||
this.clients[clientid] = false;
|
||||
});
|
||||
});
|
||||
|
||||
server.on('request', (req, rsp) => {
|
||||
const uri = url.parse(req.url).pathname;
|
||||
if (uri == '/') {
|
||||
filename = 'www/index.html';
|
||||
} else {
|
||||
filename = 'www' + uri;
|
||||
}
|
||||
var ext = filename.match(/\.\w+$/);
|
||||
var mime = null;
|
||||
if (ext) {
|
||||
mime = mimetypes[ext[0]];
|
||||
}
|
||||
if (!mime) {
|
||||
mime = 'application/octet-stream';
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(filename)) {
|
||||
rsp.writeHead(404);
|
||||
rsp.end('Not Found');
|
||||
return;
|
||||
}
|
||||
var stat = fs.statSync(filename);
|
||||
if (!stat) {
|
||||
rsp.writeHead(403);
|
||||
rsp.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
rsp.writeHead(200, {
|
||||
'Content-Length': stat.size,
|
||||
'Content-Type': mime,
|
||||
});
|
||||
rsp.end(fs.readFileSync(filename));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
rsp.writeHead(500);
|
||||
rsp.end(e.toString());
|
||||
}
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const uri = url.parse(request.url).pathname;
|
||||
|
||||
if (uri == '/api') {
|
||||
wss.handleUpgrade(request, socket, head, ws => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
WebManager.prototype.log = function(msg) {
|
||||
this.clients.forEach(ws => {
|
||||
if (!ws) return;
|
||||
ws.send(JSON.stringify({
|
||||
t: 'log',
|
||||
m: msg
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = WebManager;
|
43
www/index.html
Normal file
43
www/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CHUNICHUNIMatch</title>
|
||||
<style type="text/css">
|
||||
pre {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://"+location.host+"/api");
|
||||
socket.addEventListener("message", function (e) {
|
||||
var msg = JSON.parse(e.data);
|
||||
switch(msg.t) {
|
||||
case "log":
|
||||
addlog(msg.m);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
var autoscrollen = 1;
|
||||
function addlog(msg) {
|
||||
var line = document.createElement("pre");
|
||||
line.innerText = msg;
|
||||
document.getElementById("logcat").appendChild(line);
|
||||
if (autoscrollen) {
|
||||
line.scrollIntoView();
|
||||
}
|
||||
}
|
||||
function autoscroll(value) {
|
||||
autoscrollen = value;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Logcat</h3>
|
||||
<button onclick="autoscroll(1)">Auto scroll</button>
|
||||
<div id="logcat"></div>
|
||||
<button onclick="autoscroll(0)">No scroll</button>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user