Skip to content

Commit

Permalink
GH-3397: Switched from a named pipes server to an HTTP server.
Browse files Browse the repository at this point in the history
Named pipes are not supported by `cluster` workers.
So the communicating between the Theia backend
and the `GIT_ASKPASS` process is pure HTTP.

TODO: squash me if this is sufficient on Windows too.
XXX: still prompts to the backend console if **not** in debug mode.
Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
Akos Kitta committed Nov 13, 2018
1 parent 4fd952c commit 7b4b2e6
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 92 deletions.
46 changes: 25 additions & 21 deletions packages/git/src/electron-node/askpass/askpass-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
*--------------------------------------------------------------------------------------------*/
// Based on: https:/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 {
Expand All @@ -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:`.
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[] = [];
Expand All @@ -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();
}

Expand Down
5 changes: 1 addition & 4 deletions packages/git/src/electron-node/askpass/askpass.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,4 @@
################################################################################/
# Based on: https:/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" $*
126 changes: 59 additions & 67 deletions packages/git/src/electron-node/askpass/askpass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,44 @@
// Based on: https:/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()
Expand All @@ -51,72 +71,56 @@ export class Askpass implements Disposable {
protected readonly promptServer: DugiteGitPromptServer;

protected server: http.Server;
protected ipcHandlePathPromise: Promise<string>;
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<string> {
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 {
const chunks: string[] = [];
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<string> {
protected async prompt(requestingHost: string, request: string): Promise<string> {
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;
Expand All @@ -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<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
crypto.randomBytes(size, (error: Error, buffer: Buffer) => {
if (error) {
reject(error);
return;
}
resolve(buffer);
});
});
}

async getEnv(): Promise<AskpassEnvironment> {
if (!this.enabled) {
return {
Expand All @@ -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 {
Expand All @@ -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<string> {
protected GIT_ASKPASS(): MaybePromise<string> {
return path.join(__dirname, '..', '..', '..', 'src', 'electron-node', 'askpass', 'askpass.sh');
}

protected get ELECTRON_RUN_AS_NODE(): MaybePromise<string | undefined> {
protected ELECTRON_RUN_AS_NODE(): MaybePromise<string | undefined> {
return '1';
}

protected get THEIA_GIT_ASKPASS_NODE(): MaybePromise<string | undefined> {
protected THEIA_GIT_ASKPASS_NODE(): MaybePromise<string | undefined> {
return process.execPath;
}

protected get THEIA_GIT_ASKPASS_MAIN(): MaybePromise<string | undefined> {
protected THEIA_GIT_ASKPASS_MAIN(): MaybePromise<string | undefined> {
return path.join(__dirname, 'askpass-main.js');
}

protected get THEIA_GIT_ASKPASS_HANDLE(): MaybePromise<string | undefined> {
return this.ipcHandlePathPromise;
protected THEIA_GIT_ASKPASS_HANDLE(): MaybePromise<string | undefined> {
if (this.serverAddress) {
return `http://localhost:${this.serverAddress.port}`;
}
return undefined;
}

}

0 comments on commit 7b4b2e6

Please sign in to comment.