chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+99
View File
@@ -0,0 +1,99 @@
const got = require("got");
class NgrokClientError extends Error {
constructor(message, response, body) {
super(message);
this.name = "NgrokClientError";
this.response = response;
this.body = body;
}
}
class NgrokClient {
constructor(processUrl) {
this.internalApi = got.extend({
prefixUrl: processUrl,
retry: 0,
});
}
async request(method, path, options = {}) {
try {
if (method === "get") {
return await this.internalApi
.get(path, { searchParams: options })
.json();
} else {
return await this.internalApi[method](path, { json: options }).json();
}
} catch (error) {
let clientError;
try {
const response = JSON.parse(error.response.body);
clientError = new NgrokClientError(
response.msg,
error.response,
response
);
} catch (e) {
clientError = new NgrokClientError(
error.response.body,
error.response,
error.response.body
);
}
throw clientError;
}
}
async booleanRequest(method, path, options = {}) {
try {
return await this.internalApi[method](path, { json: options }).then(
(response) => response.statusCode === 204
);
} catch (error) {
const response = JSON.parse(error.response.body);
throw new NgrokClientError(response.msg, error.response, response);
}
}
listTunnels() {
return this.request("get", "api/tunnels");
}
startTunnel(options = {}) {
return this.request("post", "api/tunnels", options);
}
tunnelDetail(name) {
return this.request("get", `api/tunnels/${name}`);
}
stopTunnel(name) {
if (typeof name === "undefined" || name.length === 0) {
throw new Error("To stop a tunnel, please provide a name.");
}
return this.booleanRequest("delete", `api/tunnels/${name}`);
}
listRequests(options) {
return this.request("get", "api/requests/http", options);
}
replayRequest(id, tunnelName) {
return this.booleanRequest("post", "api/requests/http", { id, tunnelName });
}
deleteAllRequests() {
return this.booleanRequest("delete", "api/requests/http");
}
requestDetail(id) {
if (typeof id === "undefined" || id.length === 0) {
throw new Error("To get the details of a request, please provide an id.");
}
return this.request("get", `api/requests/http/${id}`);
}
}
module.exports = { NgrokClient, NgrokClientError };
+166
View File
@@ -0,0 +1,166 @@
const { promisify } = require("util");
const { spawn, exec: execCallback } = require("child_process");
const exec = promisify(execCallback);
const bin = require("@expo/ngrok-bin");
const ready = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
const inUse = /address already in use/;
let processPromise, activeProcess;
/*
ngrok process runs internal ngrok api
and should be spawned only ONCE
(respawn allowed if it fails or .kill method called)
*/
async function getProcess(opts) {
if (processPromise) return processPromise;
try {
processPromise = startProcess(opts);
return await processPromise;
} catch (ex) {
processPromise = null;
throw ex;
}
}
function getActiveProcess() {
return activeProcess;
}
function parseAddr(message) {
if (message[0] === "{") {
const parsed = JSON.parse(message);
return parsed.addr
} else {
const parsed = message.match(ready);
if (parsed) {
return parsed[1];
}
}
}
async function startProcess(opts) {
const start = ["start", "--none", "--log=stdout"];
if (opts.region) start.push("--region=" + opts.region);
if (opts.configPath) start.push("--config=" + opts.configPath);
const ngrok = spawn(bin, start, { windowsHide: true });
let resolve, reject;
const apiUrl = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
ngrok.stdout.on("data", (data) => {
const msg = data.toString().trim();
if (opts.onLogEvent) {
opts.onLogEvent(msg);
}
if (opts.onStatusChange) {
if (msg.match("client session established")) {
opts.onStatusChange("connected");
} else if (msg.match("session closed, starting reconnect loop")) {
opts.onStatusChange("closed");
}
}
const msgs = msg.split(/\n/);
msgs.forEach(msg => {
const addr = parseAddr(msg);
if (addr) {
resolve(`http://${addr}`);
} else if (msg.match(inUse)) {
reject(new Error(msg.substring(0, 10000)));
}
})
});
ngrok.stderr.on("data", (data) => {
const msg = data.toString().substring(0, 10000);
reject(new Error(msg));
});
ngrok.on("exit", () => {
processPromise = null;
activeProcess = null;
});
ngrok.on("error", (err) => {
reject(err);
});
try {
const url = await apiUrl;
activeProcess = ngrok;
return url;
} catch (ex) {
ngrok.kill();
throw ex;
} finally {
// Remove the stdout listeners if nobody is interested in the content.
if (!opts.onLogEvent && !opts.onStatusChange) {
ngrok.stdout.removeAllListeners("data");
}
ngrok.stderr.removeAllListeners("data");
}
}
function killProcess() {
if (!activeProcess) {
return Promise.resolve();
}
return new Promise((resolve) => {
activeProcess.on("exit", () => resolve());
activeProcess.kill();
});
}
process.on("exit", () => {
if (activeProcess) {
activeProcess.kill();
}
});
/**
* @param {string | Ngrok.Options} optsOrToken
*/
async function setAuthtoken(optsOrToken) {
const isOpts = typeof optsOrToken !== "string";
const opts = isOpts ? optsOrToken : {};
const token = isOpts ? opts.authtoken : optsOrToken;
const authtoken = ["authtoken", token];
if (opts.configPath) authtoken.push("--config=" + opts.configPath);
const ngrok = spawn(bin, authtoken, { windowsHide: true });
const killed = new Promise((resolve, reject) => {
ngrok.stdout.once("data", () => resolve());
ngrok.stderr.once("data", () => reject(new Error("cant set authtoken")));
ngrok.on("error", (err) => reject(err));
});
try {
return await killed;
} finally {
ngrok.kill();
}
}
/**
* @param {Ngrok.Options | undefined} opts
*/
async function getVersion(opts = {}) {
const { stdout } = await exec(`${bin} --version`);
return stdout.replace("ngrok version", "").trim();
}
module.exports = {
getProcess,
getActiveProcess,
killProcess,
setAuthtoken,
getVersion,
};
+58
View File
@@ -0,0 +1,58 @@
const { homedir } = require("os");
const { join } = require("path");
const { parse } = require("yaml");
const { readFileSync } = require("fs");
function defaultConfigPath() {
return join(homedir(), ".ngrok2", "ngrok.yml");
}
function defaults(opts) {
opts = opts || { proto: "http", addr: 80 };
if (opts.name) {
const configPath = opts.configPath || defaultConfigPath();
const config = parse(readFileSync(configPath, "utf8"));
if (config.tunnels && config.tunnels[opts.name]) {
opts = Object.assign(opts, config.tunnels[opts.name]);
}
}
if (typeof opts === "function") opts = { proto: "http", addr: 80 };
if (typeof opts !== "object") opts = { proto: "http", addr: opts };
if (!opts.proto) opts.proto = "http";
if (!opts.addr) opts.addr = opts.port || opts.host || 80;
if (opts.httpauth) opts.auth = opts.httpauth;
return opts;
}
function validate(opts) {
if (opts.web_addr === false || opts.web_addr === "false") {
throw new Error(
"web_addr:false is not supported, module depends on internal ngrok api"
);
}
}
function isRetriable(err) {
if (!err.response) {
return false;
}
const statusCode = err.response.statusCode;
const body = err.body;
const notReady500 = statusCode === 500 && /panic/.test(body);
const notReady502 =
statusCode === 502 &&
body.details &&
body.details.err === "tunnel session not ready yet";
const notReady503 =
statusCode === 503 &&
body.details &&
body.details.err ===
"a successful ngrok tunnel session has not yet been established";
return notReady500 || notReady502 || notReady503;
}
module.exports = {
defaults,
validate,
isRetriable,
};