Skip to content

Commit

Permalink
refactor(cli): polish code
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Jul 31, 2024
1 parent 597d982 commit 244dbb3
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 52 deletions.
70 changes: 33 additions & 37 deletions packages/cli/src/commands/proxy/index.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,77 @@
import * as http from 'node:http';

import { isValidUrl } from '@logto/core-kit';
import chalk from 'chalk';
import type { CommandModule } from 'yargs';

import { consoleLog } from '../../utils.js';

import { createOidcResponseHandler, createProxy, isLogtoRequestPath } from './utils.js';
import { type ProxyCommandArgs } from './types.js';
import { createLogtoResponseHandler, createProxy, isLogtoRequestPath } from './utils.js';

const proxy: CommandModule<
unknown,
{
url?: string;
tenant?: string;
port: number;
endpoint?: string;
}
> = {
const proxy: CommandModule<unknown, ProxyCommandArgs> = {
command: ['proxy'],
describe: 'Command for Logto proxy',
builder: (yargs) =>
yargs
.options({
url: {
alias: ['u', 'sign-in-experience-url'],
describe: 'The URL of your custom sign-in experience page',
'experience-uri': {
alias: ['x'],
describe: 'The URI of your custom sign-in experience page.',
type: 'string',
},
tenant: {
alias: ['t', 'tenant-id'],
describe: 'The ID of your Logto Cloud tenant',
'tenant-id': {
alias: ['t'],
describe:
'Your Logto Cloud tenant ID. WHen provided, endpoint URI will be set to `https://<tenant-id>.logto.app` by default.',
type: 'string',
},
endpoint: {
alias: 'ep',
describe:
'Specify the full Logto endpoint URI, which takes precedence over tenant ID when provided.',
type: 'string',
},
port: {
alias: 'p',
describe: 'The port number where the proxy server will be running on',
describe: 'The port number where the proxy server will be running on. Defaults to 9000.',
type: 'number',
default: 9000,
},
endpoint: {
alias: 'logto-endpoint',
describe:
'[Internal] Specify Logto Cloud endpoint URL. E.g. `https://[tenant-id].app.logto.dev` for dev environment. Tenant ID will be omitted when this argument is provided.',
type: 'string',
hidden: true,
},
})
.global('e'),
handler: async ({ url: signInExpUrl, tenant: tenantId, port, endpoint }) => {
if (!signInExpUrl) {
consoleLog.fatal('No sign-in experience URL provided.');
handler: async ({ 'experience-uri': expUri, 'tenant-id': tenantId, endpoint, port }) => {
if (!expUri || !isValidUrl(expUri)) {
consoleLog.fatal(
'A valid sign-in experience URI must be provided. E.g.: http://localhost:4000'
);
}
if (!tenantId && !endpoint) {
consoleLog.fatal('No tenant ID provided.');
if (!tenantId && (!endpoint || !isValidUrl(endpoint))) {
consoleLog.fatal('Either tenant ID or a valid Logto endpoint URI must be provided.');
}

const logtoCloudEndpointUrl = new URL(endpoint ?? `https://${tenantId}.logto.app}`);
const logtoEndpointUrl = new URL(endpoint ?? `https://${tenantId}.logto.app}`);
const proxyUrl = new URL(`http://localhost:${port}`);

const proxyOidcRequest = createProxy(
logtoCloudEndpointUrl.href,
const proxyLogtoRequest = createProxy(
logtoEndpointUrl.href,
async (proxyResponse, request, response) =>
createOidcResponseHandler({
createLogtoResponseHandler({
proxyResponse,
request,
response,
logtoCloudEndpointUrl,
logtoEndpointUrl,
proxyUrl,
})
);
const proxySignInExpRequest = createProxy(signInExpUrl);
const proxySignInExpRequest = createProxy(expUri);

const server = http.createServer((request, response) => {
consoleLog.info(`Incoming request: ${chalk.blue(request.url)}`);

// Proxy the request to Logto Cloud endpoint
// Proxy the requests to Logto endpoint
if (isLogtoRequestPath(request.url)) {
void proxyOidcRequest(request, response);
void proxyLogtoRequest(request, response);
return;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/commands/proxy/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import type * as http from 'node:http';

export type ProxyCommandArgs = {
'experience-uri'?: string;
'tenant-id'?: string;
endpoint?: string;
port: number;
};

export type ProxyResponseHandler = {
proxyResponse: http.IncomingMessage;
request: http.IncomingMessage;
response: http.ServerResponse;
logtoCloudEndpointUrl: URL;
logtoEndpointUrl: URL;
proxyUrl: URL;
};
45 changes: 31 additions & 14 deletions packages/cli/src/commands/proxy/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { conditional } from '@silverhand/essentials';
import { conditional, trySafe } from '@silverhand/essentials';
import chalk from 'chalk';
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
import { type OnProxyEvent } from 'http-proxy-middleware/dist/types.js';
Expand All @@ -24,35 +24,52 @@ export const createProxy = (targetUrl: string, onProxyResponse?: OnProxyEvent['p
};

/**
* Intercept the response from Logto OIDC server and replace Logto Cloud endpoint URLs in the response
* with the proxy URL. The string replace happens in the following cases:
* Intercept the response from Logto endpoint and replace Logto endpoint URLs in the response with the
* proxy URL. The string replace happens in the following cases:
* - The response is a redirect response, and the response header contains `location` property.
* - The response contains JSON data that consists of properties such as `**_endpoint` and `redirectTo`.
* - The response is HTML content (logoutsource) that contains a form action URL.
*
* However, the `issuer` and `jwks_uri` properties in the `/oidc/.well-known` response will not be replaced,
* even they also contain the Logto Cloud endpoint URL.
* Note: the `issuer` and `jwks_uri` properties in the `/oidc/.well-known` response should not be replaced,
* even they also contain the Logto endpoint URL.
*/
export const createOidcResponseHandler = async ({
export const createLogtoResponseHandler = async ({
proxyResponse,
request,
response,
logtoCloudEndpointUrl,
logtoEndpointUrl,
proxyUrl,
}: ProxyResponseHandler) => {
const { location } = proxyResponse.headers;
if (location) {
// eslint-disable-next-line @silverhand/fp/no-mutation
proxyResponse.headers.location = location.replaceAll(logtoCloudEndpointUrl.href, proxyUrl.href);
proxyResponse.headers.location = location.replace(logtoEndpointUrl.href, proxyUrl.href);
}

void responseInterceptor(async (responseBuffer) => {
void responseInterceptor(async (responseBuffer, proxyResponse) => {
const responseBody = responseBuffer.toString();
consoleLog.info(`Response received: ${chalk.green(responseBody)}`);
return responseBody
.replaceAll(`_endpoint":"${logtoCloudEndpointUrl.href}`, `_endpoint":"${proxyUrl.href}`)
.replace(`redirectTo":"${logtoCloudEndpointUrl.href}`, `redirectTo":"${proxyUrl.href}`)
.replace(`action="${logtoCloudEndpointUrl.href}`, `action="${proxyUrl.href}`);

if (proxyResponse.headers['content-type']?.includes('text/html')) {
return responseBody.replace(
`action="${logtoEndpointUrl.href}oidc/session/end/confirm"`,
`action="${proxyUrl.href}oidc/session/end/confirm"`
);
}

// eslint-disable-next-line no-restricted-syntax
const jsonData = trySafe(() => JSON.parse(responseBody) as Record<string, unknown>);

if (jsonData) {
for (const [key, value] of Object.entries(jsonData)) {
if ((key === 'redirectTo' || key.endsWith('_endpoint')) && typeof value === 'string') {
// eslint-disable-next-line @silverhand/fp/no-mutation
jsonData[key] = value.replace(logtoEndpointUrl.href, proxyUrl.href);
}
}
return JSON.stringify(jsonData);
}

return responseBody;
})(proxyResponse, request, response);
};

Expand Down

0 comments on commit 244dbb3

Please sign in to comment.