mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-15 15:19:51 +02:00
Store the cli version for display and handle auto update
This commit is contained in:
@@ -682,6 +682,8 @@ export const newts = pgTable(
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
version: varchar("version"),
|
||||
agent: varchar("agent").default("newt"), // either newt or cli
|
||||
agentVersion: varchar("agentVersion"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
|
||||
@@ -703,6 +703,8 @@ export const newts = sqliteTable(
|
||||
secretHash: text("secretHash").notNull(),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
version: text("version"),
|
||||
agent: text("agent").default("newt"), // either newt or cli
|
||||
agentVersion: text("agentVersion"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
|
||||
@@ -13,31 +13,40 @@ import logger from "@server/logger";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
// Stale-while-revalidate in-memory fallback for the releases API.
|
||||
type ReleaseInfo = {
|
||||
version: string;
|
||||
// binary filename -> sha256 hex (sourced from asset `digest` field in GitHub API)
|
||||
assetDigests: Record<string, string>;
|
||||
};
|
||||
let staleReleaseInfo: ReleaseInfo | null = null;
|
||||
|
||||
// Cache key holding the last known good release info. It never expires, so
|
||||
// it keeps serving if GitHub is unreachable, even across restarts/nodes.
|
||||
const RELEASE_INFO_KEY = "cache:newtReleaseInfo";
|
||||
// Short-lived marker controlling how often we re-check GitHub. While it's
|
||||
// missing (expired, or a previous attempt failed) every request retries.
|
||||
const RELEASE_INFO_FRESH_KEY = "cache:newtReleaseInfoFresh";
|
||||
const RELEASE_INFO_REFRESH_SECONDS = 3600;
|
||||
|
||||
/**
|
||||
* Fetches the latest stable newt release from GitHub and returns the version
|
||||
* tag together with a map of asset-name → sha256 hex digest.
|
||||
* Results are cached for one hour; stale data is returned on failure.
|
||||
* The last successful result is cached indefinitely and re-checked hourly;
|
||||
* on failure the last known good data keeps being served and every
|
||||
* subsequent request retries GitHub until it succeeds again.
|
||||
*/
|
||||
async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
try {
|
||||
const cached = await cache.get<ReleaseInfo>("cache:newtReleaseInfo");
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
async function getLatestReleaseInfo(repo: string): Promise<ReleaseInfo | null> {
|
||||
const stored = await cache.get<ReleaseInfo>(RELEASE_INFO_KEY);
|
||||
const isFresh = await cache.has(RELEASE_INFO_FRESH_KEY);
|
||||
if (stored && isFresh) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const fetchResponse = await fetch(
|
||||
"https://api.github.com/repos/fosrl/newt/releases",
|
||||
`https://api.github.com/repos/fosrl/${repo}/releases`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
@@ -47,13 +56,13 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
logger.warn(
|
||||
`Failed to fetch Newt releases from GitHub: ${fetchResponse.status} ${fetchResponse.statusText}`
|
||||
);
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
let releases: any[] = await fetchResponse.json();
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
logger.warn("No releases found for Newt repository");
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
@@ -82,7 +91,7 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
|
||||
if (releases.length === 0) {
|
||||
logger.warn("No stable releases found for Newt repository");
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
const latest = releases[0];
|
||||
@@ -106,8 +115,12 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
}
|
||||
|
||||
const info: ReleaseInfo = { version, assetDigests };
|
||||
staleReleaseInfo = info;
|
||||
await cache.set("cache:newtReleaseInfo", info, 3600);
|
||||
await cache.set(RELEASE_INFO_KEY, info, 0);
|
||||
await cache.set(
|
||||
RELEASE_INFO_FRESH_KEY,
|
||||
true,
|
||||
RELEASE_INFO_REFRESH_SECONDS
|
||||
);
|
||||
return info;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
@@ -118,14 +131,15 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const bodySchema = z.object({
|
||||
newtId: z.string(),
|
||||
secret: z.string(),
|
||||
platform: z.string() // e.g. "linux_amd64", "darwin_arm64"
|
||||
platform: z.string(), // e.g. "linux_amd64", "darwin_arm64"
|
||||
agent: z.string().optional().default("newt")
|
||||
});
|
||||
|
||||
export type GetNewtVersionBody = z.infer<typeof bodySchema>;
|
||||
@@ -153,7 +167,7 @@ export async function getNewtVersion(
|
||||
);
|
||||
}
|
||||
|
||||
const { newtId, secret, platform } = parsedBody.data;
|
||||
const { newtId, secret, platform, agent } = parsedBody.data;
|
||||
|
||||
try {
|
||||
// Verify newt credentials
|
||||
@@ -258,9 +272,13 @@ export async function getNewtVersion(
|
||||
}
|
||||
|
||||
// Fetch latest release info (version + asset digests) in one API call.
|
||||
const releaseInfo = await getLatestReleaseInfo();
|
||||
const releaseInfoNewt = await getLatestReleaseInfo("newt");
|
||||
let releaseInfoCli: ReleaseInfo | undefined | null;
|
||||
if (agent == "cli") {
|
||||
releaseInfoCli = await getLatestReleaseInfo("cli");
|
||||
}
|
||||
|
||||
if (!releaseInfo) {
|
||||
if (!releaseInfoNewt || (agent == "cli" && !releaseInfoCli)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
@@ -269,18 +287,25 @@ export async function getNewtVersion(
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersion = releaseInfo.version;
|
||||
const latestVersion = releaseInfoNewt.version;
|
||||
|
||||
// Binary name follows the get-newt.sh convention: newt_<platform>[.exe]
|
||||
const binaryName = platform.includes("windows")
|
||||
const binaryNameNewt = platform.includes("windows")
|
||||
? `newt_${platform}.exe`
|
||||
: `newt_${platform}`;
|
||||
|
||||
const downloadUrl = `https://github.com/fosrl/newt/releases/download/${latestVersion}/${binaryName}`;
|
||||
const binaryNameCli = platform.includes("windows")
|
||||
? `pangolin-cli_${platform}.exe`
|
||||
: `pangolin-cli_${platform}`;
|
||||
|
||||
const downloadUrl = `https://github.com/fosrl/newt/releases/download/${agent == "cli" ? releaseInfoCli?.version : releaseInfoNewt.version}/${agent == "cli" ? binaryNameCli : binaryNameNewt}`;
|
||||
|
||||
// Look up the SHA256 digest for this specific binary from the GitHub
|
||||
// release asset metadata (the `digest` field, format "sha256:<hex>").
|
||||
const sha256 = releaseInfo.assetDigests[binaryName] ?? "";
|
||||
const sha256 =
|
||||
releaseInfoNewt.assetDigests[
|
||||
agent == "cli" ? binaryNameCli : binaryNameNewt
|
||||
] ?? "";
|
||||
|
||||
// Determine whether the newt that's asking is already up to date.
|
||||
// We store the current version on the newt row when it registers.
|
||||
@@ -300,8 +325,8 @@ export async function getNewtVersion(
|
||||
|
||||
return response<GetNewtVersionResponse>(res, {
|
||||
data: {
|
||||
latestVersion,
|
||||
currentIsLatest,
|
||||
latestVersion, // this will always be the newt version
|
||||
currentIsLatest, // this will always be based on the newt version
|
||||
downloadUrl,
|
||||
sha256
|
||||
},
|
||||
|
||||
@@ -37,6 +37,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
publicKey,
|
||||
pingResults,
|
||||
newtVersion,
|
||||
agent,
|
||||
agentVersion,
|
||||
backwardsCompatible,
|
||||
chainId
|
||||
} = message.data;
|
||||
@@ -174,17 +176,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
await db
|
||||
.update(newts)
|
||||
.set({
|
||||
version: newtVersion as string
|
||||
})
|
||||
.where(eq(newts.newtId, newt.newtId));
|
||||
}
|
||||
|
||||
if (newtVersion && newtVersion !== newt.version) {
|
||||
// update the newt version in the database
|
||||
await db
|
||||
.update(newts)
|
||||
.set({
|
||||
version: newtVersion as string
|
||||
version: newtVersion as string,
|
||||
agent: agent,
|
||||
agentVersion:
|
||||
!agentVersion && agent == "newt"
|
||||
? newtVersion
|
||||
: agentVersion
|
||||
})
|
||||
.where(eq(newts.newtId, newt.newtId));
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ type SiteQueryRow = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
export type GetSiteResponse = SiteQueryRow["sites"] & {
|
||||
newtId: string | null;
|
||||
newtVersion: string | null;
|
||||
agent: string | null;
|
||||
agentVersion: string | null;
|
||||
countryCode: string | null;
|
||||
};
|
||||
|
||||
@@ -137,6 +139,8 @@ export async function getSite(
|
||||
...site.sites,
|
||||
newtId: site.newt ? site.newt.newtId : null,
|
||||
newtVersion: site.newt?.version ?? null,
|
||||
agent: site.newt?.agent ?? null,
|
||||
agentVersion: site.newt?.agentVersion ?? null,
|
||||
countryCode: site.sites.endpoint
|
||||
? ((await getCountryCodeForIp(site.sites.endpoint)) ?? null)
|
||||
: null
|
||||
|
||||
@@ -133,6 +133,8 @@ function querySitesBase() {
|
||||
online: sites.online,
|
||||
address: sites.address,
|
||||
newtVersion: newts.version,
|
||||
agent: newts.agent,
|
||||
agentVersion: newts.agentVersion,
|
||||
exitNodeId: sites.exitNodeId,
|
||||
exitNodeName: exitNodes.name,
|
||||
exitNodeEndpoint: exitNodes.endpoint,
|
||||
|
||||
Reference in New Issue
Block a user