diff --git a/README.md b/README.md index f01f067..e084154 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The equivalent Bash shell code looks like this: ```sh # environment variable - GIT_ASKPASS="C:/some/path/to/desktop-trampoline.exe" \ + GIT_ASKPASS="C:/some/path/to/desktop-askpass-trampoline.exe" \ # ensure Git doesn't block the process waiting for the user to provide input GIT_TERMINAL_PROMPT=0 \ git \ diff --git a/binding.gyp b/binding.gyp index 47d762f..94e7cb4 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,15 +1,8 @@ { - 'targets': [ - { - 'target_name': 'desktop-trampoline', + 'target_defaults': { 'defines': [ "NAPI_VERSION=<(napi_build_version)", ], - 'type': 'executable', - 'sources': [ - 'src/desktop-trampoline.c', - 'src/socket.c' - ], 'include_dirs': [ ' { it('exists and is a regular file', async () => - expect((await stat(trampolinePath)).isFile()).toBe(true)) + expect((await stat(askPassTrampolinePath)).isFile()).toBe(true)) it('can be executed by current process', () => - access(trampolinePath, constants.X_OK)) + access(askPassTrampolinePath, constants.X_OK)) it('fails when required environment variables are missing', () => - expect(run(trampolinePath, ['Username'])).rejects.toThrow()) + expect(run(askPassTrampolinePath, ['Username'])).rejects.toThrow()) - it('forwards arguments and valid environment variables correctly', async () => { + const captureSession = () => { const output = [] + let resolveOutput = null + + const outputPromise = new Promise(resolve => { + resolveOutput = resolve + }) + const server = createServer(socket => { + let timeoutId = null socket.pipe(split2(/\0/)).on('data', data => { output.push(data.toString('utf8')) - }) - // Don't send anything and just close the socket after the trampoline is - // done forwarding data. - socket.end() + // Hack: consider the session finished after 100ms of inactivity. + // In a real-world scenario, you'd have to parse the data to know when + // the session is finished. + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + timeoutId = setTimeout(() => { + resolveOutput(output) + socket.end() + server.close() + }, 100) + }) }) - server.unref() - - const startTrampolineServer = async () => { - return new Promise((resolve, reject) => { - server.on('error', e => reject(e)) - server.listen(0, '127.0.0.1', () => { - resolve(server.address().port) - }) + + const serverPortPromise = new Promise((resolve, reject) => { + server.on('error', e => reject(e)) + server.listen(0, '127.0.0.1', () => { + resolve(server.address().port) }) - } + }) + + return [serverPortPromise, outputPromise] + } + + it('forwards arguments and valid environment variables correctly', async () => { + const [portPromise, outputPromise] = captureSession() + const port = await portPromise - const port = await startTrampolineServer() const env = { - DESKTOP_TRAMPOLINE_IDENTIFIER: '123456', + DESKTOP_TRAMPOLINE_TOKEN: '123456', DESKTOP_PORT: port, INVALID_VARIABLE: 'foo bar', } const opts = { env } - await run(trampolinePath, ['baz'], opts) + await run(askPassTrampolinePath, ['baz'], opts) + const output = await outputPromise const outputArguments = output.slice(1, 2) expect(outputArguments).toStrictEqual(['baz']) // output[2] is the number of env variables - const outputEnv = output.slice(3) - expect(outputEnv).toHaveLength(1) - expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=123456') + const envc = parseInt(output[2]) + const outputEnv = output.slice(3, 3 + envc) + expect(outputEnv).toHaveLength(2) + expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_TOKEN=123456') + expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=ASKPASS') + }) + + it('forwards stdin when running in credential-helper mode', async () => { + const [portPromise, outputPromise] = captureSession() + const port = await portPromise + + const cp = run(helperTrampolinePath, ['get'], { + env: { DESKTOP_PORT: port }, + }) + cp.child.stdin.end('oh hai\n') + + await cp + + const output = await outputPromise + expect(output.at(-1)).toBe('oh hai\n') + }) + + it("doesn't forward stdin when running in askpass mode", async () => { + const [portPromise, outputPromise] = captureSession() + const port = await portPromise + + const cp = run(askPassTrampolinePath, ['get'], { + env: { DESKTOP_PORT: port }, + }) + cp.child.stdin.end('oh hai\n') + + await cp + + const output = await outputPromise + expect(output.at(-1)).toBe('') + }) + + it('askpass handler ignores the DESKTOP_TRAMPOLINE_IDENTIFIER env var', async () => { + const [portPromise, outputPromise] = captureSession() + const port = await portPromise + + const cp = run(askPassTrampolinePath, ['get'], { + env: { DESKTOP_PORT: port, DESKTOP_TRAMPOLINE_IDENTIFIER: 'foo' }, + }) + cp.child.stdin.end('oh hai\n') + + await cp + + const output = await outputPromise + const envc = parseInt(output[2]) + const outputEnv = output.slice(3, 3 + envc) + expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=ASKPASS') + }) + + it('credential handler ignores the DESKTOP_TRAMPOLINE_IDENTIFIER env var', async () => { + const [portPromise, outputPromise] = captureSession() + const port = await portPromise + + const cp = run(helperTrampolinePath, ['get'], { + env: { DESKTOP_PORT: port, DESKTOP_TRAMPOLINE_IDENTIFIER: 'foo' }, + }) + cp.child.stdin.end('oh hai\n') + + await cp - server.close() + const output = await outputPromise + const envc = parseInt(output[2]) + const outputEnv = output.slice(3, 3 + envc) + expect(outputEnv).toContain( + 'DESKTOP_TRAMPOLINE_IDENTIFIER=CREDENTIALHELPER' + ) }) })