1
0
mirror of synced 2024-11-28 00:20:50 +01:00

0.1.1 release

This commit is contained in:
Fang Lu 2022-02-27 14:56:44 -08:00
parent 65dcf4bd86
commit 57bc85c0dc
18 changed files with 691 additions and 121 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
package-lock.json
node_modules/
*.ini

113
README.md
View File

@ -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
View 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;

View File

@ -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));

View File

@ -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)
}

View File

@ -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;

View File

@ -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,
};

View File

@ -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
View File

@ -0,0 +1,3 @@
@echo off
npm i
pause

80
mime.js Normal file
View 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",
};

View File

@ -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
View File

@ -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": {}
}
}
}

View File

@ -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

3
start.bat Normal file
View File

@ -0,0 +1,3 @@
@echo off
node .
pause

43
test/client.js Normal file
View 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
View 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
View 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
View 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>