Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/server/src/cloud/npmRelease.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,16 @@ it.effect("authenticates the fork API without sending the GitHub token to an ass
expect(request.headers.authorization).toBe("Bearer test-token");
return json([{ tag_name: `v${version}` }]);
}
expect(request.url).toBe(`https://mirror.example/v${version}/t3-release.json`);
expect(request.url).toBe(
`https://mirror.example/releases/v${version}/t3-release.json?region=test`,
);
expect(request.headers.authorization).toBeUndefined();
return json({ version, databaseMigration: 85 });
},
{ GH_TOKEN: "test-token", T3CODE_RELEASE_BASE_URL: "https://mirror.example/" },
{
GH_TOKEN: "test-token",
T3CODE_RELEASE_BASE_URL: "https://mirror.example/releases/?region=test",
},
);
expect(yield* service.get({ spec: "nightly" })).toEqual({ version, databaseMigration: 85 });
}),
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/cloud/npmRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
CLI_RELEASE_BASE_URL_ENV,
CLI_RELEASE_METADATA_FILE,
CLI_RELEASE_REPOSITORY,
cliReleaseDownloadBaseUrl,
cliReleaseAssetUrl,
cliReleaseIndexPageUrl,
newestCliReleaseVersion,
} from "@t3tools/shared/cliRelease";
Expand Down Expand Up @@ -112,7 +112,7 @@ export const make = Effect.fn("NpmRelease.make")(function* () {
const version = yield* resolveVersion(spec);
if (version === null) return null;
const json = yield* read(
`${cliReleaseDownloadBaseUrl(version, releaseBaseUrl)}/${CLI_RELEASE_METADATA_FILE}`,
cliReleaseAssetUrl(version, CLI_RELEASE_METADATA_FILE, releaseBaseUrl),
{ api: false, allowMissing: true },
);
// Older releases have no migration asset; track-switch checks must keep them blocked.
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/cliRelease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function cliReleaseDownloadBaseUrl(
version: string,
baseUrl: string | undefined = CLI_RELEASE_DEFAULT_BASE_URL,
): string {
return `${(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL).replace(/\/+$/, "")}/v${version}`;
return releaseAssetUrl(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL, `v${version}`);
}

/** `segments` added to `baseUrl`'s path, keeping its query, such as a signed mirror URL's token. */
Expand Down
46 changes: 43 additions & 3 deletions packages/ssh/src/tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")(
function* (
installedVersion: string,
installDirectory = ".local/bin",
options?: { readonly cliScript?: string; readonly fifo?: boolean },
options?: {
readonly cliScript?: string;
readonly fifo?: boolean;
readonly releaseBaseUrl?: string;
},
) {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped();
Expand All @@ -110,9 +114,20 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")(
);
yield* fs.chmod(`${root}/${installDirectory}/t3`, 0o700);
}
yield* fs.writeFileString(`${root}/bin/curl`, '#!/bin/sh\necho "HTTP 404" >&2\nexit 22\n');
yield* fs.writeFileString(
`${root}/bin/curl`,
'#!/bin/sh\nprintf "%s\\n" "$@" > "$HOME/request"\necho "HTTP 404 $*" >&2\nexit 22\n',
);
yield* fs.chmod(`${root}/bin/curl`, 0o700);
yield* fs.writeFileString(`${root}/runner.sh`, SshTunnel.buildRemoteT3RunnerScript(ARCHIVE));
yield* fs.writeFileString(
`${root}/runner.sh`,
SshTunnel.buildRemoteT3RunnerScript({
...ARCHIVE,
...(options?.releaseBaseUrl === undefined
? {}
: { releaseBaseUrl: options.releaseBaseUrl }),
}),
);
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const child = yield* spawner.spawn(
ChildProcess.make("sh", [`${root}/runner.sh`, "pair", "--json"], {
Expand All @@ -134,6 +149,9 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")(
stdout,
stderr,
exitCode,
request: (yield* fs.exists(`${root}/request`))
? yield* fs.readFileString(`${root}/request`)
: "",
runtimeExists: yield* fs.exists(`${root}/.t3/runtime/versions/${ARCHIVE.archiveVersion}`),
};
},
Expand Down Expand Up @@ -292,6 +310,28 @@ esac
}),
);

