diff --git a/packages/git/src/electron-node/askpass/askpass-main.ts b/packages/git/src/electron-node/askpass/askpass-main.ts index e31d6275e8e94..3e7a9b9c9dbce 100644 --- a/packages/git/src/electron-node/askpass/askpass-main.ts +++ b/packages/git/src/electron-node/askpass/askpass-main.ts @@ -20,8 +20,8 @@ *--------------------------------------------------------------------------------------------*/ // Based on: https://github.com/Microsoft/vscode/blob/dd3e2d94f81139f9d18ba15a24c16c6061880b93/extensions/git/src/askpass-main.ts. +import * as url from 'url'; import * as http from 'http'; -import * as fs from 'fs'; // tslint:disable-next-line:no-any function fatal(err: any): void { @@ -30,36 +30,40 @@ function fatal(err: any): void { process.exit(1); } +// 1. Node.js executable path. In this particular case it is Electron. +// 2. The location of the corresponding JS file of the current (`__filename`) file. +// 3. `Username`/`Password`. +// 4. `for`. +// 5. The host. For example: `https://github.com`. +const expectedArgvCount = 5; + function main(argv: string[]): void { - if (argv.length !== 5) { - return fatal('Wrong number of arguments'); - } - if (!process.env['THEIA_GIT_ASKPASS_HANDLE']) { - return fatal('Missing handle'); + if (argv.length !== expectedArgvCount) { + return fatal(`Wrong number of arguments. Expected ${expectedArgvCount}. Got ${argv.length} instead.`); } - if (!process.env['THEIA_GIT_ASKPASS_PIPE']) { - return fatal('Missing pipe'); + if (!process.env['THEIA_GIT_ASKPASS_HANDLE']) { + return fatal("Missing 'THEIA_GIT_ASKPASS_HANDLE' handle."); } - if (process.env['THEIA_GIT_COMMAND'] === 'fetch') { - return fatal('Skip fetch commands'); - } + const handle = process.env['THEIA_GIT_ASKPASS_HANDLE'] as string; + const { host, hostname, port, protocol } = url.parse(handle); + const gitRequest = argv[2]; + const gitHost = argv[4].substring(1, argv[4].length - 2); - const output = process.env['THEIA_GIT_ASKPASS_PIPE'] as string; - const socketPath = process.env['THEIA_GIT_ASKPASS_HANDLE'] as string; - const request = argv[2]; - const host = argv[4].substring(1, argv[4].length - 2); const opts: http.RequestOptions = { - socketPath, + host, + hostname, + port, + protocol, path: '/', method: 'POST' }; const req = http.request(opts, res => { if (res.statusCode !== 200) { - return fatal(`Bad status code: ${res.statusCode}`); + return fatal(`Bad status code: ${res.statusCode}.`); } const chunks: string[] = []; @@ -70,17 +74,17 @@ function main(argv: string[]): void { try { const result = JSON.parse(raw); - fs.writeFileSync(output, result + '\n'); + process.stdout.write(result); } catch (err) { - return fatal('Error parsing response'); + return fatal('Error parsing response.'); } setTimeout(() => process.exit(0), 0); }); }); - req.on('error', () => fatal('Error in request')); - req.write(JSON.stringify({ request, host })); + req.on('error', err => fatal(err)); + req.write(JSON.stringify({ gitRequest, gitHost })); req.end(); } diff --git a/packages/git/src/electron-node/askpass/askpass.sh b/packages/git/src/electron-node/askpass/askpass.sh index 18ed0b875ef02..14e72e68b8624 100755 --- a/packages/git/src/electron-node/askpass/askpass.sh +++ b/packages/git/src/electron-node/askpass/askpass.sh @@ -15,7 +15,4 @@ ################################################################################/ # Based on: https://github.com/Microsoft/vscode/blob/77f0e95307675c3936c05d641f72b8b32dc8e274/extensions/git/src/askpass.sh -THEIA_GIT_ASKPASS_PIPE=`mktemp` -THEIA_GIT_ASKPASS_PIPE="$THEIA_GIT_ASKPASS_PIPE" "$THEIA_GIT_ASKPASS_NODE" "$THEIA_GIT_ASKPASS_MAIN" $* -cat $THEIA_GIT_ASKPASS_PIPE -rm $THEIA_GIT_ASKPASS_PIPE +"$THEIA_GIT_ASKPASS_NODE" "$THEIA_GIT_ASKPASS_MAIN" $* diff --git a/packages/git/src/electron-node/askpass/askpass.ts b/packages/git/src/electron-node/askpass/askpass.ts index 558a67412633e..896d2775ece33 100644 --- a/packages/git/src/electron-node/askpass/askpass.ts +++ b/packages/git/src/electron-node/askpass/askpass.ts @@ -21,24 +21,44 @@ // Based on: https://github.com/Microsoft/vscode/blob/dd3e2d94f81139f9d18ba15a24c16c6061880b93/extensions/git/src/askpass.ts import { injectable, postConstruct, inject } from 'inversify'; -import * as os from 'os'; -import * as fs from 'fs'; import * as path from 'path'; import * as http from 'http'; -import * as crypto from 'crypto'; import { ILogger } from '@theia/core/lib/common/logger'; -import { isWindows } from '@theia/core/lib/common/os'; import { Disposable } from '@theia/core/lib/common/disposable'; import { MaybePromise } from '@theia/core/lib/common/types'; -import { DugiteGitPromptServer } from '../../node/dugite-git-prompt'; import { GitPrompt } from '../../common/git-prompt'; +import { DugiteGitPromptServer } from '../../node/dugite-git-prompt'; +/** + * Environment for the Git askpass helper. + */ export interface AskpassEnvironment { + + /** + * The path to the external script to run by Git when authentication is required. + */ readonly GIT_ASKPASS: string; + + /** + * Starts the process as a normal Node.js process. User `"1"` if you want to enable it. + */ readonly ELECTRON_RUN_AS_NODE?: string; + + /** + * The path to the Node.js executable that will run the external `ASKPASS` script. + */ readonly THEIA_GIT_ASKPASS_NODE?: string; + + /** + * The JS file to run. + */ readonly THEIA_GIT_ASKPASS_MAIN?: string; + + /** + * The Git askpass handle path. In our case, this is the address of the HTTP server listening on the `Username` and `Password` requests. + */ readonly THEIA_GIT_ASKPASS_HANDLE?: string; + } @injectable() @@ -51,47 +71,31 @@ export class Askpass implements Disposable { protected readonly promptServer: DugiteGitPromptServer; protected server: http.Server; - protected ipcHandlePathPromise: Promise; - protected ipcHandlePath: string | undefined; + protected serverAddress: Readonly<{ port: number, family: string, address: string; }> | undefined; protected enabled = true; @postConstruct() protected init(): void { this.server = http.createServer((req, res) => this.onRequest(req, res)); - this.ipcHandlePathPromise = this.setup().catch(err => { - this.logger.error(err); - return ''; - }); + this.serverAddress = this.setup(); + if (this.serverAddress) { + const { address, port } = this.serverAddress; + this.logger.info(`Git askpass helper is listening on ${address}:${port}.`); + } else { + this.logger.warn("Couldn't start the HTTP server for the Git askpass helper."); + } } - protected async setup(): Promise { - const buffer = await this.randomBytes(20); - const nonce = buffer.toString('hex'); - const ipcHandlePath = this.getIPCHandlePath(nonce); - this.ipcHandlePath = ipcHandlePath; - + protected setup(): Readonly<{ port: number, family: string, address: string; }> | undefined { try { - this.server.listen(ipcHandlePath); + this.server.listen(0); this.server.on('error', err => this.logger.error(err)); + return this.server.address(); } catch (err) { - this.logger.error('Could not launch git askpass helper.', err); + this.logger.error('Could not launch Git askpass helper.', err); this.enabled = false; + return undefined; } - - return ipcHandlePath; - } - - protected getIPCHandlePath(nonce: string): string { - const fileName = `theia-git-askpass-${nonce}-sock`; - if (isWindows) { - return `\\\\.\\pipe\\${fileName}`; - } - - if (process.env['XDG_RUNTIME_DIR']) { - return path.join(process.env['XDG_RUNTIME_DIR'] as string, fileName); - } - - return path.join(os.tmpdir(), fileName); } protected onRequest(req: http.ServerRequest, res: http.ServerResponse): void { @@ -99,24 +103,24 @@ export class Askpass implements Disposable { req.setEncoding('utf8'); req.on('data', (d: string) => chunks.push(d)); req.on('end', () => { - const { request, host } = JSON.parse(chunks.join('')); - - this.prompt(host, request).then(result => { + const { gitRequest, gitHost } = JSON.parse(chunks.join('')); + this.prompt(gitHost, gitRequest).then(result => { res.writeHead(200); res.end(JSON.stringify(result)); - }, () => { + }, err => { + this.logger.error(err); res.writeHead(500); res.end(); }); }); } - protected async prompt(host: string, request: string): Promise { + protected async prompt(requestingHost: string, request: string): Promise { try { const answer = await this.promptServer.ask({ password: /password/i.test(request), text: request, - details: `Git: ${host} (Press 'Enter' to confirm or 'Escape' to cancel.)` + details: `Git: ${requestingHost} (Press 'Enter' to confirm or 'Escape' to cancel.)` }); if (GitPrompt.Success.is(answer) && typeof answer.result === 'string') { return answer.result; @@ -128,23 +132,11 @@ export class Askpass implements Disposable { } throw new Error('Unexpected answer.'); // Do not ever log the `answer`, it might contain the password. } catch (e) { - this.logger.error(`An unexpected error occurred when requesting ${request} by ${host}.`, e); + this.logger.error(`An unexpected error occurred when requesting ${request} by ${requestingHost}.`, e); return ''; } } - protected async randomBytes(size: number): Promise { - return new Promise((resolve, reject) => { - crypto.randomBytes(size, (error: Error, buffer: Buffer) => { - if (error) { - reject(error); - return; - } - resolve(buffer); - }); - }); - } - async getEnv(): Promise { if (!this.enabled) { return { @@ -159,11 +151,11 @@ export class Askpass implements Disposable { THEIA_GIT_ASKPASS_MAIN, THEIA_GIT_ASKPASS_HANDLE ] = await Promise.all([ - this.ELECTRON_RUN_AS_NODE, - this.GIT_ASKPASS, - this.THEIA_GIT_ASKPASS_NODE, - this.THEIA_GIT_ASKPASS_MAIN, - this.THEIA_GIT_ASKPASS_HANDLE + this.ELECTRON_RUN_AS_NODE(), + this.GIT_ASKPASS(), + this.THEIA_GIT_ASKPASS_NODE(), + this.THEIA_GIT_ASKPASS_MAIN(), + this.THEIA_GIT_ASKPASS_HANDLE() ]); return { @@ -177,29 +169,29 @@ export class Askpass implements Disposable { dispose(): void { this.server.close(); - if (this.ipcHandlePath && !isWindows && fs.existsSync(this.ipcHandlePath)) { - fs.unlinkSync(this.ipcHandlePath); - } } - protected get GIT_ASKPASS(): MaybePromise { + protected GIT_ASKPASS(): MaybePromise { return path.join(__dirname, '..', '..', '..', 'src', 'electron-node', 'askpass', 'askpass.sh'); } - protected get ELECTRON_RUN_AS_NODE(): MaybePromise { + protected ELECTRON_RUN_AS_NODE(): MaybePromise { return '1'; } - protected get THEIA_GIT_ASKPASS_NODE(): MaybePromise { + protected THEIA_GIT_ASKPASS_NODE(): MaybePromise { return process.execPath; } - protected get THEIA_GIT_ASKPASS_MAIN(): MaybePromise { + protected THEIA_GIT_ASKPASS_MAIN(): MaybePromise { return path.join(__dirname, 'askpass-main.js'); } - protected get THEIA_GIT_ASKPASS_HANDLE(): MaybePromise { - return this.ipcHandlePathPromise; + protected THEIA_GIT_ASKPASS_HANDLE(): MaybePromise { + if (this.serverAddress) { + return `http://localhost:${this.serverAddress.port}`; + } + return undefined; } }