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

refactor(cli): show more info and add port in-use detection #6495

Merged
merged 3 commits into from
Aug 22, 2024
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
125 changes: 79 additions & 46 deletions packages/cli/src/commands/tunnel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,18 @@ const tunnel: CommandModule<unknown, TunnelCommandArgs> = {
yargs
.options({
'experience-uri': {
alias: ['x'],
alias: ['uri'],
describe: 'The URI of your custom sign-in experience page.',
type: 'string',
},
'experience-path': {
alias: ['xp'],
alias: ['path'],
describe: 'The local folder path of your custom sign-in experience assets.',
type: 'string',
},
endpoint: {
alias: 'ep',
describe:
'Logto endpoint URI, which can be found in Logto Console. E.g.: https://<tenant-id>.logto.app/',
'Logto endpoint URI that points to your Logto Cloud instance. E.g.: https://<tenant-id>.logto.app/',
type: 'string',
},
port: {
Expand All @@ -52,55 +51,89 @@ const tunnel: CommandModule<unknown, TunnelCommandArgs> = {
default: false,
},
})
.global('e'),
.global('e')
.hide('db'),
handler: async ({ 'experience-uri': url, 'experience-path': path, endpoint, port, verbose }) => {
checkExperienceInput(url, path);

if (!endpoint || !isValidUrl(endpoint)) {
consoleLog.fatal('A valid Logto endpoint URI must be provided.');
}
const logtoEndpointUrl = new URL(endpoint);
const tunnelServiceUrl = new URL(`http://localhost:${port}`);

const proxyLogtoRequest = createProxy(
logtoEndpointUrl.href,
async (proxyResponse, request, response) =>
createLogtoResponseHandler({
proxyResponse,
request,
response,
logtoEndpointUrl,
tunnelServiceUrl,
verbose,
})
);
const proxyExperienceServerRequest = conditional(url && createProxy(url));
const proxyExperienceStaticFileRequest = conditional(path && createStaticFileProxy(path));

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

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

if (proxyExperienceServerRequest) {
void proxyExperienceServerRequest(request, response);
return;
}

if (proxyExperienceStaticFileRequest) {
void proxyExperienceStaticFileRequest(request, response);
}
});

server.listen(port, () => {
consoleLog.info(`Logto tunnel is running on ${chalk.blue(tunnelServiceUrl.href)}`);
});

const startServer = (port: number) => {
const tunnelServiceUrl = new URL(`http://localhost:${port}`);

const proxyLogtoRequest = createProxy(
logtoEndpointUrl.href,
async (proxyResponse, request, response) =>
createLogtoResponseHandler({
proxyResponse,
request,
response,
logtoEndpointUrl,
tunnelServiceUrl,
verbose,
})
);
const proxyExperienceServerRequest = conditional(url && createProxy(url));
const proxyExperienceStaticFileRequest = conditional(path && createStaticFileProxy(path));

const server = http.createServer((request, response) => {
consoleLog.info(`[${chalk.green(request.method)}] ${request.url}`);

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

if (proxyExperienceServerRequest) {
void proxyExperienceServerRequest(request, response);
return;
}

if (proxyExperienceStaticFileRequest) {
void proxyExperienceStaticFileRequest(request, response);
}
});

server.listen(port, () => {
const serviceUrl = new URL(`http://localhost:${port}`);
consoleLog.info(
`🎉 Logto tunnel service is running!
${chalk.green('➜')} Your custom sign-in UI is hosted on: ${chalk.blue(serviceUrl.href)}

${chalk.green('➜')} Don't forget to update Logto endpoint URI in your app:

${chalk.gray('From:')} ${chalk.bold(endpoint)}
${chalk.gray('To:')} ${chalk.bold(serviceUrl.href)}

${chalk.green(
'➜'
)} If you are using social sign-in, make sure the social redirect URI is also set to:

${chalk.bold(`${serviceUrl.href}callback/<connector-id>`)}

${chalk.green('➜')} ${chalk.gray(`Press ${chalk.white('Ctrl+C')} to stop the tunnel service.`)}
${chalk.green('➜')} ${chalk.gray(
`Use ${chalk.white('-v')} or ${chalk.white('--verbose')} to print verbose output.`
)}
`
);
});

server.on('error', (error: Error) => {
if ('code' in error && error.code === 'EADDRINUSE') {
consoleLog.error(`Port ${port} is already in use, trying another one...`);
startServer(port + 1);
charIeszhao marked this conversation as resolved.
Show resolved Hide resolved
return;
}
consoleLog.fatal(`Tunnel server failed to start. ${error.message}`);
});
};

startServer(port);
},
};

Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/commands/tunnel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const createProxy = (targetUrl: string, onProxyResponse?: OnProxyEvent['p
on: {
proxyRes: onProxyResponse,
error: (error) => {
consoleLog.error(chalk.red(error));
consoleLog.error(chalk.red(error.message));
},
},
}
Expand Down Expand Up @@ -54,8 +54,9 @@ export const createStaticFileProxy =
response.setHeader('content-type', getMimeType(request.url));
response.writeHead(200);
response.end(content);
} catch (error: unknown) {
consoleLog.error(chalk.red(error));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
consoleLog.error(chalk.red(errorMessage));
response.setHeader('content-type', getMimeType(request.url));
response.writeHead(existsSync(request.url) ? 500 : 404);
response.end();
Expand Down Expand Up @@ -92,7 +93,7 @@ export const createLogtoResponseHandler = async ({
void responseInterceptor(async (responseBuffer, proxyResponse) => {
const responseBody = responseBuffer.toString();
if (verbose) {
consoleLog.info(`Response received: ${chalk.green(responseBody)}`);
consoleLog.info(`[${proxyResponse.statusCode}] ${chalk.cyan(responseBody)}`);
}

if (proxyResponse.headers['content-type']?.includes('text/html')) {
Expand Down
Loading