Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63974e724 | ||
|
|
0882b7b0ca | ||
|
|
85f844f519 | ||
|
|
bf04082571 |
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
@@ -13,16 +13,15 @@ jobs:
|
|||||||
name: ${{ matrix.friendlyName }}
|
name: ${{ matrix.friendlyName }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node: [14.15.4]
|
node: [12.14.1]
|
||||||
os: [macos-latest, windows-latest, ubuntu-18.04]
|
os: [macos-10.14, windows-2019, ubuntu-18.04]
|
||||||
include:
|
include:
|
||||||
- os: macos-latest
|
- os: macos-10.14
|
||||||
friendlyName: macOS
|
friendlyName: macOS
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
friendlyName: Windows
|
friendlyName: Windows
|
||||||
- os: ubuntu-18.04
|
- os: ubuntu-18.04
|
||||||
friendlyName: Linux
|
friendlyName: Linux
|
||||||
@@ -34,33 +33,19 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
# This step can be removed as soon as official Windows arm64 builds are published:
|
|
||||||
# https://github.com/nodejs/build/issues/2450#issuecomment-705853342
|
|
||||||
- run: |
|
|
||||||
$NodeVersion = (node --version) -replace '^.'
|
|
||||||
$NodeFallbackVersion = "15.8.0"
|
|
||||||
& .\script\download-node-lib-win-arm64.ps1 $NodeVersion $NodeFallbackVersion
|
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
name: Install Windows arm64 node.lib
|
|
||||||
|
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: yarn
|
||||||
yarn install
|
|
||||||
yarn build
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Prebuild (x64)
|
- name: Prebuild Node x64
|
||||||
run: npm run prebuild-napi-x64
|
run: yarn prebuild-node
|
||||||
- name: Prebuild (arm64)
|
- name: Prebuild Electron x64
|
||||||
run: npm run prebuild-napi-arm64
|
run: yarn prebuild-electron
|
||||||
if: ${{ matrix.os != 'ubuntu-18.04' }}
|
- name: Prebuild Electron arm64
|
||||||
- name: Prebuild (Windows x86)
|
run: yarn prebuild-electron-arm64
|
||||||
run: npm run prebuild-napi-ia32
|
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
run: yarn upload
|
run: yarn upload
|
||||||
|
|||||||
70
.github/workflows/codeql-analysis.yml
vendored
70
.github/workflows/codeql-analysis.yml
vendored
@@ -1,70 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ main ]
|
|
||||||
schedule:
|
|
||||||
- cron: '45 23 * * 1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'cpp', 'javascript', 'python' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
14
README.md
14
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A cross-platform no-dependency C executable trampoline which lets GitHub Desktop
|
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
|
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
|
The intention is to support the same platforms that
|
||||||
[Electron supports](https://www.electronjs.org/docs/tutorial/support#supported-platforms).
|
[Electron supports](https://www.electronjs.org/docs/tutorial/support#supported-platforms).
|
||||||
@@ -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
|
that TCP socket, the implementation for every possible protocol like
|
||||||
`GIT_ASKPASS` can live within the GitHub Desktop codebase instead of having
|
`GIT_ASKPASS` can live within the GitHub Desktop codebase instead of having
|
||||||
multiple trampoline executables.
|
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.
|
|
||||||
|
|||||||
59
binding.gyp
59
binding.gyp
@@ -2,16 +2,12 @@
|
|||||||
'targets': [
|
'targets': [
|
||||||
{
|
{
|
||||||
'target_name': 'desktop-trampoline',
|
'target_name': 'desktop-trampoline',
|
||||||
'defines': [
|
|
||||||
"NAPI_VERSION=<(napi_build_version)",
|
|
||||||
],
|
|
||||||
'type': 'executable',
|
'type': 'executable',
|
||||||
'sources': [
|
'sources': [
|
||||||
'src/desktop-trampoline.c',
|
'src/desktop-trampoline.c',
|
||||||
'src/socket.c'
|
'src/socket.c'
|
||||||
],
|
],
|
||||||
'include_dirs': [
|
'include_dirs': [
|
||||||
'<!(node -p "require(\'node-addon-api\').include_dir")',
|
|
||||||
'include'
|
'include'
|
||||||
],
|
],
|
||||||
'xcode_settings': {
|
'xcode_settings': {
|
||||||
@@ -31,17 +27,12 @@
|
|||||||
'-pie',
|
'-pie',
|
||||||
'-D_FORTIFY_SOURCE=1',
|
'-D_FORTIFY_SOURCE=1',
|
||||||
'-fstack-protector-strong',
|
'-fstack-protector-strong',
|
||||||
'-Werror=format-security',
|
'-Werror=format-security'
|
||||||
'-fno-exceptions'
|
|
||||||
],
|
],
|
||||||
'cflags_cc!': [ '-fno-exceptions' ],
|
|
||||||
'ldflags!': [
|
'ldflags!': [
|
||||||
'-z relro',
|
'-z relro',
|
||||||
'-z now'
|
'-z now'
|
||||||
],
|
],
|
||||||
'msvs_settings': {
|
|
||||||
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
|
|
||||||
},
|
|
||||||
'conditions': [
|
'conditions': [
|
||||||
['OS=="win"', {
|
['OS=="win"', {
|
||||||
'defines': [ 'WINDOWS' ],
|
'defines': [ 'WINDOWS' ],
|
||||||
@@ -51,53 +42,5 @@
|
|||||||
}]
|
}]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'target_name': 'ssh-wrapper',
|
|
||||||
'defines': [
|
|
||||||
"NAPI_VERSION=<(napi_build_version)",
|
|
||||||
],
|
|
||||||
'type': 'executable',
|
|
||||||
'sources': [
|
|
||||||
'src/ssh-wrapper.c'
|
|
||||||
],
|
|
||||||
'include_dirs': [
|
|
||||||
'<!(node -p "require(\'node-addon-api\').include_dir")',
|
|
||||||
'include'
|
|
||||||
],
|
|
||||||
'xcode_settings': {
|
|
||||||
'OTHER_CFLAGS': [
|
|
||||||
'-Wall',
|
|
||||||
'-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',
|
|
||||||
'-fno-exceptions'
|
|
||||||
],
|
|
||||||
'cflags_cc!': [ '-fno-exceptions' ],
|
|
||||||
'ldflags!': [
|
|
||||||
'-z relro',
|
|
||||||
'-z now'
|
|
||||||
],
|
|
||||||
'msvs_settings': {
|
|
||||||
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
|
|
||||||
},
|
|
||||||
'conditions': [
|
|
||||||
# For now only build it for macOS, since it's not needed on Windows
|
|
||||||
['OS=="win"', {
|
|
||||||
'defines': [ 'WINDOWS' ],
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
3
index.d.ts
vendored
3
index.d.ts
vendored
@@ -1,5 +1,2 @@
|
|||||||
export function getDesktopTrampolinePath(): string
|
export function getDesktopTrampolinePath(): string
|
||||||
export function getDesktopTrampolineFilename(): string
|
export function getDesktopTrampolineFilename(): string
|
||||||
|
|
||||||
export function getSSHWrapperPath(): string
|
|
||||||
export function getSSHWrapperFilename(): string
|
|
||||||
|
|||||||
10
index.js
10
index.js
@@ -15,17 +15,7 @@ function getDesktopTrampolineFilename() {
|
|||||||
: 'desktop-trampoline'
|
: 'desktop-trampoline'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSSHWrapperPath() {
|
|
||||||
return Path.join(__dirname, 'build', 'Release', getSSHWrapperFilename())
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSSHWrapperFilename() {
|
|
||||||
return process.platform === 'win32' ? 'ssh-wrapper.exe' : 'ssh-wrapper'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getDesktopTrampolinePath,
|
getDesktopTrampolinePath,
|
||||||
getDesktopTrampolineFilename,
|
getDesktopTrampolineFilename,
|
||||||
getSSHWrapperPath,
|
|
||||||
getSSHWrapperFilename,
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
package.json
33
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desktop-trampoline",
|
"name": "desktop-trampoline",
|
||||||
"version": "0.9.8",
|
"version": "0.9.4",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "prettier -c **/*.js **/*.md",
|
"lint": "prettier -c **/*.js **/*.md",
|
||||||
"lint:fix": "prettier --write **/*.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-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-napi-ia32": "prebuild -t 3 -r napi -a ia32 --strip --include-regex \"(desktop-trampoline|ssh-wrapper)(\\.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-napi-arm64": "prebuild -t 3 -r napi -a arm64 --strip --include-regex \"(desktop-trampoline|ssh-wrapper)(\\.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-napi-x64 && yarn prebuild-napi-ia32 && yarn prebuild-napi-arm64",
|
"prebuild-all": "yarn prebuild-node && yarn prebuild-electron && yarn prebuild-electron-arm64",
|
||||||
"upload": "node ./script/upload.js"
|
"upload": "node ./script/upload.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -29,24 +29,11 @@
|
|||||||
"url": "https://github.com/desktop/desktop-trampoline/issues"
|
"url": "https://github.com/desktop/desktop-trampoline/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/desktop/desktop-trampoline#readme",
|
"homepage": "https://github.com/desktop/desktop-trampoline#readme",
|
||||||
"dependencies": {
|
|
||||||
"node-addon-api": "^4.3.0",
|
|
||||||
"prebuild-install": "^7.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^27.5.0",
|
"jest": "^26.4.2",
|
||||||
"node-gyp": "^8.4.1",
|
"node-gyp": "^7.1.0",
|
||||||
"prebuild": "^11.0.3",
|
"prebuild": "^10.0.1",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.1.2",
|
||||||
"split2": "^4.1.0"
|
"split2": "^3.2.2"
|
||||||
},
|
|
||||||
"binary": {
|
|
||||||
"napi_versions": [
|
|
||||||
3
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"runtime": "napi",
|
|
||||||
"target": 3
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
# This script can be removed as soon as official Windows arm64 builds are published:
|
|
||||||
# https://github.com/nodejs/build/issues/2450#issuecomment-705853342
|
|
||||||
|
|
||||||
$nodeVersion = $args[0]
|
|
||||||
$fallbackVersion = $args[1]
|
|
||||||
|
|
||||||
If ($null -eq $nodeVersion -Or $null -eq $fallbackVersion) {
|
|
||||||
Write-Error "No NodeJS version given as argument to this file. Run it like download-nodejs-win-arm64.ps1 NODE_VERSION NODE_FALLBACK_VERSION"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = "https://unofficial-builds.nodejs.org/download/release/v$nodeVersion/win-arm64/node.lib"
|
|
||||||
$fallbackUrl = "https://unofficial-builds.nodejs.org/download/release/v$fallbackVersion/win-arm64/node.lib"
|
|
||||||
|
|
||||||
# Always write to the $nodeVersion cache folder, even if we're using the fallbackVersion
|
|
||||||
$cacheFolder = "$env:TEMP\prebuild\napi\$nodeVersion\arm64"
|
|
||||||
|
|
||||||
If (!(Test-Path $cacheFolder)) {
|
|
||||||
New-Item -ItemType Directory -Force -Path $cacheFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = "$cacheFolder\node.lib"
|
|
||||||
$start_time = Get-Date
|
|
||||||
|
|
||||||
Try {
|
|
||||||
Invoke-WebRequest -Uri $url -OutFile $output
|
|
||||||
$downloadedNodeVersion = $nodeVersion
|
|
||||||
} Catch {
|
|
||||||
If ($_.Exception.Response -And $_.Exception.Response.StatusCode -eq "NotFound") {
|
|
||||||
Write-Output "No arm64 node.lib found for Node Windows $nodeVersion, trying fallback version $fallbackVersion..."
|
|
||||||
Invoke-WebRequest -Uri $fallbackUrl -OutFile $output
|
|
||||||
$downloadedNodeVersion = $fallbackVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "Downloaded arm64 NodeJS lib v$downloadedNodeVersion to $output in $((Get-Date).Subtract($start_time).Seconds) second(s)"
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -44,6 +46,42 @@ int isValidEnvVar(char *env) {
|
|||||||
return 0;
|
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) {
|
int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
|
||||||
char *desktopPortString;
|
char *desktopPortString;
|
||||||
|
|
||||||
@@ -102,29 +140,57 @@ int runTrampolineClient(SOCKET *outSocket, int argc, char **argv, char **envp) {
|
|||||||
WRITE_STRING_OR_EXIT("environment variable", validEnvVars[idx]);
|
WRITE_STRING_OR_EXIT("environment variable", validEnvVars[idx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: send stdin stuff?
|
|
||||||
|
|
||||||
char buffer[BUFFER_LENGTH + 1];
|
char buffer[BUFFER_LENGTH + 1];
|
||||||
size_t totalBytesRead = 0;
|
size_t totalBytesWritten = 0;
|
||||||
ssize_t bytesRead = 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 {
|
do {
|
||||||
bytesRead = readSocket(socket, buffer + totalBytesRead, BUFFER_LENGTH - totalBytesRead);
|
bytesToWrite = read(0, buffer, BUFFER_LENGTH);
|
||||||
|
|
||||||
if (bytesRead == -1) {
|
if (bytesToWrite == -1) {
|
||||||
printSocketError("ERROR: Error reading from socket");
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalBytesRead += bytesRead;
|
totalBytesWritten += bytesToWrite;
|
||||||
} while (bytesRead > 0);
|
} 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
|
// Write that output to stdout
|
||||||
fprintf(stdout, "%s", buffer);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
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'))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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)
|
|
||||||
expect(outputEnv).toHaveLength(2)
|
|
||||||
expect(outputEnv).toContain('DESKTOP_TRAMPOLINE_IDENTIFIER=123456')
|
|
||||||
expect(outputEnv).toContain(`DESKTOP_USERNAME=sergiou87`)
|
|
||||||
|
|
||||||
server.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
183
test/index.test.js
Normal file
183
test/index.test.js
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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/
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user