2
0

4 Commits

Author SHA1 Message Date
Sergio Padrino
e63974e724 Fix one (of many) build issues on Windows 2021-03-12 17:05:53 +01:00
Sergio Padrino
0882b7b0ca Output data from server to stdout and stderr 2021-03-12 17:05:53 +01:00
Sergio Padrino
85f844f519 Always send \0 after stdin, even if stdin is empty 2021-03-12 17:05:53 +01:00
Sergio Padrino
bf04082571 Add support for forwarding stdin data to server 2021-03-12 17:05:53 +01:00
13 changed files with 2272 additions and 2333 deletions

View File

@@ -13,18 +13,17 @@ jobs:
name: ${{ matrix.friendlyName }}
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node: [20.12.2]
os: [macos-latest, windows-latest, ubuntu-latest]
node: [12.14.1]
os: [macos-10.14, windows-2019, ubuntu-18.04]
include:
- os: macos-latest
- os: macos-10.14
friendlyName: macOS
- os: windows-latest
- os: windows-2019
friendlyName: Windows
- os: ubuntu-latest
- os: ubuntu-18.04
friendlyName: Linux
steps:
- uses: actions/checkout@v2
@@ -34,26 +33,19 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install Python setup tools
run: |
python -m pip install --upgrade setuptools packaging
- name: Install and build
run: |
yarn install
yarn build
run: yarn
- name: Lint
run: yarn lint
- name: Test
run: yarn test
shell: bash
- name: Prebuild (x64)
run: npm run prebuild-napi-x64
- name: Prebuild (arm64)
run: npm run prebuild-napi-arm64
if: ${{ matrix.os != 'ubuntu-latest' }}
- name: Prebuild (Windows x86)
run: npm run prebuild-napi-ia32
if: ${{ matrix.os == 'windows-latest' }}
- name: Prebuild Node x64
run: yarn prebuild-node
- name: Prebuild Electron x64
run: yarn prebuild-electron
- name: Prebuild Electron arm64
run: yarn prebuild-electron-arm64
- name: Publish
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: yarn upload

View File

@@ -1 +1 @@
20.12.2
12.14.1

View File

