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
5 changes: 5 additions & 0 deletions packages/cli/src/verbs/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export async function runFetch(
makeServer: VerbServerFactory = defaultServerFactory,
): Promise<number> {
const domain = assertUrlOnProfile(cmd.url, profile);
// Check the relay tab the same way and at the same time as the request URL.
// The server guards it too, but that guard only fires after the bridge is
// up, turning a typo into exit 2 ("bridge error") when it is plainly a usage
// error — and making the user wait on a connection to be told so (#209).
if (cmd.viaTab !== undefined) assertUrlOnProfile(cmd.viaTab, profile);
const server = makeServer({
...serverOptsFor(cmd.profile, profile, VERSION),
onPairCode: pairCodePrinter(io),
Expand Down
36 changes: 35 additions & 1 deletion packages/cli/tests/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { runFetch, type VerbServer } from '../src/verbs/fetch.js';
import { emptyProfile } from '../src/profiles.js';
import { EXIT, type Io } from '../src/output.js';
import { EXIT, UsageError, type Io } from '../src/output.js';
import { FetchproxySessionNotReadyError } from '@fetchproxy/server';

function memIo(): Io & { outs: string[]; errs: string[] } {
Expand Down Expand Up @@ -131,3 +131,37 @@ describe('runFetch — --via-tab', () => {
);
});
});

describe('runFetch — --via-tab is validated like the request URL', () => {
// The request URL is checked against the profile before connecting, so a
// typo is exit 1 with guidance. --via-tab skipped that check, so the same
// class of mistake travelled to the bridge and came back as exit 2 — a
// "bridge error" for what is purely a usage error (#209).
it('rejects a malformed relay tab as a usage error', async () => {
const server = stubServer();
await expect(
runFetch({ ...CMD, viaTab: 'not a url' }, PROFILE, memIo(), () => server),
).rejects.toThrow(UsageError);
// Must fail before connecting — no bridge round-trip for a typo.
expect(server.listen).not.toHaveBeenCalled();
});

it('rejects an off-domain relay tab as a usage error', async () => {
const server = stubServer();
await expect(
runFetch({ ...CMD, viaTab: 'https://evil.example/' }, PROFILE, memIo(), () => server),
).rejects.toThrow(UsageError);
expect(server.listen).not.toHaveBeenCalled();
});

it('accepts a relay tab on a declared domain', async () => {
const server = stubServer();
const code = await runFetch(
{ ...CMD, viaTab: 'https://www.tripadvisor.com/' },
PROFILE,
memIo(),
() => server,
);
expect(code).toBe(EXIT.OK);
});
});
19 changes: 14 additions & 5 deletions packages/server/src/ws-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,20 @@ export interface RequestOpts {
* By default the relay tab is `https://{host-of-the-request}/`, which is
* right for app hosts — routing `photos.x.com` through a `www.x.com` tab
* would be wrong. But it assumes every host CAN have a tab, and API hosts
* cannot: `api.example.com` typically serves no HTML app, so a tab opened
* there has no content script and the request is unroutable however hard the
* user tries. Meanwhile the signed-in `www.example.com` tab can issue that
* cross-origin fetch perfectly well — which is exactly what the site's own
* web app does.
* cannot.
*
* The mechanism is worth knowing, because the extension's own advice for
* this failure ("refresh the page to inject the content script") cannot
* work. An API host typically 404s at `/`, so Chrome renders its OWN
* document at `chrome-error://chromewebdata/` — and Chrome never injects
* content scripts into `chrome-error://` pages, `<all_urls>` match or not.
* `chrome.tabs.query` still reports the tab's URL as the requested https
* one, which is why the failure reads "1 URL match, none responded": the URL
* matches, the document behind it cannot host a relay, and no amount of
* reloading changes that.
*
* Meanwhile the signed-in `www.example.com` tab can issue that cross-origin
* fetch perfectly well — which is exactly what the site's own web app does.
*
* Naming the relay explicitly keeps the safe default intact while unblocking
* that case. The value is matched against open tabs by prefix, so
Expand Down