From bf04082571fba0906561ef126f9c1b67c730bad1 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Tue, 23 Feb 2021 18:45:14 +0100 Subject: [PATCH 1/4] Add support for forwarding stdin data to server --- src/desktop-trampoline.c | 37 ++++++++++++++- test/index.test.js | 100 +++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/desktop-trampoline.c b/src/desktop-trampoline.c index 735e057..6faed58 100644 --- a/src/desktop-trampoline.c +++ b/src/desktop-trampoline.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -102,9 +103,41 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) { WRITE_STRING_OR_EXIT("environment variable", validEnvVars[idx]); } - // TODO: send stdin stuff? - char buffer[BUFFER_LENGTH + 1]; + size_t totalBytesWritten = 0; + ssize_t bytesToWrite = 0; + + // 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 { + bytesToWrite = read(0, buffer, BUFFER_LENGTH); + + 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; \ + } + + totalBytesWritten += bytesToWrite; + } while (bytesToWrite > 0); + + if (totalBytesWritten > 0) { + writeSocket(socket, "\0", 1); + } + size_t totalBytesRead = 0; ssize_t bytesRead = 0; diff --git a/test/index.test.js b/test/index.test.js index 2e07f6a..ba278a2 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -9,6 +9,33 @@ const { createServer } = require('net') const trampolinePath = getDesktopTrampolinePath() const run = promisify(execFile) +async function startTrampolineServer(output) { + const server = createServer(socket => { + 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() + }) + 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)) @@ -19,29 +46,10 @@ describe('desktop-trampoline', () => { it('fails when required environment variables are missing', () => expect(run(trampolinePath, ['Username'])).rejects.toThrow()) - it('forwards arguments and valid environment variables correctly', async () => { + it('forwards arguments and valid environment variables correctly without stdin', async () => { const output = [] - const server = createServer(socket => { - socket.pipe(split2(/\0/)).on('data', data => { - output.push(data.toString('utf8')) - }) + const { server, port } = await startTrampolineServer(output) - // Don't send anything and just close the socket after the trampoline is - // done forwarding data. - socket.end() - }) - 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 port = await startTrampolineServer() const env = { DESKTOP_TRAMPOLINE_IDENTIFIER: '123456', DESKTOP_PORT: port, @@ -63,4 +71,54 @@ describe('desktop-trampoline', () => { server.close() }) + + it('forwards arguments, environment variables and stdin correctly', async () => { + const output = [] + const { server, port } = await startTrampolineServer(output) + + const env = { + DESKTOP_TRAMPOLINE_IDENTIFIER: '123456', + DESKTOP_PORT: port, + 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' + ) + + server.close() + }) }) From 85f844f51967a6c80576bc785da64afef209edd8 Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 24 Feb 2021 09:52:25 +0100 Subject: [PATCH 2/4] Always send \0 after stdin, even if stdin is empty --- src/desktop-trampoline.c | 4 +--- test/index.test.js | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/desktop-trampoline.c b/src/desktop-trampoline.c index 6faed58..21964af 100644 --- a/src/desktop-trampoline.c +++ b/src/desktop-trampoline.c @@ -134,9 +134,7 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) { totalBytesWritten += bytesToWrite; } while (bytesToWrite > 0); - if (totalBytesWritten > 0) { - writeSocket(socket, "\0", 1); - } + writeSocket(socket, "\0", 1); size_t totalBytesRead = 0; ssize_t bytesRead = 0; diff --git a/test/index.test.js b/test/index.test.js index ba278a2..8753b50 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -61,14 +61,18 @@ describe('desktop-trampoline', () => { await run(trampolinePath, ['baz'], opts) + console.log(output) + const outputArguments = output.slice(1, 2) expect(outputArguments).toStrictEqual(['baz']) // output[2] is the number of env variables - const outputEnv = output.slice(3) + 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('') + server.close() }) From 0882b7b0cae280c10cd6b114f3f24672e947ad6e Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 3 Mar 2021 09:49:57 +0100 Subject: [PATCH 3/4] Output data from server to stdout and stderr --- src/desktop-trampoline.c | 72 +++++++++++---- test/index.test.js | 189 +++++++++++++++++++++++++-------------- 2 files changed, 175 insertions(+), 86 deletions(-) diff --git a/src/desktop-trampoline.c b/src/desktop-trampoline.c index 21964af..fa4cff8 100644 --- a/src/desktop-trampoline.c +++ b/src/desktop-trampoline.c @@ -45,6 +45,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; @@ -126,9 +162,9 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) { } } - if (writeSocket(socket, buffer, bytesToWrite) != 0) { \ - printSocketError("ERROR: Couldn't send stdin data"); \ - return 1; \ + if (writeSocket(socket, buffer, bytesToWrite) != 0) { + printSocketError("ERROR: Couldn't send stdin data"); + return 1; } totalBytesWritten += bytesToWrite; @@ -136,26 +172,24 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) { writeSocket(socket, "\0", 1); - size_t totalBytesRead = 0; - ssize_t bytesRead = 0; - - // Read output from server - do { - bytesRead = readSocket(socket, buffer + totalBytesRead, BUFFER_LENGTH - totalBytesRead); - - if (bytesRead == -1) { - printSocketError("ERROR: Error reading from socket"); - return 1; - } - - totalBytesRead += bytesRead; - } while (bytesRead > 0); - - buffer[totalBytesRead] = '\0'; + // 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; } diff --git a/test/index.test.js b/test/index.test.js index 8753b50..949fefd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -9,14 +9,25 @@ const { createServer } = require('net') const trampolinePath = getDesktopTrampolinePath() const run = promisify(execFile) -async function startTrampolineServer(output) { +async function startTrampolineServer(output, stdout = '', stderr = '') { const server = createServer(socket => { 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. + 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() @@ -46,83 +57,127 @@ describe('desktop-trampoline', () => { it('fails when required environment variables are missing', () => expect(run(trampolinePath, ['Username'])).rejects.toThrow()) - it('forwards arguments and valid environment variables correctly without stdin', async () => { - const output = [] - const { server, port } = await startTrampolineServer(output) - - const env = { - DESKTOP_TRAMPOLINE_IDENTIFIER: '123456', - DESKTOP_PORT: port, - DESKTOP_USERNAME: 'sergiou87', - DESKTOP_USERNAME_FAKE: 'fake-user', - INVALID_VARIABLE: 'foo bar', + 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, + } } - const opts = { env } - await run(trampolinePath, ['baz'], opts) + afterEach(() => { + server.close() + }) - console.log(output) + 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('') + }) - 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`) + 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' + ) + }) - expect(output[output.length - 1]).toStrictEqual('') + it('outputs the same stdout received from the server, when no stderr is specified', async () => { + await configureTrampolineServer('This is the command stdout', '') - server.close() - }) + const opts = { env: baseEnv } + const result = await run(trampolinePath, ['baz'], opts) - it('forwards arguments, environment variables and stdin correctly', async () => { - const output = [] - const { server, port } = await startTrampolineServer(output) + expect(result.stdout).toStrictEqual('This is the command stdout') + expect(result.stderr).toStrictEqual('') + }) - const env = { - DESKTOP_TRAMPOLINE_IDENTIFIER: '123456', - DESKTOP_PORT: port, - DESKTOP_USERNAME: 'sergiou87', - } - const opts = { - env, - stdin: 'This is a test\nWith a multiline\nStandard input text', - } + it('outputs the same stderr received from the server, when no stdout is specified', async () => { + await configureTrampolineServer('', 'This is the command stderr') - 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) - }) + const opts = { env: baseEnv } + const result = await run(trampolinePath, ['baz'], opts) - process.stdin.end( - 'This is a test\nWith a multiline\nStandard input text', - 'utf-8' - ) + expect(result.stdout).toStrictEqual('') + expect(result.stderr).toStrictEqual('This is the command stderr') }) - 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`) + 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' + ) - expect(output[output.length - 1]).toBe( - 'This is a test\nWith a multiline\nStandard input text' - ) + const opts = { env: baseEnv } + const result = await run(trampolinePath, ['baz'], opts) - server.close() + expect(result.stdout).toStrictEqual('This is the command stdout') + expect(result.stderr).toStrictEqual('This is the command stderr') + }) }) }) From e63974e72463401f2dc0708b564adcba7830e67d Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 3 Mar 2021 10:28:58 +0100 Subject: [PATCH 4/4] Fix one (of many) build issues on Windows --- src/desktop-trampoline.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/desktop-trampoline.c b/src/desktop-trampoline.c index fa4cff8..293c6c9 100644 --- a/src/desktop-trampoline.c +++ b/src/desktop-trampoline.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include