it.live("keeps signed mirror credentials in the request and out of diagnostics", () =>
Effect.gen(function* () {
const result = yield* runArchiveRunner("0.0.1", ".t3/bin", {
releaseBaseUrl:
"https://mirror-user:mirror-pass@mirror.example/releases/?signature=test-secret#download",
});
assert.notEqual(result.exitCode, 0);
assert.include(
result.stderr,
`https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS`,
);
assert.include(
result.request,
`https://mirror-user:mirror-pass@mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?signature=test-secret`,
);
for (const secret of ["mirror-user", "mirror-pass", "signature", "test-secret", "download"]) {
assert.notInclude(result.stderr, secret === "download" ? "#download" : secret);
}
assert.isFalse(result.runtimeExists);
}),
);

it.live(
"skips an installed CLI whose probes hang and falls through to the archive download",
() =>
Expand Down
26 changes: 18 additions & 8 deletions packages/ssh/src/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ fi
# Self-contained release archive: no Node, npm, or compiler on the remote.
# Unpacked into the pinned-runtime layout so \`t3 service install\` reuses it.
T3_RELEASE_BASE_URL=@@T3_RELEASE_BASE_URL@@
T3_RELEASE_DISPLAY_URL=@@T3_RELEASE_DISPLAY_URL@@
T3_RELEASE_QUERY=@@T3_RELEASE_QUERY@@
T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION"
t3_runtime_ready() {
[ -x "$T3_RUNTIME_DIR/t3" ] && [ "$(cat "$T3_RUNTIME_DIR/.install-complete" 2>/dev/null)" = "$T3_ARCHIVE_VERSION" ]
Expand Down Expand Up @@ -529,16 +531,16 @@ if ! t3_runtime_ready; then
trap 'rm -rf "$T3_STAGING" "$T3_LOCK"' EXIT
t3_fetch() {
if command -v curl >/dev/null 2>&1; then
if curl -fsSL --connect-timeout 30 --max-time "$3" "$1" -o "$2"; then return; fi
if curl -fsSL --connect-timeout 30 --max-time "$3" "$1" -o "$2" 2>/dev/null; then return; else T3_FETCH_EXIT=$?; fi
elif command -v wget >/dev/null 2>&1; then
if wget -q --timeout=30 --tries=1 "$1" -O "$2"; then return; fi
if wget -q --timeout=30 --tries=1 "$1" -O "$2" 2>/dev/null; then return; else T3_FETCH_EXIT=$?; fi
else printf 'Remote host needs curl or wget to download %s.\\n' "$T3_ARCHIVE" >&2; exit 1
fi
printf 'Could not download T3 release artifact: %s\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$1" "$T3_ARCHIVE_VERSION" >&2
printf 'Could not download T3 release artifact: %s (downloader exit %s)\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$T3_RELEASE_DISPLAY_URL/\${2##*/}" "$T3_FETCH_EXIT" "$T3_ARCHIVE_VERSION" >&2
return 1
}
t3_fetch "$T3_RELEASE_BASE_URL/v$T3_ARCHIVE_VERSION/SHA256SUMS" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@
t3_fetch "$T3_RELEASE_BASE_URL/v$T3_ARCHIVE_VERSION/$T3_ARCHIVE" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@
t3_fetch "$T3_RELEASE_BASE_URL/SHA256SUMS$T3_RELEASE_QUERY" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@
t3_fetch "$T3_RELEASE_BASE_URL/$T3_ARCHIVE$T3_RELEASE_QUERY" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@
T3_EXPECTED="$(grep " \\*\\{0,1\\}$T3_ARCHIVE$" "$T3_STAGING/SHA256SUMS" | cut -d' ' -f1)"
if command -v sha256sum >/dev/null 2>&1; then
T3_ACTUAL="$(sha256sum "$T3_STAGING/$T3_ARCHIVE" | cut -d' ' -f1)"
Expand Down Expand Up @@ -867,15 +869,23 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string
if (archiveVersion !== "" && !EXACT_ARCHIVE_VERSION.test(archiveVersion)) {
throw new SshInvalidArchiveVersionError({ archiveVersion });
}
const releaseBaseUrl = cliReleaseDownloadBaseUrl("", input?.releaseBaseUrl ?? undefined).replace(
/\/v$/u,
"",
const releaseUrl = new URL(
cliReleaseDownloadBaseUrl(archiveVersion, input?.releaseBaseUrl ?? undefined),
);
const releaseQuery = releaseUrl.search;
releaseUrl.search = "";
releaseUrl.hash = "";
const releaseBaseUrl = releaseUrl.toString();
releaseUrl.username = "";
releaseUrl.password = "";
const releaseDisplayUrl = releaseUrl.toString();
return stripTrailingNewlines(
applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, {
T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath),
T3_ARCHIVE_VERSION: shellSingleQuote(archiveVersion),
T3_RELEASE_BASE_URL: shellSingleQuote(releaseBaseUrl),
T3_RELEASE_QUERY: shellSingleQuote(releaseQuery),
T3_RELEASE_DISPLAY_URL: shellSingleQuote(releaseDisplayUrl),
T3_ARCHIVE_LOCK_WAIT_SECONDS: String(REMOTE_ARCHIVE_LOCK_WAIT_SECONDS),
T3_INSTALLED_PROBE_SECONDS: String(REMOTE_INSTALLED_PROBE_SECONDS),
T3_ARCHIVE_DOWNLOAD_SECONDS: String(REMOTE_ARCHIVE_DOWNLOAD_SECONDS),
Expand Down
54 changes: 51 additions & 3 deletions scripts/build-npm-launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ exit 7
const requests: Array<string> = [];
const server = NodeHttp.createServer((request, response) => {
requests.push(request.url ?? "");
if (request.url === `/v${VERSION}/SHA256SUMS`) {
const url = new URL(request.url ?? "", "http://localhost");
const prefix = url.pathname.startsWith("/mirror/") ? "/mirror" : "";
if (prefix && url.search !== "?region=test") {
response.writeHead(400).end();
} else if (url.pathname === `${prefix}/v${VERSION}/SHA256SUMS`) {
response.end(`${checksum} ${archiveName}\n`);
} else if (request.url === `/v${VERSION}/${archiveName}`) {
} else if (url.pathname === `${prefix}/v${VERSION}/${archiveName}`) {
response.writeHead(200, { "Content-Length": archive.length }).end(archive);
} else {
response.writeHead(404).end();
Expand Down Expand Up @@ -147,13 +151,53 @@ it.layer(NodeServices.layer)("build-npm-launcher", (it) => {
assert.equal(unpack.exitCode, 0, unpack.stderr);
const packageDir = path.join(installed, "package");

const diagnosticHook = path.join(root, "diagnostic-hook.mjs");
for (const status of [404, 403, 0]) {
yield* fs.writeFileString(
diagnosticHook,
`globalThis.fetch = async (url) => {
if (url !== "https://mirror-user:mirror-pass@mirror.example/releases/v${VERSION}/SHA256SUMS?signature=test-secret#download") throw new Error("wrong request");
if (${status} === 0) throw new Error("transport signature=test-secret mirror-pass");
return new Response("", { status: ${status} });
};`,
);
const failed = yield* run(
process.execPath,
["--import", diagnosticHook, path.join(packageDir, "bin/t3.mjs")],
{
cwd: root,
env: {
PATH: (yield* HostProcess.Environment).PATH,
T3CODE_HOME: path.join(root, `failed-${status}`),
T3CODE_RELEASE_BASE_URL:
"https://mirror-user:mirror-pass@mirror.example/releases/?signature=test-secret#download",
},
},
);
assert.equal(failed.exitCode, 1);
assert.include(failed.stderr, `https://mirror.example/releases/v${VERSION}/SHA256SUMS`);
assert.include(
failed.stderr,
status === 404 ? "does not exist" : status === 403 ? "HTTP 403" : "failed to connect",
);
for (const secret of [
"mirror-user",
"mirror-pass",
"signature",
"test-secret",
"#download",
]) {
assert.notInclude(failed.stdout + failed.stderr, secret);
}
}

const release = yield* serveFakeRelease(root);
const home = path.join(root, "home");
const env = {
PATH: (yield* HostProcess.Environment).PATH,
T3CODE_HOME: home,
T3CODE_TEST_CALLS: path.join(root, "calls"),
T3CODE_RELEASE_BASE_URL: release.baseUrl,
T3CODE_RELEASE_BASE_URL: `${release.baseUrl}/mirror///?region=test#download`,
};
const launcher = yield* fs.realPath(path.join(packageDir, "bin/t3.mjs"));
const first = yield* run(process.execPath, [launcher, "serve", "a b"], {
Expand All @@ -177,6 +221,10 @@ it.layer(NodeServices.layer)("build-npm-launcher", (it) => {
[VERSION],
);

assert.deepStrictEqual(release.requests, [
`/mirror/v${VERSION}/SHA256SUMS?region=test`,
`/mirror/v${VERSION}/t3-${VERSION}-${HostProcess.Platform.defaultValue()}-${HostProcess.Architecture.defaultValue()}.tar.gz?region=test`,
]);
const requestsAfterFirst = release.requests.length;
const second = yield* run(process.execPath, [launcher, "--version"], { cwd: root, env });
assert.equal(second.exitCode, 0);
Expand Down
28 changes: 24 additions & 4 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ $ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$repo = "ScriptedAlchemy/t3code"
$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL.TrimEnd("/") } else { "https://github.com/$repo/releases/download" }
$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL } else { "https://github.com/$repo/releases/download" }
$t3Home = if ($env:T3CODE_HOME) { $env:T3CODE_HOME } else { Join-Path $HOME ".t3" }
$binDir = if ($env:T3CODE_INSTALL_BIN_DIR) { $env:T3CODE_INSTALL_BIN_DIR } else { Join-Path $HOME ".local\bin" }

function Release-Url([string] $file) {
$url = New-Object System.UriBuilder($baseUrl)
$url.Path = "$($url.Path.TrimEnd("/"))/v$version/$file"
$url.Fragment = ""
return $url.Uri.AbsoluteUri
}

function Fail([string] $message) {
Write-Error "t3 install: $message"
exit 1
Expand Down Expand Up @@ -63,6 +70,19 @@ function Draw-Download([long] $bytes, [long] $total) {
}
}
function Fetch([string] $uri, [string] $destination, [switch] $progress) {
try {
Fetch-Request $uri $destination -progress:$progress
} catch {
$status = $_.Exception.Response.StatusCode.value__
$display = New-Object System.UriBuilder($uri)
$display.UserName = $display.Password = $display.Query = $display.Fragment = ""
$detail = if ($status) { "returned HTTP $status" } else { "failed to download" }
$failure = New-Object System.Exception("GET $($display.Uri.AbsoluteUri) $detail")
$failure.Data["HttpStatus"] = $status
throw $failure
}
}
function Fetch-Request([string] $uri, [string] $destination, [switch] $progress) {
if (-not $progress -or -not $interactive) {
$ProgressPreference = if ($progress -and $terminal) { "Continue" } else { "SilentlyContinue" }
Invoke-WebRequest -Uri $uri -OutFile $destination -UseBasicParsing
Expand Down Expand Up @@ -173,15 +193,15 @@ if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) {
[Console]::Error.WriteLine(" ${muted}Installing$reset T3 Code $bold$version$reset`n")
Step "Downloading..."
try {
Fetch "$baseUrl/v$version/SHA256SUMS" (Join-Path $staging "SHA256SUMS")
Fetch (Release-Url "SHA256SUMS") (Join-Path $staging "SHA256SUMS")
} catch {
$status = $_.Exception.Response.StatusCode.value__
$status = $_.Exception.Data["HttpStatus"]
if ($status -eq 404) {
Fail "t3 $version has no release archive for win32-$arch; choose a published fork build from https://github.com/$repo/releases"
}
throw
}
Fetch "$baseUrl/v$version/$archive" (Join-Path $staging $archive) -progress
Fetch (Release-Url $archive) (Join-Path $staging $archive) -progress

Step "Verifying the download..."

Expand Down
28 changes: 22 additions & 6 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ set -eu

repo="ScriptedAlchemy/t3code"
base_url="${T3CODE_RELEASE_BASE_URL:-https://github.com/${repo}/releases/download}"
base_url="${base_url%%#*}"
base_path="${base_url%%\?*}"
base_query="${base_url#"$base_path"}"
while [ "${base_path%/}" != "$base_path" ]; do base_path="${base_path%/}"; done
t3_home="${T3CODE_HOME:-$HOME/.t3}"
case "$t3_home" in
/*) ;;
Expand Down Expand Up @@ -59,18 +63,30 @@ fi
step "Finding your release..."

# Exit 44 on a 404 so callers can tell "no such asset" from a network failure.
display_url() {
display_path="${1%%\?*}"
display_path="${display_path%%#*}"
case "$display_path" in
*://*@*)
display_rest="${display_path#*://}"
display_authority="${display_rest%%/*}"
printf '%s://%s%s' "${display_path%%://*}" "${display_authority##*@}" "${display_rest#"$display_authority"}"
;;
*) printf '%s' "$display_path" ;;
esac
}
fetch() {
if command -v curl >/dev/null 2>&1; then
status="$(curl -sSL -H "Accept: ${3:-*/*}" -w '%{http_code}' "$1" -o "$2")" || return 1
status="$(curl -sSL -H "Accept: ${3:-*/*}" -w '%{http_code}' "$1" -o "$2" 2>/dev/null)" || { printf 'GET %s failed to connect\n' "$(display_url "$1")" >&2; return 1; }
case "$status" in
2??) return 0 ;;
404) return 44 ;;
*) printf 'GET %s returned HTTP %s\n' "$1" "$status" >&2; return 1 ;;
*) printf 'GET %s returned HTTP %s\n' "$(display_url "$1")" "$status" >&2; return 1 ;;
esac
elif command -v wget >/dev/null 2>&1; then
wget -q --server-response --header="Accept: ${3:-*/*}" "$1" -O "$2" 2>"$2.headers" && rm -f "$2.headers" && return 0
if grep -q ' 404 ' "$2.headers" 2>/dev/null; then rm -f "$2.headers"; return 44; fi
cat "$2.headers" >&2; rm -f "$2.headers"; return 1
printf 'GET %s failed\n' "$(display_url "$1")" >&2; rm -f "$2.headers"; return 1
else
fail "curl or wget is required"
fi
Expand Down Expand Up @@ -123,7 +139,7 @@ download() {
download_pid=
if [ "$result" -ne 0 ]; then
printf '\n' >&2
if [ -f "$2.errors" ]; then cat "$2.errors" >&2; else cat "$2.headers" >&2; fi
printf 'GET %s failed (downloader exit %s)\n' "$(display_url "$1")" "$result" >&2
return "$result"
fi
size="$(mb "$(wc -c < "$2")")"
Expand Down Expand Up @@ -211,15 +227,15 @@ else
printf ' %sInstalling%s T3 Code %s%s%s\n\n' "$muted" "$reset" "$bold" "$version" "$reset" >&2
step "Downloading..."
fetch_status=0
fetch "${base_url}/v${version}/SHA256SUMS" "${staging}/SHA256SUMS" || fetch_status=$?
fetch "${base_path}/v${version}/SHA256SUMS${base_query}" "${staging}/SHA256SUMS" || fetch_status=$?
if [ "$fetch_status" -eq 44 ]; then
fail "t3 ${version} has no release archive for ${platform}-${arch}; choose a published fork build from https://github.com/${repo}/releases"
elif [ "$fetch_status" -ne 0 ]; then
fail "could not download the release checksums"
fi
expected="$(grep " \*\{0,1\}${archive}\$" "${staging}/SHA256SUMS" | cut -d' ' -f1)"
[ -n "$expected" ] || fail "t3 ${version} has no release archive for ${platform}-${arch}"
download "${base_url}/v${version}/${archive}" "${staging}/${archive}"
download "${base_path}/v${version}/${archive}${base_query}" "${staging}/${archive}"

step "Verifying the download..."
actual="$(checksum "${staging}/${archive}")"
Expand Down
Loading
Loading