Playit.gg tunnel #491

Closed
Sirherobrine23 wants to merge 4 commits from 484i into main
14 changed files with 165 additions and 97 deletions

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "@the-bds-maneger/core",
"version": "5.2.0",
"version": "5.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@the-bds-maneger/core",
"version": "5.2.0",
"version": "5.3.0",
"license": "GPL-3.0",
"dependencies": {
"@the-bds-maneger/core-utils": "^1.0.0",

@ -16,7 +16,7 @@
"docs": "typedoc --readme none --out docs src/index.ts",
"dev": "nodemon",
"build": "tsc",
"test:partial": "mocha -r ts-node/register --exit",
"test:partial": "mocha -r ts-node/register -r tsconfig-paths/register --exit",
"test": "npm run test:partial -- 'tests/**/*.ts'"
},
"repository": {

@ -1,6 +1,6 @@
import { platformManeger } from "@the-bds-maneger/server_versions";
import { manegerConfigProprieties } from "./configManipulate";
import { randomPort } from "./lib/randomPort";
import { randomPort } from "./utils/randomPort";
import { pathControl, bdsPlatformOptions } from "./platformPathManeger";
import * as globalPlatfroms from "./globalPlatfroms";
import * as coreUtils from "@the-bds-maneger/core-utils";

@ -1,6 +1,6 @@
import fs from "node:fs/promises";
import utils from "node:util";
import Proprieties, { properitiesBase } from "./lib/Proprieties";
import Proprieties, { properitiesBase } from "./utils/Proprieties";
export type configEdit = {name: string, data?: any};
export async function manegerConfigProprieties<updateConfig extends configEdit, configJson extends properitiesBase = any>(config: {configPath: string, configManipulate: {[Properties in updateConfig["name"]]: ((config: string, value: updateConfig["data"]) => string)|{validate?: (value: updateConfig["data"]) => boolean, regexReplace: RegExp, valueFormat: string, addIfNotExist?: string}}}) {

@ -4,7 +4,7 @@ import * as globalPlatfroms from "./globalPlatfroms";
import * as pluginManeger from "./plugin/plugin";
import * as export_import from "./export_import";
import * as pluginHooks from "./plugin/hook";
import * as proxy from "./lib/proxy";
import * as proxy from "./utils/proxy";
// Platforms
import * as Bedrock from "./bedrock";

@ -3,7 +3,7 @@ import { platformManeger } from "@the-bds-maneger/server_versions";
import { actionV2, actionsV2 } from "./globalPlatfroms";
import { pathControl, bdsPlatformOptions } from "./platformPathManeger";
import { manegerConfigProprieties } from "./configManipulate";
import { randomPort } from "./lib/randomPort";
import { randomPort } from "./utils/randomPort";
import fsOld from "node:fs";
import path from "node:path";
import fs from "node:fs/promises";

@ -1,86 +0,0 @@
import dgram from "node:dgram";
import net from "node:net";
export type proxyUdpToTcpOptions = {
udpType?: dgram.SocketType,
listen?: number,
portListen?: (port: number) => void
};
export type proxyTcpToUdpClient = {
udpType?: dgram.SocketType,
listen?: number,
remote: {
host: string,
port: number
},
};
/**
* Transfer packets from UDP to TCP to send through some tunnel that only accepts TCP
*
* This also means that it will also have error transporting the data, so it is not guaranteed to work properly even more when dealing with UDP packets.
*/
export function proxyUdpToTcp(udpPort: number, options?: proxyUdpToTcpOptions) {
const tcpServer = net.createServer();
tcpServer.on("error", err => console.error(err));
tcpServer.on("connection", socket => {
const udpClient = dgram.createSocket(options?.udpType||"udp4");
// Close Sockets
udpClient.once("close", () => socket.end());
socket.once("close", () => udpClient.close());
// Print error
udpClient.on("error", console.error);
socket.on("error", console.error);
// Pipe Datas
udpClient.on("message", data => socket.write(data));
socket.on("data", data => udpClient.send(data));
// Connect
udpClient.connect(udpPort);
});
// Listen
tcpServer.listen(options?.listen||0, function() {
const addr = this.address();
if (options?.portListen) options.portListen(addr.port);
console.debug("bds proxy port listen, %s, (udp -> tcp)", addr.port);
tcpServer.once("close", () => console.debug("bds proxy close, %s", addr.port));
});
return tcpServer;
}
export function proxyTcpToUdp(options: proxyTcpToUdpClient) {
const sessions: {[keyIP: string]: net.Socket} = {};
const udp = dgram.createSocket(options?.udpType||"udp4");
udp.on("error", console.error);
udp.on("message", (msg, ipInfo) => {
const keyInfo = `${ipInfo.address}:${ipInfo.port}`;
// Client TCP
if (!sessions[keyInfo]) {
sessions[keyInfo] = net.createConnection(options.remote);
sessions[keyInfo].on("data", data => udp.send(data, ipInfo.port, ipInfo.address));
sessions[keyInfo].on("error", console.error);
sessions[keyInfo].once("close", () => {
delete sessions[keyInfo];
console.log("Client %s:%f close", ipInfo.address, ipInfo.port);
});
console.log("Client %s:%f connected", ipInfo.address, ipInfo.port);
}
// Send message
sessions[keyInfo].write(msg);
});
// Listen port
udp.bind(options.listen||0, function(){
const addr = this.address();
console.log("Port listen, %s (tcp -> udp)", addr.port);
});
}

@ -3,7 +3,7 @@ import { pathControl, bdsPlatformOptions } from "./platformPathManeger";
import { spigotProprieties } from "./spigot";
import * as globalPlatfroms from "./globalPlatfroms";
import * as coreUtils from "@the-bds-maneger/core-utils";
import Proprieties from "./lib/Proprieties";
import Proprieties from "./utils/Proprieties";
import fsOld from "node:fs";
import path from "node:path";
import fs from "node:fs/promises";

@ -1,7 +1,7 @@
import { httpRequest, httpRequestLarge, httpRequestGithub } from "@the-bds-maneger/core-utils";
import { pathControl, bdsPlatformOptions } from "./platformPathManeger";
import * as globalPlatfroms from "./globalPlatfroms";
import Proprieties from "./lib/Proprieties"
import Proprieties from "./utils/Proprieties"
import fsOld from "node:fs";
import path from "node:path";
import fs from "node:fs/promises";

150
src/utils/proxy.ts Normal file

@ -0,0 +1,150 @@
import type { HTTPError } from "got";
import { httpRequest } from "@the-bds-maneger/core-utils";
import { format } from "node:util";
import crypto from "node:crypto";
// import dgram from "node:dgram";
import net from "node:net";
export type agentSecret = {type: "agent-secret", secret_key: string};
export type playitTunnelOptions = {secretKey: string, apiUrl?: string, controlAddress?: string, clientVersion?: string};
export type tunnelList = {
type: "account-tunnels",
agent_id: string,
tunnels: {
id: string,
enabled: boolean,
name?: string,
ip_address: string,
ip_hostname: string,
custom_domain?: string,
assigned_domain: string,
display_address: string,
is_dedicated_ip: boolean,
from_port: number,
to_port: number,
tunnel_type: "minecraft-bedrock"|"minecraft-java",
port_type: "udp"|"tcp"|"both",
firewall_id?: string,
protocol: {
protocol: "to-agent",
local_ip: string,
local_port: number,
agent_id: number
}
}[]
}
export type playitAgentAccountStatus = {type: "agent-account-status", status: "verified-account", account_id: number};
export type agentConfig = {
type: "agent-config",
last_update: number,
ping_targets: string[],
ping_target_addresses: string[],
control_address: string,
refresh_from_api: true,
secret_key: string,
mappings: []
};
export type playitTunnelAuth = {
type: "signed-tunnel-request",
auth: {
details: {
account_id: number,
request_timestamp: number,
session_id: number
},
signature: {
System: {
signature: number[]
}
}
},
content: number[]
}
/**
* Create a key to asynchronously authenticate playit.gg clients
*/
export async function playitClainSecret(clainUrlCallback?: (url: string) => void) {
const claimCode = crypto.pseudoRandomBytes(5).toString("hex");
const url = format("https://playit.gg/claim/%s?type=%s&name=%s", claimCode, "self-managed", `bdscore_agent`);
if (clainUrlCallback) clainUrlCallback(url); else console.log("Playit claim url: %s", url);
// Register to API
let waitAuth = 0;
let authAttempts = 0;
async function getSecret(): Promise<agentSecret> {
return httpRequest.getJSON({url: "https://api.playit.cloud/agent", method: "POST", headers: {}, body: {type: "exchange-claim-for-secret", claim_key: claimCode}}).catch(async (err: HTTPError) => {
if (err?.response?.statusCode === 404||err?.response?.statusCode === 401) {
if (err.response.statusCode === 404) if (authAttempts++ > 225) throw new Error("client not open auth url");
if (err.response.statusCode === 401) if (waitAuth++ > 16) throw new Error("Claim code not authorized per client");
await new Promise(resolve => setTimeout(resolve, 500));
return getSecret();
}
throw err;
});
}
return (await getSecret()).secret_key;
}
/**
* Crie um tunnel para o minecraft bedrock ou minecraft java para que usuarios fora da rede possa se conectar no servidor
*/
export async function playitTunnel(options: playitTunnelOptions) {
options = {apiUrl: "api.playit.cloud", controlAddress: "control.playit.gg", clientVersion: "0.2.3", ...options};
if (!options.secretKey) throw new Error("Required secret key to auth in playit.gg");
const Authorization = format("agent-key %s", options.secretKey);
const playitApiUrls = {
agent: format("https://%s/agent", options.apiUrl),
account: format("https://%s/account", options.apiUrl)
};
const accountInfo = await httpRequest.getJSON<playitAgentAccountStatus>({url: playitApiUrls.agent, method: "POST", headers: {Authorization}, body: {type: "get-agent-account-status", client_version: options.clientVersion}}).catch((err: HTTPError) => {
if (err.response.statusCode === 400) throw new Error("Secret key is invalid or not registred");
throw err;
});
if (accountInfo.status !== "verified-account") throw new Error("Verify account fist");
const agentInfo = await httpRequest.getJSON<agentConfig>({url: playitApiUrls.agent, method: "POST", headers: {Authorization}, body: {type: "get-agent-config", client_version: options.clientVersion}});
const signTunnel = await httpRequest.getJSON<playitTunnelAuth>({url: playitApiUrls.agent, method: "POST", headers: {Authorization}, body: { type: "sign-tunnel-request", RegisterAgent: null }}).catch(err => err.response?.body?.toString()||err);
async function listTunnels() {
const data = await httpRequest.getJSON<tunnelList>({
url: playitApiUrls.account,
method: "POST",
headers: {Authorization},
body: {
type: "list-account-tunnels"
}
}).catch(err => Promise.reject(JSON.parse(err.response?.body?.toString())));
data.tunnels = data.tunnels.filter(tunnel => (["minecraft-bedrock", "minecraft-java"]).includes(tunnel.tunnel_type));
return data;
}
async function createTunnel(options: {tunnel_for: "bedrock"|"java", name: string, local_ip: string, local_port: number}) {
const agent_id = (await listTunnels()).agent_id;
const tunnelCreated = await httpRequest.getJSON<{ type: "created", id: string }>({
url: playitApiUrls.account,
method: "POST",
headers: {Authorization},
body: {
type: "create-tunnel", agent_id,
name: options.name,
tunnel_type: options.tunnel_for === "bedrock"?"minecraft-bedrock":"minecraft-java",
port_type: options.tunnel_for === "bedrock"?"udp":"tcp",
port_count: 1,
local_ip: options.local_ip, local_port: options.local_port,
}
});
return (await listTunnels()).tunnels.find(tunnel => tunnel.id === tunnelCreated.id);
}
async function connectTunnel(name?: string) {
const tunnelInfo = (await listTunnels()).tunnels.find(tunnel => name?(tunnel.name === name||tunnel.id === name):true);
if (!tunnelInfo) throw new Error("No tunnel selected");
if (tunnelInfo.port_type !== "tcp") throw new Error("Current support only TCP tunnel");
}
return {agentInfo, signTunnel, listTunnels, createTunnel, connectTunnel};
}

@ -1,4 +1,4 @@
import Proprieties from "../../src/lib/Proprieties";
import Proprieties from "../../src/utils/Proprieties";
const stringExample = `test.ls=aaa\ntest.ab=true`;
describe("Proprieties", () => {

@ -11,7 +11,11 @@
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"allowJs": true,
"lib": ["ES6"]
"lib": ["ES6"],
"baseUrl": ".",
"paths": {
"@core_utils/*": ["./src/utils/*"]
}
},
"include": [
"src/**/*"