Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add proxy for @line/liff-inspector #8

Merged
merged 2 commits into from
Feb 28, 2025
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
24 changes: 20 additions & 4 deletions src/serve/commands/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,27 @@ describe("serveCommand", () => {
port: "8000",
proxyType: "local-proxy",
localProxyPort: "9001",
localProxyInspectorPort: "9223",
ngrokCommand: "ngrok",
inspect: true,
};

const cwd = process.cwd();
const proxy = new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
const keyPath = path.resolve(cwd, "localhost-key.pem");
const certPath = path.resolve(cwd, "localhost.pem");

const liffAppProxy = new LocalProxy({
keyPath,
certPath,
port: "9001",
});

const liffInspectorProxy = new LocalProxy({
keyPath,
certPath,
port: "9223",
});

const command = installServeCommands(program);
await command.parseAsync([
"_",
Expand All @@ -46,13 +56,19 @@ describe("serveCommand", () => {
options.port,
"--local-proxy-port",
options.localProxyPort,
"--local-proxy-inspector-port",
options.localProxyInspectorPort,
"--inspect",
"--proxy-type",
options.proxyType,
"--ngrok-command",
options.ngrokCommand,
]);

expect(serveAction).toHaveBeenCalledWith(options, proxy);
expect(serveAction).toHaveBeenCalledWith(
options,
liffAppProxy,
liffInspectorProxy,
);
});
});
9 changes: 7 additions & 2 deletions src/serve/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ export const installServeCommands = (program: Command) => {
"The port number of the application proxy server to listen on when running the CLI.",
"9000",
)
.option(
"--local-proxy-inspector-port <localProxyInspectorPort>",
"The port number of the inspector proxy server to listen on when running the CLI.",
"9223",
)
.option(
"--ngrok-command <ngrokCommand>",
"The command to run ngrok.",
"ngrok",
)
.action(async (options) => {
const proxy = resolveProxy(options);
await serveAction(options, proxy);
const { liffAppProxy, liffInspectorProxy } = resolveProxy(options);
await serveAction(options, liffAppProxy, liffInspectorProxy);
});
return serve;
};
41 changes: 40 additions & 1 deletion src/serve/proxy/local-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ export class LocalProxy implements ProxyInterface {
}

async connect(targetUrl: URL): Promise<URL> {
if (targetUrl.protocol === "http:") {
return this.connectHttp(targetUrl);
}

if (targetUrl.protocol === "ws:") {
return this.connectWs(targetUrl);
}

throw new Error(`Unsupported protocol: ${targetUrl.protocol}`);
}

private async connectHttp(targetUrl: URL): Promise<URL> {
const proxy = httpProxy.createProxyServer({});

const proxyUrl = new URL("https://localhost");
Expand Down Expand Up @@ -55,8 +67,33 @@ export class LocalProxy implements ProxyInterface {
});
}

async cleanup(): Promise<void> {
private async connectWs(targetUrl: URL): Promise<URL> {
const proxy = httpProxy.createProxyServer({ target: targetUrl.toString() });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const proxyUrl = new URL("wss://localhost");
proxyUrl.port = this.port;

return new Promise((resolve, reject) => {
this.server = https
.createServer({ key: this.key, cert: this.cert }, (req, res) => {
proxy.web(req, res);
})
.listen(this.port)
.on("listening", () => {
resolve(proxyUrl);
})
.on("upgrade", function (req, socket, head) {
req.headers["x-forwarded-proto"] = "https";
proxy.ws(req, socket, head);
})
.on("error", (e) => {
reject(e);
});
});
}

async cleanup(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.server) {
resolve();
return;
Expand All @@ -65,6 +102,8 @@ export class LocalProxy implements ProxyInterface {
if (e) reject(e);
else resolve();
});
}).finally(() => {
this.server = undefined;
});
}
}
13 changes: 11 additions & 2 deletions src/serve/resolveProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,35 @@ describe("resolveProxy", () => {
const options = {
proxyType: "local-proxy",
localProxyPort: "9000",
localProxyInspectorPort: "9223",
ngrokCommand: "ngrok",
};

expect(resolveProxy(options)).toBeInstanceOf(LocalProxy);
expect(resolveProxy(options)).toEqual({
liffAppProxy: expect.any(LocalProxy),
liffInspectorProxy: expect.any(LocalProxy),
});
});

it("should return NgrokV1Proxy when proxyType is ngrok-v1", () => {
const options = {
proxyType: "ngrok-v1",
localProxyPort: "9000",
localProxyInspectorPort: "9223",
ngrokCommand: "ngrok",
};

expect(resolveProxy(options)).toBeInstanceOf(NgrokV1Proxy);
expect(resolveProxy(options)).toEqual({
liffAppProxy: expect.any(NgrokV1Proxy),
liffInspectorProxy: expect.any(NgrokV1Proxy),
});
});

