sshifu 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -4
- package/README.md +0 -254
- package/bin/sshifu-server.js +0 -33
- package/bin/sshifu-trust.js +0 -33
- package/scripts/install.js +0 -272
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sshifu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SSH authentication system with short-lived certificates and OAuth authentication (GitHub organizations)",
|
|
5
5
|
"main": "bin/sshifu.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sshifu": "bin/sshifu.js"
|
|
8
|
-
"sshifu-server": "bin/sshifu-server.js",
|
|
9
|
-
"sshifu-trust": "bin/sshifu-trust.js"
|
|
7
|
+
"sshifu": "bin/sshifu.js"
|
|
10
8
|
},
|
|
11
9
|
"scripts": {
|
|
12
10
|
"postinstall": "node scripts/install.js"
|
package/README.md
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
# Sshifu
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
<img src="logo.png" alt="Sshifu Logo" width="250px">
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
**Sshifu** (SSH + Fu / 師傅 "master") is a lightweight SSH authentication system that uses short-lived OpenSSH certificates with OAuth authentication (GitHub organizations).
|
|
8
|
-
|
|
9
|
-
A minimal alternative to complex SSH access platforms like Teleport, while remaining fully compatible with existing OpenSSH tooling.
|
|
10
|
-
|
|
11
|
-
## Features
|
|
12
|
-
|
|
13
|
-
- 🔐 **Short-lived SSH certificates** - Automatic certificate issuance with configurable TTL (default 8 hours)
|
|
14
|
-
- 🌐 **GitHub OAuth authentication** - Authenticate users via GitHub organization membership
|
|
15
|
-
- 🛠️ **Standard OpenSSH compatibility** - Works with existing `ssh` command without workflow changes
|
|
16
|
-
- 📦 **Minimal infrastructure** - Single server component, no database required
|
|
17
|
-
- 👥 **Designed for small teams** - Optimized for teams with <50 users
|
|
18
|
-
|
|
19
|
-
## Quick Start
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# Expose your server publicly (here we're using localhost.run for testing)
|
|
23
|
-
ssh -R 80:localhost:8080 nokey@localhost.run
|
|
24
|
-
|
|
25
|
-
# Prepare GitHub OAuth credentials & note the localhost.run URL from above.
|
|
26
|
-
# In another terminal, start the sshifu server and follow the wizard.
|
|
27
|
-
npx sshifu-server
|
|
28
|
-
|
|
29
|
-
# On the SSH server machine, configure trust (requires sudo)
|
|
30
|
-
sudo npx sshifu-trust <localhost.run address>
|
|
31
|
-
|
|
32
|
-
# From another machine, connect to the SSH server
|
|
33
|
-
npx sshifu <localhost.run address> <ssh server address>
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Architecture
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
┌─────────────┐
|
|
40
|
-
│ User CLI │ sshifu
|
|
41
|
-
│ (sshifu) │
|
|
42
|
-
└──────┬──────┘
|
|
43
|
-
│
|
|
44
|
-
│ 1. Start login session
|
|
45
|
-
│ 2. Poll for approval
|
|
46
|
-
│ 3. Request certificate
|
|
47
|
-
▼
|
|
48
|
-
┌─────────────────────────┐
|
|
49
|
-
│ sshifu-server │
|
|
50
|
-
│ - OAuth gateway │
|
|
51
|
-
│ - SSH Certificate Auth │
|
|
52
|
-
└─────────────────────────┘
|
|
53
|
-
│
|
|
54
|
-
│ Configure trust
|
|
55
|
-
▼
|
|
56
|
-
┌─────────────────────────┐
|
|
57
|
-
│ Target SSH Server │
|
|
58
|
-
│ (configured via │
|
|
59
|
-
│ sshifu-trust) │
|
|
60
|
-
└─────────────────────────┘
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Components
|
|
64
|
-
|
|
65
|
-
| Tool | Purpose |
|
|
66
|
-
|------|---------|
|
|
67
|
-
| `sshifu` | CLI used by users to authenticate and connect to SSH servers |
|
|
68
|
-
| `sshifu-server` | Web server acting as OAuth gateway and SSH Certificate Authority |
|
|
69
|
-
| `sshifu-trust` | Server-side CLI to configure SSH servers to trust the Sshifu CA |
|
|
70
|
-
|
|
71
|
-
## Installation Options
|
|
72
|
-
|
|
73
|
-
#### Option 1: Install Globally via npm
|
|
74
|
-
|
|
75
|
-
For frequent use, install globally:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npm install -g sshifu
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Then use commands directly:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
sshifu auth.example.com user@target-server.com
|
|
85
|
-
sshifu-server
|
|
86
|
-
sshifu-trust auth.example.com
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
#### Option 2: Pre-built Binary
|
|
90
|
-
|
|
91
|
-
Download the latest release for your platform from the [releases page](https://github.com/azophy/sshifu/releases):
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
# Linux (amd64)
|
|
95
|
-
curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-linux-amd64.tar.gz | tar xz
|
|
96
|
-
sudo mv sshifu* /usr/local/bin/
|
|
97
|
-
|
|
98
|
-
# macOS (Intel)
|
|
99
|
-
curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-darwin-amd64.tar.gz | tar xz
|
|
100
|
-
sudo mv sshifu* /usr/local/bin/
|
|
101
|
-
|
|
102
|
-
# macOS (Apple Silicon)
|
|
103
|
-
curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-darwin-arm64.tar.gz | tar xz
|
|
104
|
-
sudo mv sshifu* /usr/local/bin/
|
|
105
|
-
|
|
106
|
-
# Windows (amd64)
|
|
107
|
-
curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-windows-amd64.zip -o sshifu.zip
|
|
108
|
-
unzip sshifu.zip
|
|
109
|
-
# Move sshifu.exe to a directory in your PATH
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
#### Option 3: Build from Source
|
|
113
|
-
|
|
114
|
-
Requires Go 1.25+:
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
go build ./cmd/sshifu
|
|
118
|
-
go build ./cmd/sshifu-server
|
|
119
|
-
go build ./cmd/sshifu-trust
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Getting GitHub OAuth Client ID & Secret
|
|
123
|
-
|
|
124
|
-
To configure OAuth authentication with GitHub, you need to create a GitHub OAuth App:
|
|
125
|
-
|
|
126
|
-
1. Go to your GitHub organization's settings: `https://github.com/organizations/<your-org>/settings`
|
|
127
|
-
2. Navigate to **Developer settings** (left sidebar)
|
|
128
|
-
3. Click **OAuth Apps** → **New OAuth App**
|
|
129
|
-
4. Fill in the application details:
|
|
130
|
-
- **Application name**: `sshifu` (or any descriptive name)
|
|
131
|
-
- **Homepage URL**: Your sshifu-server public URL (e.g., `https://auth.example.com`)
|
|
132
|
-
- **Authorization callback URL**: Same as your sshifu-server public URL (e.g., `https://auth.example.com`)
|
|
133
|
-
5. Click **Register application**
|
|
134
|
-
6. After registration, you'll see your **Client ID** - copy it
|
|
135
|
-
7. Click **Generate a new client secret** and copy the secret
|
|
136
|
-
|
|
137
|
-
> ⚠️ **Important**: The client secret is only shown once. Store it securely and never commit it to version control.
|
|
138
|
-
|
|
139
|
-
## Authentication Flow
|
|
140
|
-
|
|
141
|
-
```
|
|
142
|
-
┌──────────┐ ┌───────────────┐ ┌──────────┐
|
|
143
|
-
│ sshifu │ │ sshifu-server │ │ GitHub │
|
|
144
|
-
└────┬─────┘ └───────┬───────┘ └────┬─────┘
|
|
145
|
-
│ │ │
|
|
146
|
-
│ POST /api/v1/login/start │ │
|
|
147
|
-
│─────────────────────────────────>│ │
|
|
148
|
-
│ │ │
|
|
149
|
-
│ session_id, login_url │ │
|
|
150
|
-
│<─────────────────────────────────│ │
|
|
151
|
-
│ │ │
|
|
152
|
-
│ [Display login URL to user] │ │
|
|
153
|
-
│ │ │
|
|
154
|
-
│ │ [User opens URL in browser] │
|
|
155
|
-
│ │ │
|
|
156
|
-
│ │ Redirect to GitHub OAuth │
|
|
157
|
-
│ │─────────────────────────────────>│
|
|
158
|
-
│ │ │
|
|
159
|
-
│ │ User authenticates │
|
|
160
|
-
│ │<─────────────────────────────────│
|
|
161
|
-
│ │ │
|
|
162
|
-
│ │ Verify org membership │
|
|
163
|
-
│ │─────────────────────────────────>│
|
|
164
|
-
│ │ │
|
|
165
|
-
│ GET /api/v1/login/status │ Session approved │
|
|
166
|
-
│─────────────────────────────────>│ │
|
|
167
|
-
│ │ │
|
|
168
|
-
│ status: approved, access_token │ │
|
|
169
|
-
│<─────────────────────────────────│ │
|
|
170
|
-
│ │ │
|
|
171
|
-
│ POST /api/v1/sign/user │ │
|
|
172
|
-
│─────────────────────────────────>│ │
|
|
173
|
-
│ │ │
|
|
174
|
-
│ SSH certificate │ │
|
|
175
|
-
│<─────────────────────────────────│ │
|
|
176
|
-
│ │ │
|
|
177
|
-
│ ssh -o CertificateFile=<cert> │ │
|
|
178
|
-
│─────────────────────────────────>│ │
|
|
179
|
-
│ [SSH connection established] │
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Configuration
|
|
183
|
-
|
|
184
|
-
### Server Configuration
|
|
185
|
-
|
|
186
|
-
| Field | Description | Default |
|
|
187
|
-
|-------|-------------|---------|
|
|
188
|
-
| `server.listen` | Address to listen on | `:8080` |
|
|
189
|
-
| `server.public_url` | Public URL of the server | Required |
|
|
190
|
-
| `ca.private_key` | Path to CA private key | `./ca` |
|
|
191
|
-
| `ca.public_key` | Path to CA public key | `./ca.pub` |
|
|
192
|
-
| `cert.ttl` | Certificate time-to-live | `8h` |
|
|
193
|
-
| `auth.providers` | OAuth provider configurations | Required |
|
|
194
|
-
|
|
195
|
-
### Certificate Extensions
|
|
196
|
-
|
|
197
|
-
By default, issued certificates include:
|
|
198
|
-
- `permit-pty` - Allow pseudo-terminal allocation
|
|
199
|
-
- `permit-port-forwarding` - Allow TCP port forwarding
|
|
200
|
-
- `permit-agent-forwarding` - Allow SSH agent forwarding
|
|
201
|
-
- `permit-x11-forwarding` - Allow X11 forwarding
|
|
202
|
-
|
|
203
|
-
## Project Structure
|
|
204
|
-
|
|
205
|
-
```
|
|
206
|
-
sshifu/
|
|
207
|
-
├── cmd/
|
|
208
|
-
│ ├── sshifu/ # User CLI
|
|
209
|
-
│ ├── sshifu-server/ # Server component
|
|
210
|
-
│ └── sshifu-trust/ # Server setup tool
|
|
211
|
-
├── internal/
|
|
212
|
-
│ ├── api/ # HTTP API handlers
|
|
213
|
-
│ ├── cert/ # SSH certificate operations
|
|
214
|
-
│ ├── config/ # Configuration loading
|
|
215
|
-
│ ├── oauth/ # OAuth provider implementations
|
|
216
|
-
│ ├── session/ # Login session management
|
|
217
|
-
│ └── ssh/ # SSH utilities
|
|
218
|
-
├── web/ # Web frontend (login pages)
|
|
219
|
-
├── config.example.yml # Example configuration
|
|
220
|
-
└── go.mod
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Security Considerations
|
|
224
|
-
|
|
225
|
-
- **Short-lived certificates** reduce the impact of compromised keys
|
|
226
|
-
- **CA private key** should be stored securely on the server
|
|
227
|
-
- **GitHub organization membership** is verified on each login
|
|
228
|
-
- **No long-term secrets** stored on client machines
|
|
229
|
-
- **Transparent authorization** - the target server's OS determines final access permissions
|
|
230
|
-
|
|
231
|
-
## Limitations (v1)
|
|
232
|
-
|
|
233
|
-
The following features are intentionally out of scope for the initial release:
|
|
234
|
-
|
|
235
|
-
- Role-based access control (RBAC)
|
|
236
|
-
- Server access policies
|
|
237
|
-
- Automatic account provisioning on SSH servers
|
|
238
|
-
- Session recording or audit logging
|
|
239
|
-
- Admin dashboard
|
|
240
|
-
- Certificate revocation
|
|
241
|
-
|
|
242
|
-
## Requirements
|
|
243
|
-
|
|
244
|
-
- OpenSSH 6.7+ (for certificate support)
|
|
245
|
-
- Linux/Unix-like operating system (server components)
|
|
246
|
-
- Windows, macOS, or Linux (client CLI)
|
|
247
|
-
|
|
248
|
-
## License
|
|
249
|
-
|
|
250
|
-
[MIT License](LICENSE)
|
|
251
|
-
|
|
252
|
-
## Contributing
|
|
253
|
-
|
|
254
|
-
Contributions are welcome! Please read the contributing guidelines before submitting pull requests.
|
package/bin/sshifu-server.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* sshifu-server CLI wrapper
|
|
5
|
-
* Executes the sshifu-server binary from the bin directory
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { spawn } = require('child_process');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
|
|
12
|
-
const binName = process.platform === 'win32' ? 'sshifu-server.exe' : 'sshifu-server';
|
|
13
|
-
const binPath = path.join(__dirname, binName);
|
|
14
|
-
|
|
15
|
-
if (!fs.existsSync(binPath)) {
|
|
16
|
-
console.error(`Error: sshifu-server binary not found at ${binPath}`);
|
|
17
|
-
console.error('The postinstall script may have failed. Try running:');
|
|
18
|
-
console.error(' npm rebuild sshifu');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const child = spawn(binPath, process.argv.slice(2), {
|
|
23
|
-
stdio: 'inherit',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
child.on('error', (err) => {
|
|
27
|
-
console.error(`Failed to start sshifu-server: ${err.message}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
child.on('close', (code) => {
|
|
32
|
-
process.exit(code);
|
|
33
|
-
});
|
package/bin/sshifu-trust.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* sshifu-trust CLI wrapper
|
|
5
|
-
* Executes the sshifu-trust binary from the bin directory
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { spawn } = require('child_process');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
|
|
12
|
-
const binName = process.platform === 'win32' ? 'sshifu-trust.exe' : 'sshifu-trust';
|
|
13
|
-
const binPath = path.join(__dirname, binName);
|
|
14
|
-
|
|
15
|
-
if (!fs.existsSync(binPath)) {
|
|
16
|
-
console.error(`Error: sshifu-trust binary not found at ${binPath}`);
|
|
17
|
-
console.error('The postinstall script may have failed. Try running:');
|
|
18
|
-
console.error(' npm rebuild sshifu');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const child = spawn(binPath, process.argv.slice(2), {
|
|
23
|
-
stdio: 'inherit',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
child.on('error', (err) => {
|
|
27
|
-
console.error(`Failed to start sshifu-trust: ${err.message}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
child.on('close', (code) => {
|
|
32
|
-
process.exit(code);
|
|
33
|
-
});
|
package/scripts/install.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postinstall script for sshifu
|
|
5
|
-
* Downloads the appropriate binary for the current platform from GitHub Releases
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const https = require('https');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const zlib = require('zlib');
|
|
13
|
-
const { execSync } = require('child_process');
|
|
14
|
-
|
|
15
|
-
const PACKAGE_NAME = 'sshifu';
|
|
16
|
-
const BINARIES = ['sshifu', 'sshifu-server', 'sshifu-trust'];
|
|
17
|
-
const DOWNLOAD_TIMEOUT = 60000; // 60 seconds
|
|
18
|
-
|
|
19
|
-
// Map npm platform/arch to Go GOOS/GOARCH
|
|
20
|
-
const PLATFORM_MAP = {
|
|
21
|
-
'linux-x64': { os: 'linux', arch: 'amd64' },
|
|
22
|
-
'linux-arm64': { os: 'linux', arch: 'arm64' },
|
|
23
|
-
'linux-arm': { os: 'linux', arch: 'arm' },
|
|
24
|
-
'darwin-x64': { os: 'darwin', arch: 'amd64' },
|
|
25
|
-
'darwin-arm64': { os: 'darwin', arch: 'arm64' },
|
|
26
|
-
'win32-x64': { os: 'windows', arch: 'amd64' },
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function getPlatformInfo() {
|
|
30
|
-
const npmPlatform = `${process.platform}-${process.arch}`;
|
|
31
|
-
const mapped = PLATFORM_MAP[npmPlatform];
|
|
32
|
-
|
|
33
|
-
if (!mapped) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`Unsupported platform: ${npmPlatform}\n` +
|
|
36
|
-
`Supported platforms: ${Object.keys(PLATFORM_MAP).join(', ')}`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return mapped;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getBinaryExtension() {
|
|
44
|
-
return process.platform === 'win32' ? '.exe' : '';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getArchiveExtension() {
|
|
48
|
-
return process.platform === 'win32' ? '.zip' : '.tar.gz';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getDownloadUrl(version, platformInfo) {
|
|
52
|
-
const ext = getArchiveExtension();
|
|
53
|
-
const archiveName = `${PACKAGE_NAME}-${platformInfo.os}-${platformInfo.arch}${ext}`;
|
|
54
|
-
return `https://github.com/azophy/sshifu/releases/download/${version}/${archiveName}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getLatestVersion() {
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
const options = {
|
|
60
|
-
hostname: 'api.github.com',
|
|
61
|
-
path: '/repos/azophy/sshifu/releases/latest',
|
|
62
|
-
method: 'GET',
|
|
63
|
-
headers: {
|
|
64
|
-
'User-Agent': 'sshifu-npm-installer',
|
|
65
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const req = https.get(options, (res) => {
|
|
70
|
-
let data = '';
|
|
71
|
-
|
|
72
|
-
res.on('data', (chunk) => {
|
|
73
|
-
data += chunk;
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
res.on('end', () => {
|
|
77
|
-
if (res.statusCode === 200) {
|
|
78
|
-
try {
|
|
79
|
-
const parsed = JSON.parse(data);
|
|
80
|
-
resolve(parsed.tag_name || parsed.name);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
reject(new Error('Failed to parse GitHub API response'));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
reject(new Error(`GitHub API returned status ${res.statusCode}`));
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
req.on('error', reject);
|
|
91
|
-
req.on('timeout', () => {
|
|
92
|
-
req.destroy();
|
|
93
|
-
reject(new Error('Request timed out'));
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function downloadFile(url, totalBytes = 0) {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
const options = {
|
|
101
|
-
headers: {
|
|
102
|
-
'User-Agent': 'sshifu-npm-installer',
|
|
103
|
-
},
|
|
104
|
-
timeout: DOWNLOAD_TIMEOUT,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
https.get(url, options, (res) => {
|
|
108
|
-
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
109
|
-
// Follow redirect
|
|
110
|
-
downloadFile(res.headers.location, totalBytes).then(resolve).catch(reject);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (res.statusCode !== 200) {
|
|
115
|
-
reject(new Error(`Download failed with status ${res.statusCode}`));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const chunks = [];
|
|
120
|
-
let downloaded = 0;
|
|
121
|
-
|
|
122
|
-
res.on('data', (chunk) => {
|
|
123
|
-
downloaded += chunk.length;
|
|
124
|
-
chunks.push(chunk);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
res.on('end', () => {
|
|
128
|
-
resolve(Buffer.concat(chunks));
|
|
129
|
-
});
|
|
130
|
-
}).on('error', reject)
|
|
131
|
-
.on('timeout', () => {
|
|
132
|
-
reject(new Error('Download timed out'));
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function extractArchive(archiveData, destDir) {
|
|
138
|
-
const ext = getArchiveExtension();
|
|
139
|
-
|
|
140
|
-
if (ext === '.tar.gz') {
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
const tar = require('child_process').spawn('tar', ['xzf', '-', '-C', destDir], {
|
|
143
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
tar.on('close', (code) => {
|
|
147
|
-
if (code === 0) resolve();
|
|
148
|
-
else reject(new Error(`tar exited with code ${code}`));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
tar.stdin.write(archiveData);
|
|
152
|
-
tar.stdin.end();
|
|
153
|
-
});
|
|
154
|
-
} else if (ext === '.zip') {
|
|
155
|
-
// For Windows, write to temp file and extract
|
|
156
|
-
const tempFile = path.join(destDir, 'temp.zip');
|
|
157
|
-
fs.writeFileSync(tempFile, archiveData);
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
// Try PowerShell first
|
|
161
|
-
execSync(`powershell -command "Expand-Archive -Path '${tempFile}' -DestinationPath '${destDir}' -Force"`, {
|
|
162
|
-
stdio: 'inherit',
|
|
163
|
-
});
|
|
164
|
-
fs.unlinkSync(tempFile);
|
|
165
|
-
return Promise.resolve();
|
|
166
|
-
} catch (e) {
|
|
167
|
-
// Fallback to other methods if needed
|
|
168
|
-
fs.unlinkSync(tempFile);
|
|
169
|
-
return Promise.reject(new Error('Failed to extract zip archive'));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return Promise.reject(new Error(`Unsupported archive format: ${ext}`));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function makeExecutable(filePath) {
|
|
177
|
-
if (process.platform !== 'win32') {
|
|
178
|
-
fs.chmodSync(filePath, 0o755);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function install() {
|
|
183
|
-
console.log(`🔐 ${PACKAGE_NAME} installer`);
|
|
184
|
-
console.log('========================\n');
|
|
185
|
-
|
|
186
|
-
const platformInfo = getPlatformInfo();
|
|
187
|
-
const binExt = getBinaryExtension();
|
|
188
|
-
|
|
189
|
-
console.log(`Platform: ${process.platform}-${process.arch}`);
|
|
190
|
-
console.log(`Mapped to: ${platformInfo.os}-${platformInfo.arch}\n`);
|
|
191
|
-
|
|
192
|
-
// Determine version
|
|
193
|
-
let version;
|
|
194
|
-
try {
|
|
195
|
-
version = process.env.SSHIFU_VERSION || await getLatestVersion();
|
|
196
|
-
} catch (e) {
|
|
197
|
-
console.error(`Failed to get version: ${e.message}`);
|
|
198
|
-
console.error('\nFalling back to latest release...');
|
|
199
|
-
version = 'v0.2.2';
|
|
200
|
-
}
|
|
201
|
-
console.log(`Version: ${version}`);
|
|
202
|
-
|
|
203
|
-
// Get download URL
|
|
204
|
-
const downloadUrl = getDownloadUrl(version, platformInfo);
|
|
205
|
-
console.log(`Downloading from: ${downloadUrl}\n`);
|
|
206
|
-
|
|
207
|
-
// Download archive
|
|
208
|
-
console.log('Downloading archive...');
|
|
209
|
-
let archiveData;
|
|
210
|
-
try {
|
|
211
|
-
archiveData = await downloadFile(downloadUrl);
|
|
212
|
-
console.log(`Downloaded ${Math.round(archiveData.length / 1024)} KB`);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
console.error(`Failed to download: ${e.message}`);
|
|
215
|
-
console.error('\nMake sure you have a stable internet connection.');
|
|
216
|
-
console.error('If the problem persists, download manually from:');
|
|
217
|
-
console.error('https://github.com/azophy/sshifu/releases\n');
|
|
218
|
-
console.error('Then extract to: ' + path.join(__dirname, '..', 'bin'));
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Create bin directory
|
|
223
|
-
const binDir = path.join(__dirname, '..', 'bin');
|
|
224
|
-
if (!fs.existsSync(binDir)) {
|
|
225
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create temp directory for extraction
|
|
229
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${PACKAGE_NAME}-`));
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
// Extract archive
|
|
233
|
-
console.log('Extracting binaries...');
|
|
234
|
-
await extractArchive(archiveData, tempDir);
|
|
235
|
-
|
|
236
|
-
// Move binaries to bin directory
|
|
237
|
-
console.log('Installing binaries...');
|
|
238
|
-
for (const binary of BINARIES) {
|
|
239
|
-
const srcName = binary + binExt;
|
|
240
|
-
const srcPath = path.join(tempDir, srcName);
|
|
241
|
-
const destPath = path.join(binDir, srcName);
|
|
242
|
-
|
|
243
|
-
if (fs.existsSync(srcPath)) {
|
|
244
|
-
fs.copyFileSync(srcPath, destPath);
|
|
245
|
-
makeExecutable(destPath);
|
|
246
|
-
console.log(` ✓ ${binary}${binExt}`);
|
|
247
|
-
} else {
|
|
248
|
-
console.warn(` ⚠ ${binary}${binExt} not found in archive`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
console.log('\n✅ Installation complete!');
|
|
253
|
-
console.log(`\nYou can now run:`);
|
|
254
|
-
console.log(` ${BINARIES.map(b => b + binExt).join(', ')}`);
|
|
255
|
-
console.log(`\nOr use npx:`);
|
|
256
|
-
console.log(` npx sshifu <server> <target>`);
|
|
257
|
-
|
|
258
|
-
} catch (e) {
|
|
259
|
-
console.error(`Installation failed: ${e.message}`);
|
|
260
|
-
console.error('\nStack:', e.stack);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
} finally {
|
|
263
|
-
// Cleanup temp directory
|
|
264
|
-
try {
|
|
265
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
266
|
-
} catch (e) {
|
|
267
|
-
// Ignore cleanup errors
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
install();
|