2
0

25 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
tidy-dev
28e4f64ae8 Merge pull request #7 from kittenmilk/patch-1
fix typo in README.md
2021-03-11 17:32:34 -05:00
lillian rose
8fb3d4fa9b fix typo in README.md
line 12: beign - being
2021-03-11 13:34:36 -06:00
Sergio Padrino
fa6110530f Merge pull request #6 from desktop/releases/0.9.4
Release 0.9.4
2021-03-11 09:05:38 -08:00
Sergio Padrino
7eabea0f1a Bump version to 0.9.4 2021-03-11 17:50:43 +01:00
Sergio Padrino
82635217c5 Merge pull request #5 from desktop/improve-windows-error-logs 2021-03-11 08:48:56 -08:00
Sergio Padrino
0ff24f8744 Improve error message when the trampoline can't connect to the server 2021-03-11 17:36:00 +01:00
Sergio Padrino
dd13c4b6cc Fix getting WSA error description on Windows 2021-03-11 04:04:25 -08:00
Sergio Padrino
639ed9be89 Show WSA error description on initialization too 2021-03-04 11:08:58 +01:00
Sergio Padrino
ff581e8b44 Get description for Winsock errors 2021-03-04 10:52:04 +01:00
Sergio Padrino
1a8de46fb7 Merge pull request #3 from desktop/releases/0.9.3 2021-02-18 01:15:47 -08:00
Sergio Padrino
ad699b35c0 Update releases.md 2021-02-18 10:08:32 +01:00
Sergio Padrino
31bc7170f6 Bump version to 0.9.3 2021-02-18 10:07:10 +01:00
Sergio Padrino
9cc5d4f9ee Document the release process 2021-02-18 10:07:02 +01:00
Sergio Padrino
731c53d97f Merge pull request #2 from philipturnbull/buffer-off-by-one
Reserve space for the NUL terminator
2021-02-18 00:07:25 -08:00
Phil Turnbull
6669833e3d Reserve space for the NUL terminator
If we read exactly `BUFFER_LENGTH` characters then we will overflow the buffer
when writing the NUL terminator. We need to reserve one extra character for the
NUL terminator.
2021-02-17 18:09:31 -05:00
Sergio Padrino
ca8d10fddb Merge pull request #1 from desktop/releases/0.9.2 2021-02-17 02:50:24 -08:00
Sergio Padrino
c77487f645 Bump package version to 0.9.2 2021-02-17 11:27:56 +01:00
Sergio Padrino
88065c4f2a Use specific OS versions in CI scripts 2021-02-17 11:09:05 +01:00
Sergio Padrino
6ac5a9240d Add more compilation flags to improve security 2021-02-17 11:03:00 +01:00
Sergio Padrino
ca0c849738 Bump package version to 0.9.1 2021-02-02 14:57:28 +01:00
Sergio Padrino
8c44f4b5ea Remove DESKTOP_PORT from the valid env vars
DESKTOP_PORT is only for internal usage, no need to forward it.
2021-02-02 14:57:02 +01:00
8 changed files with 303 additions and 60 deletions

View File

@@ -17,13 +17,13 @@ jobs:
fail-fast: false
matrix:
node: [12.14.1]
os: [macos-latest, windows-latest, ubuntu-latest]
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

View File

@@ -9,7 +9,7 @@ The intention is to support the same platforms that
## Building
This project is written as a Node project with the C-portions beign compiled by
This project is written as a Node project with the C-portions being compiled by
node-gyp. Installing dependencies and building requires Node.js, and yarn. With
those prerequisites the initial setup should be as easy as running `yarn` and
subsequent builds can be done using `yarn build`. There are some tests available

View File