@@ -2,7 +2,7 @@
A cross-platform no-dependency C executable trampoline which lets GitHub Desktop
intercede in order to provide Git with any additional info it needs (like
credentials through `GIT_ASKPASS` or `SSH_ASKPASS`).
credentials through `GIT_ASKPASS`).
The intention is to support the same platforms that
[Electron supports](https://www.electronjs.org/docs/tutorial/support#supported-platforms).
@@ -60,7 +60,7 @@ The equivalent Bash shell code looks like this:
```sh
# environment variable
GIT_ASKPASS="C:/some/path/to/desktop-askpass-trampoline.exe" \
GIT_ASKPASS="C:/some/path/to/desktop-trampoline.exe" \
# ensure Git doesn't block the process waiting for the user to provide input
GIT_TERMINAL_PROMPT=0 \
git \
@@ -125,15 +125,3 @@ Thanks to this, with only one generic trampoline that forwards everything via
that TCP socket, the implementation for every possible protocol like
`GIT_ASKPASS` can live within the GitHub Desktop codebase instead of having
multiple trampoline executables.
## SSH Wrapper
Along with the trampoline, an SSH wrapper is provided for macOS. The reason for
this is macOS before Monterey include an "old" version of OpenSSH that will
ignore the `SSH_ASKPASS` variable unless it's unable to write to a tty.
This SSH wrapper achieves exactly that: just runs whatever `ssh` exists in the
path in a way that will use `SSH_ASKPASS` when necessary.
More recent versions of OpenSSH (starting with 8.3) don't require this wrapper,
since they added support for a new `SSH_ASKPASS_REQUIRE` environment variable.

View File

@@ -1,10 +1,13 @@
{
'target_defaults': {
'defines': [
"NAPI_VERSION=<(napi_build_version)",
'targets': [
{
'target_name': 'desktop-trampoline',
'type': 'executable',
'sources': [
'src/desktop-trampoline.c',
'src/socket.c'
],
'include_dirs': [
'<!(node -p "require(\'node-addon-api\').include_dir")',
'include'
],
'xcode_settings': {
@@ -24,61 +27,20 @@
'-pie',
'-D_FORTIFY_SOURCE=1',
'-fstack-protector-strong',
'-Werror=format-security',
'-fno-exceptions'
'-Werror=format-security'
],
'cflags_cc!': [ '-fno-exceptions' ],
'ldflags!': [
'-z relro',
'-z now'
],
'msvs_settings': {
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
},
'conditions': [
['OS=="win"', { 'defines': [ 'WINDOWS' ] }]
]
},
'targets': [
{
'target_name': 'desktop-askpass-trampoline',
'type': 'executable',
'sources': [
'src/desktop-trampoline.c',
'src/socket.c'
],
'conditions': [
['OS=="win"', {
'defines': [ 'WINDOWS' ],
'link_settings': {
'libraries': [ 'Ws2_32.lib' ]
}
}]
]
},
{
'target_name': 'desktop-credential-helper-trampoline',
'type': 'executable',
'defines': [
'CREDENTIAL_HELPER'
],
'sources': [
'src/desktop-trampoline.c',
'src/socket.c'
],
'conditions': [
['OS=="win"', {
'link_settings': {
'libraries': [ 'Ws2_32.lib' ]
}
}]
]
},
{
'target_name': 'ssh-wrapper',
'type': 'executable',
'sources': [
'src/ssh-wrapper.c'
],
},
],
}

10
index.d.ts vendored
View File

@@ -1,8 +1,2 @@
export function getDesktopAskpassTrampolinePath(): string
export function getDesktopAskpassTrampolineFilename(): string
export function getDesktopCredentialHelperTrampolinePath(): string
export function getDesktopCredentialHelperTrampolineFilename(): string
export function getSSHWrapperPath(): string
export function getSSHWrapperFilename(): string
export function getDesktopTrampolinePath(): string
export function getDesktopTrampolineFilename(): string

View File

@@ -1,48 +1,21 @@
const Path = require('path')
function getDesktopAskpassTrampolinePath() {
function getDesktopTrampolinePath() {
return Path.join(
__dirname,
'build',
'Release',
getDesktopAskpassTrampolineFilename()
getDesktopTrampolineFilename()
)
}
function getDesktopAskpassTrampolineFilename() {
function getDesktopTrampolineFilename() {
return process.platform === 'win32'
? 'desktop-askpass-trampoline.exe'
: 'desktop-askpass-trampoline'
}
function getDesktopCredentialHelperTrampolinePath() {
return Path.join(
__dirname,
'build',
'Release',
getDesktopCredentialHelperTrampolineFilename()
)
}
function getDesktopCredentialHelperTrampolineFilename() {
return process.platform === 'win32'
? 'desktop-credential-helper-trampoline.exe'
: 'desktop-credential-helper-trampoline'
}
function getSSHWrapperPath() {
return Path.join(__dirname, 'build', 'Release', getSSHWrapperFilename())
}
function getSSHWrapperFilename() {
return process.platform === 'win32' ? 'ssh-wrapper.exe' : 'ssh-wrapper'
? 'desktop-trampoline.exe'
: 'desktop-trampoline'
}
module.exports = {
getDesktopAskpassTrampolinePath,
getDesktopAskpassTrampolineFilename,
getDesktopCredentialHelperTrampolinePath,
getDesktopCredentialHelperTrampolineFilename,
getSSHWrapperPath,
getSSHWrapperFilename,
getDesktopTrampolinePath,
getDesktopTrampolineFilename,
}

View File

@@ -1,6 +1,6 @@
{
"name": "desktop-trampoline",
"version": "0.9.9",
"version": "0.9.4",
"main": "index.js",
"keywords": [],
"author": "",
@@ -15,10 +15,10 @@
"test": "jest",
"lint": "prettier -c **/*.js **/*.md",
"lint:fix": "prettier --write **/*.js **/*.md",
"prebuild-napi-x64": "prebuild -t 3 -r napi -a x64 --strip --include-regex \"(desktop-trampoline|ssh-wrapper)(\\.exe)?$\"",
"prebuild-napi-ia32": "prebuild -t 3 -r napi -a ia32 --strip --include-regex \"(desktop-trampoline|ssh-wrapper)(\\.exe)?$\"",
"prebuild-napi-arm64": "prebuild -t 3 -r napi -a arm64 --strip --include-regex \"(desktop-trampoline|ssh-wrapper)(\\.exe)?$\"",
"prebuild-all": "yarn prebuild-napi-x64 && yarn prebuild-napi-ia32 && yarn prebuild-napi-arm64",
"prebuild-node": "prebuild -t 10.11.0 -t 11.9.0 -t 12.0.0 -t 14.8.0 --strip --include-regex \"desktop-trampoline(\\.exe)?$\"",
"prebuild-electron": "prebuild -t 7.0.0 -t 8.0.0 -t 9.0.0 -t 10.0.0 -t 11.0.0 -r electron --strip --include-regex \"desktop-trampoline(\\.exe)?$\"",
"prebuild-electron-arm64": "prebuild -t 7.0.0 -t 8.0.0 -t 9.0.0 -t 10.0.0 -t 11.0.0 -r electron -a arm64 --strip --include-regex \"desktop-trampoline(\\.exe)?$\"",
"prebuild-all": "yarn prebuild-node && yarn prebuild-electron && yarn prebuild-electron-arm64",
"upload": "node ./script/upload.js"
},
"repository": {
@@ -29,24 +29,11 @@
"url": "https://github.com/desktop/desktop-trampoline/issues"
},
"homepage": "https://github.com/desktop/desktop-trampoline#readme",
"dependencies": {
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.1.2"
},
"devDependencies": {
"jest": "^27.5.0",
"node-gyp": "^10.1.0",
"prebuild": "^13.0.1",
"prettier": "^2.5.1",
"split2": "^4.1.0"
},
"binary": {
"napi_versions": [
3
]
},
"config": {
"runtime": "napi",
"target": 3
"jest": "^26.4.2",
"node-gyp": "^7.1.0",
"prebuild": "^10.0.1",
"prettier": "^2.1.2",
"split2": "^3.2.2"
}
}

View File

@@ -1,5 +1,7 @@
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -9,13 +11,6 @@
#define BUFFER_LENGTH 4096
#define MAXIMUM_NUMBER_LENGTH 33
#ifdef CREDENTIAL_HELPER
#define DESKTOP_TRAMPOLINE_IDENTIFIER "CREDENTIALHELPER"
#else
#define DESKTOP_TRAMPOLINE_IDENTIFIER "ASKPASS"
#endif
#define WRITE_STRING_OR_EXIT(dataName, dataString) \
if (writeSocket(socket, dataString, strlen(dataString) + 1) != 0) { \
printSocketError("ERROR: Couldn't send " dataName); \
@@ -24,9 +19,12 @@ if (writeSocket(socket, dataString, strlen(dataString) + 1) != 0) { \
// This is a list of valid environment variables that GitHub Desktop might
// send or expect to receive.
#define NUMBER_OF_VALID_ENV_VARS 1
#define NUMBER_OF_VALID_ENV_VARS 4
static const char *sValidEnvVars[NUMBER_OF_VALID_ENV_VARS] = {
"DESKTOP_TRAMPOLINE_IDENTIFIER",
"DESKTOP_TRAMPOLINE_TOKEN",
"DESKTOP_USERNAME",
"DESKTOP_ENDPOINT",
};
/** Returns 1 if a given env variable is valid, 0 otherwise. */
@@ -48,6 +46,42 @@ int isValidEnvVar(char *env) {
return 0;
}
/**
* Reads a string from the socket, reading first 2 bytes to get its length and
* then the string itself.
*/
ssize_t readDelimitedString(SOCKET socket, char *buffer, size_t bufferSize) {
uint16_t outputLength = 0;
if (readSocket(socket, &outputLength, sizeof(uint16_t)) < (int)sizeof(uint16_t)) {
printSocketError("ERROR: Error reading from socket");
return -1;
}
if (outputLength > bufferSize) {
fprintf(stderr, "ERROR: received string is bigger than buffer (%d > %zu)", outputLength, bufferSize);
return -1;
}
size_t totalBytesRead = 0;
ssize_t bytesRead = 0;
// Read output from server
do {
bytesRead = readSocket(socket, buffer + totalBytesRead, outputLength - totalBytesRead);
if (bytesRead == -1) {
printSocketError("ERROR: Error reading from socket");
return -1;
}
totalBytesRead += bytesRead;
} while (bytesRead > 0);
buffer[totalBytesRead] = '\0';
return totalBytesRead;
}
int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
char *desktopPortString;
@@ -87,9 +121,8 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
}
// Get the number of environment variables
char *validEnvVars[NUMBER_OF_VALID_ENV_VARS + 1];
validEnvVars[0] = "DESKTOP_TRAMPOLINE_IDENTIFIER=" DESKTOP_TRAMPOLINE_IDENTIFIER;
int envc = 1;
char *validEnvVars[NUMBER_OF_VALID_ENV_VARS];
int envc = 0;
for (char **env = envp; *env != 0; env++) {
if (isValidEnvVar(*env)) {
validEnvVars[envc] = *env;
@@ -107,37 +140,57 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
WRITE_STRING_OR_EXIT("environment variable", validEnvVars[idx]);
}
char stdinBuffer[BUFFER_LENGTH + 1];
int stdinBytes = 0;
#ifdef CREDENTIAL_HELPER
stdinBytes = fread(stdinBuffer, sizeof(char), BUFFER_LENGTH, stdin);
#endif
stdinBuffer[stdinBytes] = '\0';
WRITE_STRING_OR_EXIT("stdin", stdinBuffer);
char buffer[BUFFER_LENGTH + 1];
size_t totalBytesRead = 0;
ssize_t bytesRead = 0;
size_t totalBytesWritten = 0;
ssize_t bytesToWrite = 0;
// Read output from server
// Make stdin reading non-blocking, to prevent getting stuck when no data is
// provided via stdin.
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
// Send stdin data
do {
bytesRead = readSocket(socket, buffer + totalBytesRead, BUFFER_LENGTH - totalBytesRead);
bytesToWrite = read(0, buffer, BUFFER_LENGTH);
if (bytesRead == -1) {
printSocketError("ERROR: Error reading from socket");
if (bytesToWrite == -1) {
if (totalBytesWritten == 0) {
// No stdin content found, continuing...
break;
} else {
fprintf(stderr, "ERROR: Error reading stdin data");
return 1;
}
}
if (writeSocket(socket, buffer, bytesToWrite) != 0) {
printSocketError("ERROR: Couldn't send stdin data");
return 1;
}
totalBytesRead += bytesRead;
} while (bytesRead > 0);
totalBytesWritten += bytesToWrite;
} while (bytesToWrite > 0);
buffer[totalBytesRead] = '\0';
writeSocket(socket, "\0", 1);
// Read stdout from the server
if (readDelimitedString(socket, buffer, BUFFER_LENGTH) == -1) {
fprintf(stderr, "ERROR: Couldn't read stdout from socket");
return 1;
}
// Write that output to stdout
fprintf(stdout, "%s", buffer);
// Read stderr from the server
if (readDelimitedString(socket, buffer, BUFFER_LENGTH) == -1) {
fprintf(stderr, "ERROR: Couldn't read stdout from socket");
return 1;
}
// Write that output to stderr
fprintf(stderr, "%s", buffer);
return 0;
}

View File

@@ -1,37 +0,0 @@
#ifdef WINDOWS
int main(int argc, char **argv) {
// Not needed on Windows, this will just create a dummy executable
return -1;
}
#else
#include <unistd.h>
#include <stdio.h>
/**
* This is a wrapper for the ssh command. It is used to make sure ssh runs without
* a tty on macOS, allowing GitHub Desktop to intercept different prompts from
* ssh (e.g. passphrase, adding a host to the list of known hosts...).
* This is not necessary on more recent versions of OpenSSH (starting with v8.3)
* which include support for the SSH_ASKPASS_REQUIRE environment variable.
*/
int main(int argc, char **argv) {
pid_t child = fork();
if (child < 0) {
fprintf(stderr, "Failed to fork\n");
return -1;
}
if (child != 0) {
// This is the parent process. Just exit.
return 0;
}
setsid();
return execvp("ssh", argv);
}
#endif

View File

@@ -1,153 +0,0 @@
const { stat, access } = require('fs').promises
const { constants } = require('fs')
const { execFile } = require('child_process')
const { promisify } = require('util')
const {
getDesktopAskpassTrampolinePath,
getDesktopCredentialHelperTrampolinePath,
} = require('../index')
const split2 = require('split2')
const { createServer } = require('net')
const askPassTrampolinePath = getDesktopAskpassTrampolinePath()
const helperTrampolinePath = getDesktopCredentialHelperTrampolinePath()
const run = promisify(execFile)
describe('desktop-trampoline', () => {
it('exists and is a regular file', async () =>
expect((await stat(askPassTrampolinePath)).isFile()).toBe(true))
it('can be executed by current process', () =>
access(askPassTrampolinePath, constants.X_OK))
it('fails when required environment variables are missing', () =>
expect(run(askPassTrampolinePath, ['Username'])).rejects.toThrow())
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'))
// 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)
})
})
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 env = {
DESKTOP_TRAMPOLINE_TOKEN: '123456',
DESKTOP_PORT: port,
INVALID_VARIABLE: 'foo bar',
}
const opts = { env }
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 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
const output = await outputPromise
const envc = parseInt(output[2])
const outputEnv = output.slice(3, 3 + envc)
expect(outputEnv).toContain(
'DESKTOP_TRAMPOLINE_IDENTIFIER=CREDENTIALHELPER'
)
})
})

183
test/index.test.js Normal file
View File

@@ -0,0 +1,183 @@
const { stat, access } = require('fs').promises
const { constants } = require('fs')
const { execFile } = require('child_process')
const { promisify } = require('util')
const { getDesktopTrampolinePath } = require('../index')
const split2 = require('split2')
const { createServer } = require('net')
const trampolinePath = getDesktopTrampolinePath()
const run = promisify(execFile)
async function startTrampolineServer(output, stdout = '', stderr = '') {
const server = createServer(socket => {
socket.pipe(split2(/\0/)).on('data', data => {
output.push(data.toString('utf8'))
})
const buffer = Buffer.alloc(2, 0)
// Send stdout
buffer.writeUInt16LE(stdout.length, 0)
socket.write(buffer)
socket.write(stdout)
// Send stderr
buffer.writeUInt16LE(stderr.length, 0)
socket.write(buffer)
socket.write(stderr)
// Close the socket
socket.end()
})
server.unref()
const startServer = async () => {
return new Promise((resolve, reject) => {
server.on('error', e => reject(e))
server.listen(0, '127.0.0.1', () => {
resolve(server.address().port)
})
})
}
return {
server,
port: await startServer(),
}
}
describe('desktop-trampoline', () => {
it('exists and is a regular file', async () =>
expect((await stat(trampolinePath)).isFile()).toBe(true))
it('can be executed by current process', () =>
access(trampolinePath, constants.X_OK))
it('fails when required environment variables are missing', () =>
expect(run(trampolinePath, ['Username'])).rejects.toThrow())
describe('with a trampoline server', () => {
let server = null
let output = []
let baseEnv = {}
async function configureTrampolineServer(stdout = '', stderr = '') {
output = []
const serverInfo = await startTrampolineServer(output, stdout, stderr)
server = serverInfo.server
baseEnv = {
DESKTOP_PORT: serverInfo.port,
}
}
afterEach(() => {
server.close()
})
it('forwards arguments and valid environment variables correctly without stdin', async () => {
await configureTrampolineServer()
const env = {
...baseEnv,
DESKTOP_TRAMPOLINE_IDENTIFIER: '123456',
DESKTOP_USERNAME: 'sergiou87',
DESKTOP_USERNAME_FAKE: 'fake-user',
INVALID_VARIABLE: 'foo bar',
}
const opts = { env }
await run(trampolinePath, ['baz'], opts)
const outputArguments = output.slice(1, 2)
expect(outputArguments).toStrictEqual(['baz'])
// output[2] is the number of env variables
const outputEnv = output.slice(3, output.length - 1)
expect(outputEnv).toHaveLength(2)
expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=123456')
expect(outputEnv).toContain(`DESKTOP_USERNAME=sergiou87`)
expect(output[output.length - 1]).toStrictEqual('')
})
it('forwards arguments, environment variables and stdin correctly', async () => {
await configureTrampolineServer()
const env = {
...baseEnv,
DESKTOP_TRAMPOLINE_IDENTIFIER: '123456',
DESKTOP_USERNAME: 'sergiou87',
}
const opts = {
env,
stdin: 'This is a test\nWith a multiline\nStandard input text',
}
const run = new Promise((resolve, reject) => {
const process = execFile(trampolinePath, ['baz'], opts, function (
err,
stdout,
stderr
) {
if (!err) {
resolve({ stdout, stderr, exitCode: 0 })
return
}
reject(err)
})
process.stdin.end(
'This is a test\nWith a multiline\nStandard input text',
'utf-8'
)
})
await run
const outputArguments = output.slice(1, 2)
expect(outputArguments).toStrictEqual(['baz'])
// output[2] is the number of env variables
const outputEnv = output.slice(3, output.length - 1)
expect(outputEnv).toHaveLength(2)
expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=123456')
expect(outputEnv).toContain(`DESKTOP_USERNAME=sergiou87`)
expect(output[output.length - 1]).toBe(
'This is a test\nWith a multiline\nStandard input text'
)
})
it('outputs the same stdout received from the server, when no stderr is specified', async () => {
await configureTrampolineServer('This is the command stdout', '')
const opts = { env: baseEnv }
const result = await run(trampolinePath, ['baz'], opts)
expect(result.stdout).toStrictEqual('This is the command stdout')
expect(result.stderr).toStrictEqual('')
})
it('outputs the same stderr received from the server, when no stdout is specified', async () => {
await configureTrampolineServer('', 'This is the command stderr')
const opts = { env: baseEnv }
const result = await run(trampolinePath, ['baz'], opts)
expect(result.stdout).toStrictEqual('')
expect(result.stderr).toStrictEqual('This is the command stderr')
})
it('outputs the same stdout and stderr received from the server, when both are specified', async () => {
await configureTrampolineServer(
'This is the command stdout',
'This is the command stderr'
)
const opts = { env: baseEnv }
const result = await run(trampolinePath, ['baz'], opts)
expect(result.stdout).toStrictEqual('This is the command stdout')
expect(result.stderr).toStrictEqual('This is the command stderr')
})
})
})

View File

@@ -1,42 +0,0 @@
const { stat, access } = require('fs').promises
const { constants } = require('fs')
const { execFile } = require('child_process')
const { promisify } = require('util')
const { getSSHWrapperPath } = require('../index')
const sshWrapperPath = getSSHWrapperPath()
const run = promisify(execFile)
describe('ssh-wrapper', () => {
it('exists and is a regular file', async () =>
expect((await stat(sshWrapperPath)).isFile()).toBe(true))
// On Windows, the binary generated is just useless, so no point to test it.
// Also, this won't be used on Linux (for now at least), so don't bother to
// run the tests there.
if (process.platform !== 'darwin') {
return
}
it('can be executed by current process', () =>
access(sshWrapperPath, constants.X_OK))
it('attempts to use ssh-askpass program', async () => {
// Try to connect to github.com with a non-existent known_hosts file to force
// ssh to prompt the user and use askpass.
const result = await run(
sshWrapperPath,
['-o', 'UserKnownHostsFile=/path/to/fake/known_hosts', 'git@github.com'],
{
env: {
SSH_ASKPASS: '/path/to/fake/ssh-askpass',
DISPLAY: '.',
},
}
)
expect(result.stderr).toMatch(
/ssh_askpass: exec\(\/path\/to\/fake\/ssh-askpass\): No such file or directory/
)
})
})

3887
yarn.lock
View File

File diff suppressed because it is too large Load Diff