it("should throw an error when an unknown proxyType is specified", () => {
const options = {
proxyType: "unknown",
localProxyPort: "9000",
localProxyInspectorPort: "9223",
ngrokCommand: "ngrok",
};

Expand Down
35 changes: 26 additions & 9 deletions src/serve/resolveProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,40 @@ import { ProxyInterface } from "./proxy/proxy-interface.js";
type Options = {
proxyType: string;
localProxyPort: string;
localProxyInspectorPort: string;
ngrokCommand: string;
};

export const resolveProxy = (options: Options): ProxyInterface => {
export const resolveProxy = (
options: Options,
): { liffAppProxy: ProxyInterface; liffInspectorProxy: ProxyInterface } => {
if (options.proxyType === "local-proxy") {
const cwd = process.cwd();
return new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
port: options.localProxyPort,
});
const keyPath = path.resolve(cwd, "localhost-key.pem");
const certPath = path.resolve(cwd, "localhost.pem");
return {
liffAppProxy: new LocalProxy({
keyPath,
certPath,
port: options.localProxyPort,
}),
liffInspectorProxy: new LocalProxy({
keyPath,
certPath,
port: options.localProxyInspectorPort,
}),
};
}

if (options.proxyType === "ngrok-v1") {
return new NgrokV1Proxy({
ngrokCommand: options.ngrokCommand,
});
return {
liffAppProxy: new NgrokV1Proxy({
ngrokCommand: options.ngrokCommand,
}),
liffInspectorProxy: new NgrokV1Proxy({
ngrokCommand: options.ngrokCommand,
}),
};
}

throw new Error(`Unknown proxy type: ${options.proxyType}`);
Expand Down
92 changes: 34 additions & 58 deletions src/serve/serveAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ vi.mock("../api/liff.js", () => {
vi.mock("../channel/resolveChannel.js");
vi.mock("node:child_process");

const cwd = process.cwd();
const keyPath = path.resolve(cwd, "localhost-key.pem");
const certPath = path.resolve(cwd, "localhost.pem");

const liffAppProxy = new LocalProxy({
keyPath,
certPath,
port: "9000",
});

const liffInspectorProxy = new LocalProxy({
keyPath,
certPath,
port: "9223",
});

describe("serveAction", () => {
let mockUpdateApp: LiffApiClient["updateApp"];

Expand All @@ -33,14 +49,16 @@ describe("serveAction", () => {
mockUpdateApp = liffApiClientInstance.updateApp;
});

afterEach(() => {
afterEach(async () => {
await liffAppProxy.cleanup();
await liffInspectorProxy.cleanup();
vi.restoreAllMocks();
});

it("should update a LIFF app successfully when the --url option is specified", async () => {
const options = {
liffId: "123-xxx",
url: "https://example.com",
url: "http://example.com",
localProxyPort: "9000",
};

Expand All @@ -52,16 +70,7 @@ describe("serveAction", () => {
});
vi.mocked(mockUpdateApp).mockResolvedValueOnce();

const cwd = process.cwd();
const proxy = new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
port: "9000",
});

await serveAction(options, proxy);

await proxy.cleanup();
await serveAction(options, liffAppProxy, liffInspectorProxy);

expect(mockUpdateApp).toHaveBeenCalledWith(options.liffId, {
view: { url: "https://localhost:9000/" },
Expand All @@ -74,7 +83,7 @@ describe("serveAction", () => {
it("should update a LIFF app successfully with liff-inspector when the --inspect option is specified", async () => {
const options = {
liffId: "123-xxx",
url: "https://example.com?hoge=fuga",
url: "http://example.com?hoge=fuga",
inspect: true,
localProxyPort: "9000",
};
Expand All @@ -87,67 +96,41 @@ describe("serveAction", () => {
});
vi.mocked(mockUpdateApp).mockResolvedValueOnce();

const cwd = process.cwd();
const proxy = new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
port: "9000",
});

await serveAction(options, proxy);

await proxy.cleanup();
await serveAction(options, liffAppProxy, liffInspectorProxy);

expect(spawn).toHaveBeenCalledWith(
"npx",
[
"@line/liff-inspector",
"--key=./localhost-key.pem",
"--cert=./localhost.pem",
],
{
shell: true,
stdio: "inherit",
},
);
expect(spawn).toHaveBeenCalledWith("npx", ["@line/liff-inspector"], {
shell: true,
stdio: "inherit",
});

expect(mockUpdateApp).toHaveBeenCalledWith(options.liffId, {
view: {
url: "https://localhost:9000/?hoge=fuga&li.origin=wss%3A%2F%2Flocalhost%3A9222",
url: "https://localhost:9000/?hoge=fuga&li.origin=wss%3A%2F%2Flocalhost%3A9223%2F",
},
});
});

it("should handle channel not found", async () => {
const options = {
liffId: "123-xxx",
url: "https://example.com",
url: "http://example.com",
localProxyPort: "9000",
};

vi.mocked(resolveChannel).mockResolvedValueOnce(undefined);

const cwd = process.cwd();
const proxy = new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
port: "9000",
});

await expect(serveAction(options, proxy)).rejects
await expect(serveAction(options, liffAppProxy, liffInspectorProxy)).rejects
.toThrow(`Access token not found.
Please set the current channel first.
`);

await proxy.cleanup();

expect(mockUpdateApp).not.toHaveBeenCalled();
});

it("Should not update a LIFF app when --url, --host, and --port are specified", async () => {
const options = {
liffId: "123-xxx",
url: "https://example.com",
url: "http://example.com",
host: "localhost",
port: "8080",
localProxyPort: "9000",
Expand All @@ -161,19 +144,12 @@ describe("serveAction", () => {
});
vi.mocked(mockUpdateApp).mockResolvedValueOnce();

const cwd = process.cwd();
const proxy = new LocalProxy({
keyPath: path.resolve(cwd, "localhost-key.pem"),
certPath: path.resolve(cwd, "localhost.pem"),
port: "9000",
});

await expect(serveAction(options, proxy)).rejects.toThrow(
await expect(
serveAction(options, liffAppProxy, liffInspectorProxy),
).rejects.toThrow(
"When --url is specified, --host, and --port cannot be specified.",
);

await proxy.cleanup();

expect(mockUpdateApp).not.toHaveBeenCalled();
});
});
Loading