vpn-tool-cli 1.0.1
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.
- checksums.yaml +7 -0
- data/README.md +25 -0
- data/bin/vpn-tool +9 -0
- data/lib/vpn-tool.rb +806 -0
- data/vpn-tool.gemspec +25 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cacab01f3fba575403bbef54bfd9c87dcf469c4f240d0016222134f86094beed
|
|
4
|
+
data.tar.gz: 6edecd43b7aa01d9b6fea40bef0220aee2641a5e214093a8b7133f1f2f98a169
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e70a31060db2057413acb51ae02349ffd58bfa6547fc316f420aae97c69240411d82aceb05254aa2f2b7a16ab4d1c586746231211ca00dced035765a72e43a40
|
|
7
|
+
data.tar.gz: 5cd147c4b208c79947ababd730c3f84bc759aef28b7628a265cdb0261f15380905e327a567d12e7a85646cd822a377f395f2bf0af37c3a8b79cce943d4a0bf59
|
data/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# vpn-tool
|
|
2
|
+
|
|
3
|
+
Ruby CLI for VPN gateway management.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install vpn-tool
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
vpn-tool --version
|
|
15
|
+
vpn-tool auth register
|
|
16
|
+
vpn-tool auth login
|
|
17
|
+
vpn-tool auth list
|
|
18
|
+
vpn-tool start --daemon
|
|
19
|
+
vpn-tool status --daemon
|
|
20
|
+
vpn-tool countries
|
|
21
|
+
vpn-tool connect --country US
|
|
22
|
+
vpn-tool disconnect
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Repository: https://github.com/jjjm03299-wq/vpn_tool
|
data/bin/vpn-tool
ADDED
data/lib/vpn-tool.rb
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "commander-tool"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "socket"
|
|
9
|
+
|
|
10
|
+
module VpnTool
|
|
11
|
+
BASE_URL = "https://vpn-data-cleaner-api--hb044082.replit.app"
|
|
12
|
+
VERSION = "1.0.0"
|
|
13
|
+
|
|
14
|
+
CONFIG_DIR = File.join(Dir.home, ".vpn-tool")
|
|
15
|
+
PIN_FILE = File.join(CONFIG_DIR, "pin")
|
|
16
|
+
SESSION_FILE = File.join(CONFIG_DIR, "session")
|
|
17
|
+
DAEMON_PID_FILE = File.join(CONFIG_DIR, "daemon.pid")
|
|
18
|
+
|
|
19
|
+
DAEMON_HOST = "127.0.0.1"
|
|
20
|
+
DAEMON_PORT = 5900
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def ensure_config
|
|
25
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
26
|
+
File.chmod(0o700, CONFIG_DIR)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def file_exists?(path)
|
|
30
|
+
File.file?(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def read_file(path)
|
|
34
|
+
return nil unless file_exists?(path)
|
|
35
|
+
|
|
36
|
+
File.read(path).strip
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write_file(path, value)
|
|
40
|
+
ensure_config
|
|
41
|
+
|
|
42
|
+
File.write(path, value)
|
|
43
|
+
File.chmod(0o600, path)
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
warn "Error: #{e.message}"
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def remove_file(path)
|
|
52
|
+
File.delete(path) if File.exist?(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def valid_pin?(pin)
|
|
56
|
+
pin && pin.match?(/\A\d{4}\z/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prompt(message)
|
|
60
|
+
print message
|
|
61
|
+
$stdout.flush
|
|
62
|
+
STDIN.gets&.chomp
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ----------------------------------------------------------
|
|
66
|
+
# Authentication
|
|
67
|
+
# ----------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def require_login
|
|
70
|
+
return true if file_exists?(SESSION_FILE)
|
|
71
|
+
|
|
72
|
+
puts "Not logged in."
|
|
73
|
+
puts "Run: vpn-tool auth login"
|
|
74
|
+
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_pin(message)
|
|
79
|
+
unless file_exists?(PIN_FILE)
|
|
80
|
+
puts "No PIN registered."
|
|
81
|
+
puts "Run: vpn-tool auth register"
|
|
82
|
+
return false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
pin = prompt(message)
|
|
86
|
+
saved_pin = read_file(PIN_FILE)
|
|
87
|
+
|
|
88
|
+
unless pin == saved_pin
|
|
89
|
+
puts "Incorrect PIN."
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ----------------------------------------------------------
|
|
97
|
+
# Daemon
|
|
98
|
+
# ----------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def daemon_pid
|
|
101
|
+
value = read_file(DAEMON_PID_FILE)
|
|
102
|
+
|
|
103
|
+
return nil unless value&.match?(/\A\d+\z/)
|
|
104
|
+
|
|
105
|
+
value.to_i
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def daemon_running?
|
|
109
|
+
pid = daemon_pid
|
|
110
|
+
|
|
111
|
+
return false unless pid && pid > 0
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
Process.kill(0, pid)
|
|
115
|
+
true
|
|
116
|
+
rescue Errno::ESRCH
|
|
117
|
+
false
|
|
118
|
+
rescue Errno::EPERM
|
|
119
|
+
true
|
|
120
|
+
rescue StandardError
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def daemon_connection
|
|
126
|
+
TCPSocket.new(
|
|
127
|
+
DAEMON_HOST,
|
|
128
|
+
DAEMON_PORT
|
|
129
|
+
)
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def require_daemon
|
|
135
|
+
socket = daemon_connection
|
|
136
|
+
|
|
137
|
+
if socket
|
|
138
|
+
socket.close
|
|
139
|
+
return true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
puts "Cannot connect to daemon server."
|
|
143
|
+
puts "Run: vpn-tool start --daemon"
|
|
144
|
+
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def start_daemon
|
|
149
|
+
ensure_config
|
|
150
|
+
|
|
151
|
+
if daemon_running?
|
|
152
|
+
puts "Daemon already running (PID #{daemon_pid})."
|
|
153
|
+
puts "TCP: tcp://#{DAEMON_HOST}:#{DAEMON_PORT}"
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
remove_file(DAEMON_PID_FILE)
|
|
158
|
+
|
|
159
|
+
pid = fork do
|
|
160
|
+
Process.setsid
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
server = TCPServer.new(
|
|
164
|
+
DAEMON_HOST,
|
|
165
|
+
DAEMON_PORT
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
write_file(
|
|
169
|
+
DAEMON_PID_FILE,
|
|
170
|
+
Process.pid.to_s
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
loop do
|
|
174
|
+
client = server.accept
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
request = client.gets&.chomp
|
|
178
|
+
|
|
179
|
+
response =
|
|
180
|
+
case request
|
|
181
|
+
when "PING", "STATUS"
|
|
182
|
+
{
|
|
183
|
+
status: "running",
|
|
184
|
+
service: "vpn-tool-daemon",
|
|
185
|
+
version: VERSION,
|
|
186
|
+
pid: Process.pid,
|
|
187
|
+
host: DAEMON_HOST,
|
|
188
|
+
port: DAEMON_PORT
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
{
|
|
192
|
+
status: "ok",
|
|
193
|
+
service: "vpn-tool-daemon",
|
|
194
|
+
version: VERSION
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
client.puts(
|
|
199
|
+
JSON.generate(response)
|
|
200
|
+
)
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
client.puts(
|
|
203
|
+
JSON.generate(
|
|
204
|
+
status: "error",
|
|
205
|
+
message: e.message
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
ensure
|
|
209
|
+
client.close
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
warn "Daemon error: #{e.message}"
|
|
214
|
+
ensure
|
|
215
|
+
remove_file(DAEMON_PID_FILE)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
Process.detach(pid)
|
|
220
|
+
|
|
221
|
+
sleep 0.2
|
|
222
|
+
|
|
223
|
+
if daemon_running?
|
|
224
|
+
puts "Daemon started."
|
|
225
|
+
puts "TCP: tcp://#{DAEMON_HOST}:#{DAEMON_PORT}"
|
|
226
|
+
puts "PID: #{daemon_pid}"
|
|
227
|
+
else
|
|
228
|
+
puts "Cannot start daemon."
|
|
229
|
+
end
|
|
230
|
+
rescue StandardError => e
|
|
231
|
+
warn "Error starting daemon: #{e.message}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def stop_daemon
|
|
235
|
+
pid = daemon_pid
|
|
236
|
+
|
|
237
|
+
unless pid && daemon_running?
|
|
238
|
+
remove_file(DAEMON_PID_FILE)
|
|
239
|
+
|
|
240
|
+
puts "Daemon not running."
|
|
241
|
+
puts "Cannot connect to daemon server."
|
|
242
|
+
puts "Run: vpn-tool start --daemon"
|
|
243
|
+
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
begin
|
|
248
|
+
Process.kill("TERM", pid)
|
|
249
|
+
|
|
250
|
+
puts "Daemon stopped (PID #{pid})."
|
|
251
|
+
rescue Errno::ESRCH
|
|
252
|
+
puts "Daemon not running."
|
|
253
|
+
rescue Errno::EPERM
|
|
254
|
+
abort "Error: permission denied for daemon PID #{pid}."
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
abort "Error stopping daemon: #{e.message}"
|
|
257
|
+
ensure
|
|
258
|
+
remove_file(DAEMON_PID_FILE) unless daemon_running?
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def daemon_status
|
|
263
|
+
if daemon_running?
|
|
264
|
+
socket = daemon_connection
|
|
265
|
+
|
|
266
|
+
if socket
|
|
267
|
+
socket.puts("STATUS")
|
|
268
|
+
|
|
269
|
+
response = socket.gets
|
|
270
|
+
|
|
271
|
+
socket.close
|
|
272
|
+
|
|
273
|
+
puts(
|
|
274
|
+
response || "Daemon running."
|
|
275
|
+
)
|
|
276
|
+
else
|
|
277
|
+
puts(
|
|
278
|
+
"Daemon PID #{daemon_pid} is running, " \
|
|
279
|
+
"but cannot connect to daemon server."
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
puts "Daemon not running."
|
|
284
|
+
puts "Cannot connect to daemon server."
|
|
285
|
+
puts "Run: vpn-tool start --daemon"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ----------------------------------------------------------
|
|
290
|
+
# API
|
|
291
|
+
# ----------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def api_request(method, endpoint, data = nil)
|
|
294
|
+
uri = URI.join(BASE_URL, endpoint)
|
|
295
|
+
|
|
296
|
+
request =
|
|
297
|
+
case method.to_s.upcase
|
|
298
|
+
when "GET"
|
|
299
|
+
Net::HTTP::Get.new(uri)
|
|
300
|
+
when "POST"
|
|
301
|
+
Net::HTTP::Post.new(uri)
|
|
302
|
+
else
|
|
303
|
+
raise "Unsupported HTTP method: #{method}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if data
|
|
307
|
+
request["Content-Type"] = "application/json"
|
|
308
|
+
request.body = JSON.generate(data)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
http = Net::HTTP.new(
|
|
312
|
+
uri.host,
|
|
313
|
+
uri.port
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
http.use_ssl = uri.scheme == "https"
|
|
317
|
+
http.open_timeout = 20
|
|
318
|
+
http.read_timeout = 20
|
|
319
|
+
|
|
320
|
+
response = http.request(request)
|
|
321
|
+
|
|
322
|
+
puts response.body
|
|
323
|
+
|
|
324
|
+
response
|
|
325
|
+
rescue StandardError => e
|
|
326
|
+
warn "API error: #{e.message}"
|
|
327
|
+
nil
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def api_get(endpoint)
|
|
331
|
+
api_request(:get, endpoint)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def api_post(endpoint, data = {})
|
|
335
|
+
api_request(:post, endpoint, data)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# ----------------------------------------------------------
|
|
339
|
+
# CLI
|
|
340
|
+
# ----------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
def build_cli
|
|
343
|
+
cli = Commander.new
|
|
344
|
+
|
|
345
|
+
cli.add_command(
|
|
346
|
+
"version",
|
|
347
|
+
"Show vpn-tool version"
|
|
348
|
+
) do
|
|
349
|
+
puts "vpn-tool #{VERSION}"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# --------------------------------------------------------
|
|
353
|
+
# Daemon commands
|
|
354
|
+
# --------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
start =
|
|
357
|
+
cli.add_command(
|
|
358
|
+
"start",
|
|
359
|
+
"Start the vpn-tool daemon"
|
|
360
|
+
) do |options, _args|
|
|
361
|
+
unless options[:daemon]
|
|
362
|
+
abort(
|
|
363
|
+
"Error: --daemon is required.\n" \
|
|
364
|
+
"Example: vpn-tool start --daemon"
|
|
365
|
+
)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
start_daemon
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
start.add_option(
|
|
372
|
+
["--daemon"],
|
|
373
|
+
"Start daemon"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
stop =
|
|
377
|
+
cli.add_command(
|
|
378
|
+
"stop",
|
|
379
|
+
"Stop the vpn-tool daemon"
|
|
380
|
+
) do |options, _args|
|
|
381
|
+
unless options[:daemon]
|
|
382
|
+
abort(
|
|
383
|
+
"Error: --daemon is required.\n" \
|
|
384
|
+
"Example: vpn-tool stop --daemon"
|
|
385
|
+
)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
stop_daemon
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
stop.add_option(
|
|
392
|
+
["--daemon"],
|
|
393
|
+
"Stop daemon"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
status =
|
|
397
|
+
cli.add_command(
|
|
398
|
+
"status",
|
|
399
|
+
"Show VPN status"
|
|
400
|
+
) do |options, _args|
|
|
401
|
+
if options[:daemon]
|
|
402
|
+
daemon_status
|
|
403
|
+
next
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
exit 1 unless require_daemon
|
|
407
|
+
|
|
408
|
+
api_get("/api/vpn/status")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
status.add_option(
|
|
412
|
+
["--daemon"],
|
|
413
|
+
"Show daemon status"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
cli.add_command(
|
|
417
|
+
"status-daemon",
|
|
418
|
+
"Show daemon status"
|
|
419
|
+
) do
|
|
420
|
+
daemon_status
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# --------------------------------------------------------
|
|
424
|
+
# VPN commands
|
|
425
|
+
# --------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
cli.add_command(
|
|
428
|
+
"countries",
|
|
429
|
+
"List VPN countries"
|
|
430
|
+
) do
|
|
431
|
+
exit 1 unless require_daemon
|
|
432
|
+
|
|
433
|
+
api_get("/api/vpn/countries")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
cli.add_command(
|
|
437
|
+
"ip",
|
|
438
|
+
"Show current IP"
|
|
439
|
+
) do
|
|
440
|
+
exit 1 unless require_daemon
|
|
441
|
+
|
|
442
|
+
api_get("/api/vpn/ip")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
cli.add_command(
|
|
446
|
+
"health",
|
|
447
|
+
"Show API health"
|
|
448
|
+
) do
|
|
449
|
+
exit 1 unless require_daemon
|
|
450
|
+
|
|
451
|
+
api_get("/api/healthz")
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
connect =
|
|
455
|
+
cli.add_command(
|
|
456
|
+
"connect",
|
|
457
|
+
"Connect to a VPN server"
|
|
458
|
+
) do |options, _args|
|
|
459
|
+
exit 1 unless require_daemon
|
|
460
|
+
|
|
461
|
+
unless require_login
|
|
462
|
+
exit 1
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
country = options[:country]
|
|
466
|
+
|
|
467
|
+
unless country
|
|
468
|
+
abort(
|
|
469
|
+
"Error: --country is required.\n" \
|
|
470
|
+
"Example: vpn-tool connect --country US"
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
country = country.upcase
|
|
475
|
+
|
|
476
|
+
unless country.match?(/\A[A-Z]{2}\z/)
|
|
477
|
+
abort(
|
|
478
|
+
"Country must be a 2-letter country code."
|
|
479
|
+
)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
api_post(
|
|
483
|
+
"/api/vpn/connect",
|
|
484
|
+
{
|
|
485
|
+
"countryCode" => country
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
connect.add_option(
|
|
491
|
+
["-c", "--country"],
|
|
492
|
+
"Country <value>"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
cli.add_command(
|
|
496
|
+
"disconnect",
|
|
497
|
+
"Disconnect from VPN"
|
|
498
|
+
) do
|
|
499
|
+
exit 1 unless require_daemon
|
|
500
|
+
|
|
501
|
+
if require_login
|
|
502
|
+
api_post(
|
|
503
|
+
"/api/vpn/disconnect",
|
|
504
|
+
{}
|
|
505
|
+
)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# --------------------------------------------------------
|
|
510
|
+
# Process commands
|
|
511
|
+
# --------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
cli.add_command(
|
|
514
|
+
"ps",
|
|
515
|
+
"List running processes"
|
|
516
|
+
) do
|
|
517
|
+
system("ps")
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
kill =
|
|
521
|
+
cli.add_command(
|
|
522
|
+
"kill",
|
|
523
|
+
"Kill a process by PID"
|
|
524
|
+
) do |options, _args|
|
|
525
|
+
pid = options[:pid]
|
|
526
|
+
|
|
527
|
+
unless pid
|
|
528
|
+
abort(
|
|
529
|
+
"Error: --pid is required.\n" \
|
|
530
|
+
"Example: vpn-tool kill --pid 1234"
|
|
531
|
+
)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
unless pid.match?(/\A\d+\z/)
|
|
535
|
+
abort "Error: PID must be a number."
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
pid = pid.to_i
|
|
539
|
+
|
|
540
|
+
if pid <= 0
|
|
541
|
+
abort "Error: PID must be greater than 0."
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
begin
|
|
545
|
+
Process.kill("TERM", pid)
|
|
546
|
+
|
|
547
|
+
puts "Process #{pid} terminated."
|
|
548
|
+
rescue Errno::ESRCH
|
|
549
|
+
abort(
|
|
550
|
+
"Error: process #{pid} was not found."
|
|
551
|
+
)
|
|
552
|
+
rescue Errno::EPERM
|
|
553
|
+
abort(
|
|
554
|
+
"Error: permission denied for process #{pid}."
|
|
555
|
+
)
|
|
556
|
+
rescue Errno::EINVAL
|
|
557
|
+
abort "Error: invalid PID."
|
|
558
|
+
rescue StandardError => e
|
|
559
|
+
abort "Error: #{e.message}"
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
kill.add_option(
|
|
564
|
+
["--pid"],
|
|
565
|
+
"Process ID <value>"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# --------------------------------------------------------
|
|
569
|
+
# Auth
|
|
570
|
+
# --------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
auth =
|
|
573
|
+
cli.add_command(
|
|
574
|
+
"auth",
|
|
575
|
+
"Authentication commands"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
auth.add_subcommand("help") do
|
|
579
|
+
puts <<~HELP
|
|
580
|
+
Authentication commands:
|
|
581
|
+
|
|
582
|
+
vpn-tool auth help
|
|
583
|
+
vpn-tool auth register
|
|
584
|
+
vpn-tool auth login
|
|
585
|
+
vpn-tool auth status
|
|
586
|
+
vpn-tool auth list
|
|
587
|
+
vpn-tool auth reset
|
|
588
|
+
vpn-tool auth logout
|
|
589
|
+
vpn-tool auth remove
|
|
590
|
+
HELP
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
auth.add_subcommand("register") do
|
|
594
|
+
ensure_config
|
|
595
|
+
|
|
596
|
+
if file_exists?(PIN_FILE)
|
|
597
|
+
puts "PIN already registered."
|
|
598
|
+
puts "Run: vpn-tool auth reset"
|
|
599
|
+
next
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
pin = prompt("Enter new PIN: ")
|
|
603
|
+
|
|
604
|
+
unless valid_pin?(pin)
|
|
605
|
+
puts "PIN must be exactly 4 digits."
|
|
606
|
+
next
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
confirm = prompt("Confirm new PIN: ")
|
|
610
|
+
|
|
611
|
+
unless pin == confirm
|
|
612
|
+
puts "PIN confirmation does not match."
|
|
613
|
+
next
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
if write_file(PIN_FILE, pin)
|
|
617
|
+
puts "PIN registered successfully."
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
auth.add_subcommand("login") do
|
|
622
|
+
puts "login works"
|
|
623
|
+
|
|
624
|
+
next unless check_pin(
|
|
625
|
+
"Enter PIN to login: "
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if write_file(
|
|
629
|
+
SESSION_FILE,
|
|
630
|
+
Time.now.to_i.to_s
|
|
631
|
+
)
|
|
632
|
+
puts "Login successful."
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
auth.add_subcommand("status") do
|
|
637
|
+
puts "status works"
|
|
638
|
+
|
|
639
|
+
next unless check_pin(
|
|
640
|
+
"Enter PIN to status: "
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
puts(
|
|
644
|
+
"PIN registered: #{file_exists?(PIN_FILE)}"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
puts(
|
|
648
|
+
"Session active: #{file_exists?(SESSION_FILE)}"
|
|
649
|
+
)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# --------------------------------------------------------
|
|
653
|
+
# Auth list
|
|
654
|
+
#
|
|
655
|
+
# Metadata only.
|
|
656
|
+
# The PIN itself is never displayed.
|
|
657
|
+
# --------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
auth.add_subcommand("list") do
|
|
660
|
+
ensure_config
|
|
661
|
+
|
|
662
|
+
puts "Authentication metadata:"
|
|
663
|
+
puts
|
|
664
|
+
puts "Config directory: #{CONFIG_DIR}"
|
|
665
|
+
puts "PIN registered: #{file_exists?(PIN_FILE)}"
|
|
666
|
+
puts "PIN file: #{PIN_FILE}"
|
|
667
|
+
puts "Session active: #{file_exists?(SESSION_FILE)}"
|
|
668
|
+
puts "Session file: #{SESSION_FILE}"
|
|
669
|
+
|
|
670
|
+
if file_exists?(SESSION_FILE)
|
|
671
|
+
session = read_file(SESSION_FILE)
|
|
672
|
+
|
|
673
|
+
if session&.match?(/\A\d+\z/)
|
|
674
|
+
puts(
|
|
675
|
+
"Session created: #{Time.at(session.to_i)}"
|
|
676
|
+
)
|
|
677
|
+
else
|
|
678
|
+
puts "Session created: unknown"
|
|
679
|
+
end
|
|
680
|
+
else
|
|
681
|
+
puts "Session created: none"
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
puts "PIN value: hidden"
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
auth.add_subcommand("reset") do
|
|
688
|
+
next unless check_pin(
|
|
689
|
+
"Enter PIN to reset: "
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
pin = prompt("Enter new PIN: ")
|
|
693
|
+
|
|
694
|
+
unless valid_pin?(pin)
|
|
695
|
+
puts "PIN must be exactly 4 digits."
|
|
696
|
+
next
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
confirm = prompt("Confirm new PIN: ")
|
|
700
|
+
|
|
701
|
+
unless pin == confirm
|
|
702
|
+
puts "PIN confirmation does not match."
|
|
703
|
+
next
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
if write_file(PIN_FILE, pin)
|
|
707
|
+
puts "PIN reset successfully."
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
auth.add_subcommand("logout") do
|
|
712
|
+
next unless check_pin(
|
|
713
|
+
"Enter PIN to logout: "
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
remove_file(SESSION_FILE)
|
|
717
|
+
|
|
718
|
+
puts "Logged out successfully."
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
auth.add_subcommand("remove") do
|
|
722
|
+
next unless check_pin(
|
|
723
|
+
"Enter PIN to remove: "
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
confirm = prompt(
|
|
727
|
+
"Type REMOVE to continue: "
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
unless confirm == "REMOVE"
|
|
731
|
+
puts "Remove cancelled."
|
|
732
|
+
next
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
remove_file(PIN_FILE)
|
|
736
|
+
remove_file(SESSION_FILE)
|
|
737
|
+
|
|
738
|
+
puts "PIN and session removed."
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
cli
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def help_text
|
|
745
|
+
<<~HELP
|
|
746
|
+
vpn-tool #{VERSION}
|
|
747
|
+
|
|
748
|
+
Authentication:
|
|
749
|
+
|
|
750
|
+
vpn-tool auth help
|
|
751
|
+
vpn-tool auth register
|
|
752
|
+
vpn-tool auth login
|
|
753
|
+
vpn-tool auth status
|
|
754
|
+
vpn-tool auth list
|
|
755
|
+
vpn-tool auth reset
|
|
756
|
+
vpn-tool auth logout
|
|
757
|
+
vpn-tool auth remove
|
|
758
|
+
|
|
759
|
+
VPN:
|
|
760
|
+
|
|
761
|
+
vpn-tool countries
|
|
762
|
+
vpn-tool connect --country US
|
|
763
|
+
vpn-tool status
|
|
764
|
+
vpn-tool ip
|
|
765
|
+
vpn-tool disconnect
|
|
766
|
+
vpn-tool health
|
|
767
|
+
|
|
768
|
+
Process:
|
|
769
|
+
|
|
770
|
+
vpn-tool ps
|
|
771
|
+
vpn-tool kill --pid 1234
|
|
772
|
+
|
|
773
|
+
Daemon:
|
|
774
|
+
|
|
775
|
+
vpn-tool start --daemon
|
|
776
|
+
vpn-tool stop --daemon
|
|
777
|
+
vpn-tool status --daemon
|
|
778
|
+
vpn-tool status-daemon
|
|
779
|
+
|
|
780
|
+
Version:
|
|
781
|
+
|
|
782
|
+
vpn-tool --version
|
|
783
|
+
vpn-tool version
|
|
784
|
+
HELP
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def run(argv = ARGV)
|
|
788
|
+
cli = build_cli
|
|
789
|
+
|
|
790
|
+
if argv.empty?
|
|
791
|
+
puts help_text
|
|
792
|
+
elsif argv.first == "--version" ||
|
|
793
|
+
argv.first == "-v"
|
|
794
|
+
puts "vpn-tool #{VERSION}"
|
|
795
|
+
elsif argv.first == "--help" ||
|
|
796
|
+
argv.first == "-h"
|
|
797
|
+
puts help_text
|
|
798
|
+
else
|
|
799
|
+
cli.parse(argv)
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
if __FILE__ == $PROGRAM_NAME
|
|
805
|
+
VpnTool.run
|
|
806
|
+
end
|
data/vpn-tool.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Gem::Specification.new do |spec|
|
|
2
|
+
spec.name = "vpn-tool-cli"
|
|
3
|
+
spec.version = "1.0.1"
|
|
4
|
+
spec.authors = ["Jjjm"]
|
|
5
|
+
spec.email = ["dodi66412@gmail.com"]
|
|
6
|
+
spec.summary = "VPN gateway management CLI"
|
|
7
|
+
spec.description = "Ruby command-line client for VPN gateway management with local authentication and daemon support."
|
|
8
|
+
spec.homepage = "https://github.com/jjjm03299-wq/vpn_tool"
|
|
9
|
+
spec.license = "MIT"
|
|
10
|
+
spec.required_ruby_version = ">= 3.0"
|
|
11
|
+
|
|
12
|
+
spec.files = Dir[
|
|
13
|
+
"lib/**/*.rb",
|
|
14
|
+
"bin/*",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"*.gemspec"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
spec.bindir = "bin"
|
|
21
|
+
spec.executables = ["vpn-tool"]
|
|
22
|
+
spec.require_paths = ["lib"]
|
|
23
|
+
|
|
24
|
+
spec.add_runtime_dependency "commander-tool", "~> 1.0", ">= 1.0.2"
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: vpn-tool-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jjjm
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: commander-tool
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 1.0.2
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.0'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 1.0.2
|
|
32
|
+
description: Ruby command-line client for VPN gateway management with local authentication
|
|
33
|
+
and daemon support.
|
|
34
|
+
email:
|
|
35
|
+
- dodi66412@gmail.com
|
|
36
|
+
executables:
|
|
37
|
+
- vpn-tool
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- README.md
|
|
42
|
+
- bin/vpn-tool
|
|
43
|
+
- lib/vpn-tool.rb
|
|
44
|
+
- vpn-tool.gemspec
|
|
45
|
+
homepage: https://github.com/jjjm03299-wq/vpn_tool
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata: {}
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3.0'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.6.2
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: VPN gateway management CLI
|
|
66
|
+
test_files: []
|