re-org source tree slightly
network layer is net/ protocol is protocol/
This commit is contained in:
14
cvmts/src/net/NetworkClient.ts
Normal file
14
cvmts/src/net/NetworkClient.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { EventEmitter } from "stream";
|
||||
|
||||
interface NetworkClientEvents extends EventEmitter {
|
||||
on(event: 'msg', listener: (buf: Buffer, binary: boolean) => void): this;
|
||||
on(event: 'disconnect', listener: () => void): this;
|
||||
}
|
||||
|
||||
export interface NetworkClient extends NetworkClientEvents {
|
||||
getIP(): string;
|
||||
send(msg: string): Promise<void>;
|
||||
sendBinary(msg: Uint8Array): Promise<void>;
|
||||
close(): void;
|
||||
isOpen(): boolean;
|
||||
}
|
||||
11
cvmts/src/net/NetworkServer.ts
Normal file
11
cvmts/src/net/NetworkServer.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { EventEmitter } from "stream";
|
||||
import { User } from "../User";
|
||||
|
||||
interface NetworkServerEvents extends EventEmitter {
|
||||
on(event: 'connect', listener: (user: User) => void): this;
|
||||
}
|
||||
|
||||
export interface NetworkServer extends NetworkServerEvents {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
72
cvmts/src/net/tcp/TCPClient.ts
Normal file
72
cvmts/src/net/tcp/TCPClient.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import EventEmitter from 'events';
|
||||
import { NetworkClient } from '../NetworkClient.js';
|
||||
import { Socket } from 'net';
|
||||
|
||||
const TextHeader = 0;
|
||||
const BinaryHeader = 1;
|
||||
|
||||
export default class TCPClient extends EventEmitter implements NetworkClient {
|
||||
private socket: Socket;
|
||||
private cache: string;
|
||||
|
||||
constructor(socket: Socket) {
|
||||
super();
|
||||
this.socket = socket;
|
||||
this.cache = '';
|
||||
this.socket.on('end', () => {
|
||||
this.emit('disconnect');
|
||||
});
|
||||
this.socket.on('data', (data) => {
|
||||
var msg = data.toString('utf-8');
|
||||
if (msg[msg.length - 1] === '\n') msg = msg.slice(0, -1);
|
||||
this.cache += msg;
|
||||
this.readCache();
|
||||
});
|
||||
}
|
||||
|
||||
private readCache() {
|
||||
for (var index = this.cache.indexOf(';'); index !== -1; index = this.cache.indexOf(';')) {
|
||||
this.emit('msg', this.cache.slice(0, index + 1));
|
||||
this.cache = this.cache.slice(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
getIP(): string {
|
||||
return this.socket.remoteAddress!;
|
||||
}
|
||||
|
||||
send(msg: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
let _msg = new Uint32Array([TextHeader, ...Buffer.from(msg, 'utf-8')]);
|
||||
this.socket.write(Buffer.from(_msg), (err) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendBinary(msg: Uint8Array): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
let _msg = new Uint32Array([BinaryHeader, msg.length, ...msg]);
|
||||
this.socket.write(Buffer.from(_msg), (err) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.emit('disconnect');
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
isOpen(): boolean {
|
||||
return this.socket.writable;
|
||||
}
|
||||
}
|
||||
50
cvmts/src/net/tcp/TCPServer.ts
Normal file
50
cvmts/src/net/tcp/TCPServer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// TODO: replace tcp protocol with smth like
|
||||
// struct msg { beu32 len; char data[len] }
|
||||
// (along with a length cap obviously)
|
||||
import EventEmitter from 'events';
|
||||
import { NetworkServer } from '../NetworkServer.js';
|
||||
import { Server, Socket } from 'net';
|
||||
import IConfig from '../../IConfig.js';
|
||||
import TCPClient from './TCPClient.js';
|
||||
import { IPDataManager } from '../../IPData.js';
|
||||
import { User } from '../../User.js';
|
||||
import pino from 'pino';
|
||||
import { BanManager } from '../../BanManager.js';
|
||||
|
||||
export default class TCPServer extends EventEmitter implements NetworkServer {
|
||||
listener: Server;
|
||||
Config: IConfig;
|
||||
logger = pino({ name: 'CVMTS.TCPServer' });
|
||||
clients: TCPClient[];
|
||||
private banmgr: BanManager;
|
||||
|
||||
constructor(config: IConfig, banmgr: BanManager) {
|
||||
super();
|
||||
this.Config = config;
|
||||
this.listener = new Server();
|
||||
this.clients = [];
|
||||
this.listener.on('connection', (socket) => this.onConnection(socket));
|
||||
this.banmgr = banmgr;
|
||||
}
|
||||
|
||||
private async onConnection(socket: Socket) {
|
||||
this.logger.info(`New TCP connection from ${socket.remoteAddress}`);
|
||||
if (await this.banmgr.isIPBanned(socket.remoteAddress!)) {
|
||||
socket.write('6.banned;');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
var client = new TCPClient(socket);
|
||||
this.clients.push(client);
|
||||
this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config));
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => {
|
||||
this.logger.info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`);
|
||||
});
|
||||
}
|
||||
stop(): void {
|
||||
this.listener.close();
|
||||
}
|
||||
}
|
||||
76
cvmts/src/net/ws/WSClient.ts
Normal file
76
cvmts/src/net/ws/WSClient.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import { NetworkClient } from '../NetworkClient.js';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export default class WSClient extends EventEmitter implements NetworkClient {
|
||||
socket: WebSocket;
|
||||
ip: string;
|
||||
enforceTextOnly = true
|
||||
|
||||
constructor(ws: WebSocket, ip: string) {
|
||||
super();
|
||||
this.socket = ws;
|
||||
this.ip = ip;
|
||||
this.socket.on('message', (buf: Buffer, isBinary: boolean) => {
|
||||
// Close the user's connection if they send a binary message
|
||||
// when we are not expecting them yet.
|
||||
if (isBinary && this.enforceTextOnly) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('msg', buf, isBinary);
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
this.emit('disconnect');
|
||||
});
|
||||
}
|
||||
|
||||
isOpen(): boolean {
|
||||
return this.socket.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
getIP(): string {
|
||||
return this.ip;
|
||||
}
|
||||
|
||||
send(msg: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
if (!this.isOpen()) res();
|
||||
|
||||
this.socket.send(msg, (err) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendBinary(msg: Uint8Array): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
if (!this.isOpen()) res();
|
||||
|
||||
this.socket.send(msg, (err) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.isOpen()) {
|
||||
// While this seems counterintutive, do note that the WebSocket protocol
|
||||
// *sends* a data frame whilist closing a connection. Therefore, if the other end
|
||||
// has forcibly hung up (closed) their connection, the best way to handle that
|
||||
// is to just let the inner TCP socket propegate that, which `ws` will do for us.
|
||||
// Otherwise, we'll try to send data to a closed client then SIGPIPE.
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
152
cvmts/src/net/ws/WSServer.ts
Normal file
152
cvmts/src/net/ws/WSServer.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as http from 'http';
|
||||
import { NetworkServer } from '../NetworkServer.js';
|
||||
import EventEmitter from 'events';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import internal from 'stream';
|
||||
import IConfig from '../../IConfig.js';
|
||||
import { isIP } from 'net';
|
||||
import { IPDataManager } from '../../IPData.js';
|
||||
import WSClient from './WSClient.js';
|
||||
import { User } from '../../User.js';
|
||||
import pino from 'pino';
|
||||
import { BanManager } from '../../BanManager.js';
|
||||
|
||||
export default class WSServer extends EventEmitter implements NetworkServer {
|
||||
private httpServer: http.Server;
|
||||
private wsServer: WebSocketServer;
|
||||
private clients: WSClient[];
|
||||
private Config: IConfig;
|
||||
private logger = pino({ name: 'CVMTS.WSServer' });
|
||||
private banmgr: BanManager;
|
||||
|
||||
constructor(config: IConfig, banmgr: BanManager) {
|
||||
super();
|
||||
this.Config = config;
|
||||
this.clients = [];
|
||||
this.httpServer = http.createServer();
|
||||
this.wsServer = new WebSocketServer({ noServer: true });
|
||||
this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head));
|
||||
this.httpServer.on('request', (req, res) => {
|
||||
res.writeHead(426);
|
||||
res.write('This server only accepts WebSocket connections.');
|
||||
res.end();
|
||||
});
|
||||
this.banmgr = banmgr;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => {
|
||||
this.logger.info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.httpServer.close();
|
||||
}
|
||||
|
||||
private async httpOnUpgrade(req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) {
|
||||
var killConnection = () => {
|
||||
socket.write('HTTP/1.1 400 Bad Request\n\n400 Bad Request');
|
||||
socket.destroy();
|
||||
};
|
||||
|
||||
if (req.headers['sec-websocket-protocol'] !== 'guacamole') {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.Config.http.origin) {
|
||||
// If the client is not sending an Origin header, kill the connection.
|
||||
if (!req.headers.origin) {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the Origin header sent by the client, if it fails, kill the connection.
|
||||
var _uri;
|
||||
var _host;
|
||||
try {
|
||||
_uri = new URL(req.headers.origin.toLowerCase());
|
||||
_host = _uri.host;
|
||||
} catch {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// detect fake origin headers
|
||||
if (_uri.pathname !== '/' || _uri.search !== '') {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the domain name is not in the list of allowed origins, kill the connection.
|
||||
if (!this.Config.http.originAllowedDomains.includes(_host)) {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let ip: string;
|
||||
if (this.Config.http.proxying) {
|
||||
// If the requesting IP isn't allowed to proxy, kill it
|
||||
if (this.Config.http.proxyAllowedIps.indexOf(req.socket.remoteAddress!) === -1) {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
// Make sure x-forwarded-for is set
|
||||
if (req.headers['x-forwarded-for'] === undefined) {
|
||||
killConnection();
|
||||
this.logger.error('X-Forwarded-For header not set. This is most likely a misconfiguration of your reverse proxy.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Get the first IP from the X-Forwarded-For variable
|
||||
ip = req.headers['x-forwarded-for']?.toString().replace(/\ /g, '').split(',')[0];
|
||||
} catch {
|
||||
// If we can't get the IP, kill the connection
|
||||
this.logger.error('Invalid X-Forwarded-For header. This is most likely a misconfiguration of your reverse proxy.');
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
// If for some reason the IP isn't defined, kill it
|
||||
if (!ip) {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
// Make sure the IP is valid. If not, kill the connection.
|
||||
if (!isIP(ip)) {
|
||||
killConnection();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!req.socket.remoteAddress) return;
|
||||
ip = req.socket.remoteAddress;
|
||||
}
|
||||
|
||||
if (await this.banmgr.isIPBanned(ip)) {
|
||||
socket.write('HTTP/1.1 403 Forbidden\n\nYou have been banned.');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
|
||||
this.wsServer.emit('connection', ws, req);
|
||||
this.onConnection(ws, req, ip);
|
||||
});
|
||||
}
|
||||
|
||||
private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string) {
|
||||
let client = new WSClient(ws, ip);
|
||||
this.clients.push(client);
|
||||
let user = new User(client, IPDataManager.GetIPData(ip), this.Config);
|
||||
|
||||
this.emit('connect', user);
|
||||
|
||||
ws.on('error', (e) => {
|
||||
this.logger.error(`${e} (caused by connection ${ip})`);
|
||||
ws.close();
|
||||
});
|
||||
|
||||
this.logger.info(`New WebSocket connection from ${user.IP.address}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user