@@ -13,12 +13,25 @@
'xcode_settings': {
'OTHER_CFLAGS': [
'-Wall',
'-Werror'
],
'-Werror',
'-Werror=format-security',
'-fPIC',
'-D_FORTIFY_SOURCE=1',
'-fstack-protector-strong'
]
},
'cflags!': [
'-Wall',
'-Werror',
'-fPIC',
'-pie',
'-D_FORTIFY_SOURCE=1',
'-fstack-protector-strong',
'-Werror=format-security'
],
'ldflags!': [
'-z relro',
'-z now'
],
'conditions': [
['OS=="win"', {

25
docs/releases.md Normal file
View File

@@ -0,0 +1,25 @@
# Releases
All releases are published using GitHub releases. Anyone with push access to the
repository can create a new release.
### Release Process
1. Create a branch named `releases/X.Y.Z`, where `X.Y.Z` is the version you want
to release.
1. Update the `version` field in the `package.json` with the new version you're
about to release.
1. Open a Pull Request for that branch.
1. Once the branch is approved, `git tag vX.Y.Z` the version you wish to
publish. **Important:** the version in the tag name must be preceeded by a
`v`.
1. `git push --follow-tags` to ensure all new commits (and the tag) are pushed
to the remote. Pushing the tag will start the release process.
1. Wait a few minutes for the build to finish (look for the build in
https://github.com/desktop/desktop-trampoline/actions)
1. Once the build is complete it will create a new release with all of the
assets and suggested release notes.
1. Update the changelog to whatever makes sense for this release. It should be
focused on user-facing changes.
1. Confirm all assets are uploaded for all the supported platforms.
1. Merge the Pull Request into `main` and you're done :tada:

View File

@@ -1,6 +1,6 @@
{
"name": "desktop-trampoline",
"version": "0.9.0",
"version": "0.9.4",
"main": "index.js",
"keywords": [],
"author": "",

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>
@@ -17,10 +19,9 @@ 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 5
#define NUMBER_OF_VALID_ENV_VARS 4
static const char *sValidEnvVars[NUMBER_OF_VALID_ENV_VARS] = {
"DESKTOP_TRAMPOLINE_IDENTIFIER",
"DESKTOP_PORT",
"DESKTOP_TRAMPOLINE_TOKEN",
"DESKTOP_USERNAME",
"DESKTOP_ENDPOINT",
@@ -33,8 +34,8 @@ int isValidEnvVar(char *env) {
// Make sure that not only the passed env var string starts with the
// candidate contesnts, but also that there is a '=' character right after:
// Valid: "DESKTOP_PORT=50"
// Not valid: "DESKTOP_PORT_SOMETHING=50"
// Valid: "DESKTOP_USERNAME=sergiou87"
// Not valid: "DESKTOP_USERNAME_SOMETHING=sergiou87"
if (strncmp(env, candidate, strlen(candidate)) == 0
&& strlen(env) > strlen(candidate)
&& env[strlen(candidate)] == '=') {
@@ -45,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;
@@ -67,7 +104,9 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
*outSocket = socket;
if (connectSocket(socket, desktopPort) != 0) {
printSocketError("ERROR: Couldn't connect to 127.0.0.1:%d", desktopPort);
printSocketError("ERROR: Couldn't connect to 127.0.0.1:%d - Please make "
"sure you don't have an antivirus or firewall blocking "
"this connection.", desktopPort);
return 1;
}
@@ -101,29 +140,57 @@ 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;
char buffer[BUFFER_LENGTH];
size_t totalBytesRead = 0;
ssize_t bytesRead = 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);
// Read output from server
// 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

@@ -6,13 +6,30 @@
#include <stdlib.h>
#include <string.h>
#ifdef WINDOWS
#define MAX_WSA_ERROR_DESCRIPTION_LENGTH 4096
void getWSALastErrorDescription(wchar_t *buffer, int bufferLength) {
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)buffer, bufferLength - 1, NULL);
}
#endif
int initializeNetwork(void) {
#ifdef WINDOWS
// Initialize Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2,2), &wsaData);
if (result != NO_ERROR) {
fprintf(stderr, "ERROR: WSAStartup failed: %d\n", result);
wchar_t errorDescription[MAX_WSA_ERROR_DESCRIPTION_LENGTH];
getWSALastErrorDescription(errorDescription, MAX_WSA_ERROR_DESCRIPTION_LENGTH);
fprintf(stderr, "ERROR: WSAStartup failed (%d). Error %ld: %ls\n",
result, WSAGetLastError(), errorDescription);
return 1;
}
#endif
@@ -55,8 +72,7 @@ int readSocket(SOCKET socket, void *buffer, size_t length) {
return recv(socket, buffer, length, 0);
}
void printSocketError(char *fmt, ...)
{
void printSocketError(char *fmt, ...) {
char formatted_string[4096];
va_list argptr;
@@ -65,7 +81,10 @@ void printSocketError(char *fmt, ...)
va_end(argptr);
#ifdef WINDOWS
fprintf(stderr, "%s: %ld\n", formatted_string, WSAGetLastError());
wchar_t errorDescription[MAX_WSA_ERROR_DESCRIPTION_LENGTH];
getWSALastErrorDescription(errorDescription, MAX_WSA_ERROR_DESCRIPTION_LENGTH);
fprintf(stderr, "%s (%ld): %ls\n", formatted_string, WSAGetLastError(), errorDescription);
#else
fprintf(stderr, "%s (%d): %s\n", formatted_string, errno, strerror(errno));
#endif

View File

@@ -9,6 +9,44 @@ 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))
@@ -19,46 +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', async () => {
const output = []
const server = createServer(socket => {
socket.pipe(split2(/\0/)).on('data', data => {
output.push(data.toString('utf8'))
})
describe('with a trampoline server', () => {
let server = null
let output = []
let baseEnv = {}
// Don't send anything and just close the socket after the trampoline is
// done forwarding data.
socket.end()
async function configureTrampolineServer(stdout = '', stderr = '') {
output = []
const serverInfo = await startTrampolineServer(output, stdout, stderr)
server = serverInfo.server
baseEnv = {
DESKTOP_PORT: serverInfo.port,
}
}
afterEach(() => {
server.close()
})
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)
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 port = await startTrampolineServer()
const env = {
DESKTOP_PORT_FAKE: 32123,
DESKTOP_TRAMPOLINE_IDENTIFIER: '123456',
DESKTOP_PORT: port,
INVALID_VARIABLE: 'foo bar',
}
const opts = { env }
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`)
await run(trampolinePath, ['baz'], opts)
expect(output[output.length - 1]).toBe(
'This is a test\nWith a multiline\nStandard input text'
)
})
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(2)
expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=123456')
expect(outputEnv).toContain(`DESKTOP_PORT=${port}`)
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)
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')
})
})
})