terminalwire-client 0.3.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/terminalwire/client/entitlement/environment_variables.rb +26 -0
- data/lib/terminalwire/client/entitlement/paths.rb +97 -0
- data/lib/terminalwire/client/entitlement/policy.rb +117 -0
- data/lib/terminalwire/client/entitlement/schemes.rb +26 -0
- data/lib/terminalwire/client/entitlement.rb +9 -0
- data/lib/terminalwire/client/exec.rb +44 -0
- data/lib/terminalwire/client/handler.rb +67 -0
- data/lib/terminalwire/client/resource.rb +204 -0
- data/lib/terminalwire/client/server_license_verification.rb +74 -0
- data/lib/terminalwire/client.rb +45 -0
- data/lib/terminalwire-client.rb +3 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60d8965eb9d76fbfe435e72747553489817c77e2a82fa5cdba7933d6c7798c21
|
4
|
+
data.tar.gz: 611a958e01b7216ebb23dd311db799f84bc76cfcda5b7c6dcecaaaf71959ed0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1f9c42e92bc39458fd0ba4c3ea09fa5a240444f3a2e368ebe1aa62819b85dbe20037cadb3331d628737ed40e5e2300cbde5260a201571046b00a7cbf87e445b6
|
7
|
+
data.tar.gz: 5277aa2111746a0c19e25b4dcef9a1e51cb941e0efbd6ce408957e5166257da051cebc13920f3cb92039d2db43bf361c3529f98110ac5f74945c8a046debe8c8
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Terminalwire::Client::Entitlement
|
2
|
+
# ENV vars that the server can access on the client.
|
3
|
+
class EnvironmentVariables
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@permitted = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(&)
|
11
|
+
@permitted.each(&)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permit(variable)
|
15
|
+
@permitted << variable.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def permitted?(key)
|
19
|
+
include? key.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def serialize
|
23
|
+
map { |name| { name: } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Terminalwire::Client::Entitlement
|
2
|
+
# A list of paths and permissions that server has to write on the client workstation.
|
3
|
+
class Paths
|
4
|
+
class Permit
|
5
|
+
attr_reader :path, :mode
|
6
|
+
# Ensure the default file mode is read/write for owner only. This ensures
|
7
|
+
# that if the server tries uploading an executable file, it won't be when it
|
8
|
+
# lands on the client.
|
9
|
+
#
|
10
|
+
# Eventually we'll move this into entitlements so the client can set maximum
|
11
|
+
# permissions for files and directories.
|
12
|
+
MODE = 0o600 # rw-------
|
13
|
+
|
14
|
+
# Constants for permission bit masks
|
15
|
+
OWNER_PERMISSIONS = 0o700 # rwx------
|
16
|
+
GROUP_PERMISSIONS = 0o070 # ---rwx---
|
17
|
+
OTHERS_PERMISSIONS = 0o007 # ------rwx
|
18
|
+
|
19
|
+
# We'll validate that modes are within this range.
|
20
|
+
MODE_RANGE = 0o000..0o777
|
21
|
+
|
22
|
+
def initialize(path:, mode: MODE)
|
23
|
+
@path = Pathname.new(path)
|
24
|
+
@mode = convert(mode)
|
25
|
+
end
|
26
|
+
|
27
|
+
def permitted_path?(path)
|
28
|
+
# This MUST be done via File.fnmatch because Pathname#fnmatch does not work. If you
|
29
|
+
# try changing this 🚨 YOU MAY CIRCUMVENT THE SECURITY MEASURES IN PLACE. 🚨
|
30
|
+
File.fnmatch @path.to_s, path.to_s, File::FNM_PATHNAME
|
31
|
+
end
|
32
|
+
|
33
|
+
def permitted_mode?(value)
|
34
|
+
# Ensure the mode is at least as permissive as the permitted mode.
|
35
|
+
mode = convert(value)
|
36
|
+
|
37
|
+
# Extract permission bits for owner, group, and others
|
38
|
+
owner_bits = mode & OWNER_PERMISSIONS
|
39
|
+
group_bits = mode & GROUP_PERMISSIONS
|
40
|
+
others_bits = mode & OTHERS_PERMISSIONS
|
41
|
+
|
42
|
+
# Ensure that the mode doesn't grant more permissions than @mode in any class (owner, group, others)
|
43
|
+
(owner_bits <= @mode & OWNER_PERMISSIONS) &&
|
44
|
+
(group_bits <= @mode & GROUP_PERMISSIONS) &&
|
45
|
+
(others_bits <= @mode & OTHERS_PERMISSIONS)
|
46
|
+
end
|
47
|
+
|
48
|
+
def permitted?(path:, mode: @mode)
|
49
|
+
permitted_path?(path) && permitted_mode?(mode)
|
50
|
+
end
|
51
|
+
|
52
|
+
def serialize
|
53
|
+
{
|
54
|
+
location: @path.to_s,
|
55
|
+
mode: @mode
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
def convert(value)
|
61
|
+
mode = Integer(value)
|
62
|
+
raise ArgumentError, "The mode #{format_octet value} must be an octet value between #{format_octet MODE_RANGE.first} and #{format_octet MODE_RANGE.last}" unless MODE_RANGE.cover?(mode)
|
63
|
+
mode
|
64
|
+
end
|
65
|
+
|
66
|
+
def format_octet(value)
|
67
|
+
format("0o%03o", value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
include Enumerable
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
@permitted = []
|
75
|
+
end
|
76
|
+
|
77
|
+
def each(&)
|
78
|
+
@permitted.each(&)
|
79
|
+
end
|
80
|
+
|
81
|
+
def permit(path, **)
|
82
|
+
@permitted.append Permit.new(path:, **)
|
83
|
+
end
|
84
|
+
|
85
|
+
def permitted?(path, mode: nil)
|
86
|
+
if mode
|
87
|
+
find { |it| it.permitted_path?(path) and it.permitted_mode?(mode) }
|
88
|
+
else
|
89
|
+
find { |it| it.permitted_path?(path) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def serialize
|
94
|
+
map(&:serialize)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Terminalwire::Client::Entitlement
|
2
|
+
module Policy
|
3
|
+
# A policy has the authority, paths, and schemes that the server is allowed to access.
|
4
|
+
class Base
|
5
|
+
attr_reader :paths, :authority, :schemes, :environment_variables
|
6
|
+
|
7
|
+
def initialize(authority:)
|
8
|
+
@authority = authority
|
9
|
+
@paths = Paths.new
|
10
|
+
|
11
|
+
# Permit the domain directory. This is necessary for basic operation of the client.
|
12
|
+
@paths.permit storage_path
|
13
|
+
@paths.permit storage_pattern
|
14
|
+
|
15
|
+
@schemes = Schemes.new
|
16
|
+
# Permit http & https by default.
|
17
|
+
@schemes.permit "http"
|
18
|
+
@schemes.permit "https"
|
19
|
+
|
20
|
+
@environment_variables = EnvironmentVariables.new
|
21
|
+
# Permit the HOME and TERMINALWIRE_HOME environment variables.
|
22
|
+
@environment_variables.permit "TERMINALWIRE_HOME"
|
23
|
+
end
|
24
|
+
|
25
|
+
def root_path
|
26
|
+
# TODO: This needs to be passed into the Policy so that it can be set by the client.
|
27
|
+
Terminalwire::Client.root_path
|
28
|
+
end
|
29
|
+
|
30
|
+
def authority_path
|
31
|
+
root_path.join("authorities/#{authority}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def storage_path
|
35
|
+
authority_path.join("storage")
|
36
|
+
end
|
37
|
+
|
38
|
+
def storage_pattern
|
39
|
+
storage_path.join("**/*")
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize
|
43
|
+
{
|
44
|
+
authority: @authority,
|
45
|
+
schemes: @schemes.serialize,
|
46
|
+
paths: @paths.serialize,
|
47
|
+
environment_variables: @environment_variables.serialize
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Root < Base
|
53
|
+
AUTHORITY = "terminalwire.com".freeze
|
54
|
+
|
55
|
+
# Terminalwire checks these to install the binary stubs path.
|
56
|
+
SHELL_INITIALIZATION_FILE_PATHS = %w[
|
57
|
+
~/.bash_profile
|
58
|
+
~/.bashrc
|
59
|
+
~/.zprofile
|
60
|
+
~/.zshrc
|
61
|
+
~/.profile
|
62
|
+
~/.config/fish/config.fish
|
63
|
+
~/.bash_login
|
64
|
+
~/.cshrc
|
65
|
+
~/.tcshrc
|
66
|
+
].freeze
|
67
|
+
|
68
|
+
# Ensure the binary stubs are executable. This increases the
|
69
|
+
# file mode entitlement so that stubs created in ./bin are executable.
|
70
|
+
BINARY_PATH_FILE_MODE = 0o755
|
71
|
+
|
72
|
+
def initialize(*, **, &)
|
73
|
+
# Make damn sure the authority is set to Terminalwire.
|
74
|
+
super(*, authority: AUTHORITY, **, &)
|
75
|
+
|
76
|
+
# Now setup special permitted paths.
|
77
|
+
@paths.permit root_path
|
78
|
+
@paths.permit root_pattern
|
79
|
+
# Permit the dotfiles so terminalwire can install the binary stubs.
|
80
|
+
SHELL_INITIALIZATION_FILE_PATHS.each do |path|
|
81
|
+
@paths.permit path
|
82
|
+
end
|
83
|
+
|
84
|
+
# Permit terminalwire to grant execute permissions to the binary stubs.
|
85
|
+
@paths.permit binary_pattern, mode: BINARY_PATH_FILE_MODE
|
86
|
+
|
87
|
+
# Used to check if terminalwire is setup in the user's PATH environment variable.
|
88
|
+
@environment_variables.permit "PATH"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Grant access to the `~/.terminalwire/**/*` path so users can install
|
92
|
+
# terminalwire apps via `terminalwire install svbtle`, etc.
|
93
|
+
def root_pattern
|
94
|
+
root_path.join("**/*").freeze
|
95
|
+
end
|
96
|
+
|
97
|
+
# Path where the terminalwire binary stubs are stored.
|
98
|
+
def binary_path
|
99
|
+
root_path.join("bin").freeze
|
100
|
+
end
|
101
|
+
|
102
|
+
# Pattern for the binary path.
|
103
|
+
def binary_pattern
|
104
|
+
binary_path.join("*").freeze
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.resolve(*, authority:, **, &)
|
109
|
+
case authority
|
110
|
+
when Policy::Root::AUTHORITY
|
111
|
+
Root.new(*, **, &)
|
112
|
+
else
|
113
|
+
Base.new *, authority:, **, &
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Terminalwire::Client::Entitlement
|
2
|
+
# URLs the server can open on the client.
|
3
|
+
class Schemes
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@permitted = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(&)
|
11
|
+
@permitted.each(&)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permit(scheme)
|
15
|
+
@permitted << scheme.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def permitted?(url)
|
19
|
+
include? URI(url).scheme
|
20
|
+
end
|
21
|
+
|
22
|
+
def serialize
|
23
|
+
map { |scheme| { scheme: } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module Terminalwire::Client
|
4
|
+
# Entitlements are the security boundary between the server and the client that lives on the client.
|
5
|
+
# The server might request a file or directory from the client, and the client will check the entitlements
|
6
|
+
# to see if the server is authorized to access the requested resource.
|
7
|
+
module Entitlement
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Terminalwire::Client
|
6
|
+
# Called by the `terminalwire-exec` shebang in scripts. This makes it easy for people
|
7
|
+
# to create their own scripts that use Terminalwire that look like this:
|
8
|
+
#
|
9
|
+
# ```sh
|
10
|
+
# #!/usr/bin/env terminalwire-exec
|
11
|
+
# url: "https://terminalwire.com/terminal"
|
12
|
+
# ```
|
13
|
+
#
|
14
|
+
# These files are saved, then `chmod + x` is run on them and they become executable.
|
15
|
+
class Exec
|
16
|
+
attr_reader :arguments, :path, :configuration, :url
|
17
|
+
|
18
|
+
def initialize(path:, arguments:)
|
19
|
+
@arguments = arguments
|
20
|
+
@path = Pathname.new(path)
|
21
|
+
@configuration = YAML.safe_load_file(@path)
|
22
|
+
@url = URI(@configuration.fetch("url"))
|
23
|
+
rescue Errno::ENOENT => e
|
24
|
+
raise Terminalwire::Error, "File not found: #{@path}"
|
25
|
+
rescue URI::InvalidURIError => e
|
26
|
+
raise Terminalwire::Error, "Invalid URI: #{@url}"
|
27
|
+
rescue KeyError => e
|
28
|
+
raise Terminalwire::Error, "Missing key in configuration: #{e}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def start
|
32
|
+
Terminalwire::Client.websocket(url:, arguments:)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.start
|
36
|
+
case ARGV
|
37
|
+
in path, *arguments
|
38
|
+
new(path:, arguments:).start
|
39
|
+
end
|
40
|
+
rescue NoMatchingPatternError => e
|
41
|
+
raise Terminalwire::Error, "Launched with incorrect arguments: #{ARGV}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Terminalwire::Client
|
2
|
+
# The handler is the main class that connects to the Terminalwire server and
|
3
|
+
# dispatches messages to the appropriate resources.
|
4
|
+
class Handler
|
5
|
+
VERSION = "0.1.0".freeze
|
6
|
+
|
7
|
+
include Terminalwire::Logging
|
8
|
+
|
9
|
+
attr_reader :adapter, :resources, :endpoint
|
10
|
+
attr_accessor :entitlement
|
11
|
+
|
12
|
+
def initialize(adapter, arguments: ARGV, program_name: $0, endpoint:)
|
13
|
+
@endpoint = endpoint
|
14
|
+
@adapter = adapter
|
15
|
+
@program_arguments = arguments
|
16
|
+
@program_name = program_name
|
17
|
+
@entitlement = Entitlement::Policy.resolve(authority: @endpoint.authority)
|
18
|
+
|
19
|
+
yield self if block_given?
|
20
|
+
|
21
|
+
@resources = Resource::Handler.new do |it|
|
22
|
+
it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
|
23
|
+
it << Resource::STDIN.new("stdin", @adapter, entitlement:)
|
24
|
+
it << Resource::STDERR.new("stderr", @adapter, entitlement:)
|
25
|
+
it << Resource::Browser.new("browser", @adapter, entitlement:)
|
26
|
+
it << Resource::File.new("file", @adapter, entitlement:)
|
27
|
+
it << Resource::Directory.new("directory", @adapter, entitlement:)
|
28
|
+
it << Resource::EnvironmentVariable.new("environment_variable", @adapter, entitlement:)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_license
|
33
|
+
# Connect to the Terminalwire license server to verify the URL endpoint
|
34
|
+
# and displays a message to the user, if any are present.
|
35
|
+
$stdout.print ServerLicenseVerification.new(url: @endpoint.to_url).message
|
36
|
+
rescue
|
37
|
+
$stderr.puts "Failed to verify server license."
|
38
|
+
end
|
39
|
+
|
40
|
+
def connect
|
41
|
+
verify_license
|
42
|
+
|
43
|
+
@adapter.write(
|
44
|
+
event: "initialization",
|
45
|
+
protocol: { version: VERSION },
|
46
|
+
entitlement: @entitlement.serialize,
|
47
|
+
program: {
|
48
|
+
name: @program_name,
|
49
|
+
arguments: @program_arguments
|
50
|
+
}
|
51
|
+
)
|
52
|
+
|
53
|
+
loop do
|
54
|
+
handle @adapter.read
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle(message)
|
59
|
+
case message
|
60
|
+
in { event: "resource", action: "command", name:, parameters: }
|
61
|
+
@resources.dispatch(**message)
|
62
|
+
in { event: "exit", status: }
|
63
|
+
exit Integer(status)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "io/console"
|
3
|
+
|
4
|
+
module Terminalwire::Client::Resource
|
5
|
+
# Dispatches messages from the Client::Handler to the appropriate resource.
|
6
|
+
class Handler
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@resources = {}
|
11
|
+
yield self if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@resources.values.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(resource)
|
19
|
+
# Detect if the resource is already registered and throw an error
|
20
|
+
if @resources.key?(resource.name)
|
21
|
+
raise "Resource #{resource.name} already registered"
|
22
|
+
else
|
23
|
+
@resources[resource.name] = resource
|
24
|
+
end
|
25
|
+
end
|
26
|
+
alias :<< :add
|
27
|
+
|
28
|
+
def dispatch(**message)
|
29
|
+
case message
|
30
|
+
in { event:, action:, name:, command:, parameters: }
|
31
|
+
resource = @resources.fetch(name)
|
32
|
+
resource.command(command, **parameters)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Dispatcher, security, and response macros for resources.
|
38
|
+
class Base < Terminalwire::Resource::Base
|
39
|
+
def initialize(*, entitlement:, **)
|
40
|
+
super(*, **)
|
41
|
+
@entitlement = entitlement
|
42
|
+
connect
|
43
|
+
end
|
44
|
+
|
45
|
+
def command(command, **parameters)
|
46
|
+
begin
|
47
|
+
if permit(command, **parameters)
|
48
|
+
succeed self.public_send(command, **parameters)
|
49
|
+
else
|
50
|
+
fail "Client denied #{command}", command:, parameters:
|
51
|
+
end
|
52
|
+
rescue => e
|
53
|
+
fail e.message, command:, parameters:
|
54
|
+
raise
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def permit(...)
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class EnvironmentVariable < Base
|
66
|
+
# Accepts a list of environment variables to permit.
|
67
|
+
def read(name:)
|
68
|
+
ENV[name]
|
69
|
+
end
|
70
|
+
|
71
|
+
# def write(name:, value:)
|
72
|
+
# ENV[name] = value
|
73
|
+
# end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def permit(command, name:, **)
|
78
|
+
@entitlement.environment_variables.permitted? name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class STDOUT < Base
|
83
|
+
def connect
|
84
|
+
@io = $stdout
|
85
|
+
end
|
86
|
+
|
87
|
+
def print(data:)
|
88
|
+
@io.print(data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def print_line(data:)
|
92
|
+
@io.puts(data)
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
def permit(...)
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class STDERR < STDOUT
|
103
|
+
def connect
|
104
|
+
@io = $stderr
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class STDIN < Base
|
109
|
+
def connect
|
110
|
+
@io = $stdin
|
111
|
+
end
|
112
|
+
|
113
|
+
def read_line
|
114
|
+
@io.gets
|
115
|
+
end
|
116
|
+
|
117
|
+
def read_password
|
118
|
+
@io.getpass
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
|
123
|
+
def permit(...)
|
124
|
+
true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class File < Base
|
129
|
+
File = ::File
|
130
|
+
|
131
|
+
def read(path:)
|
132
|
+
File.read File.expand_path(path)
|
133
|
+
end
|
134
|
+
|
135
|
+
def write(path:, content:, mode: nil)
|
136
|
+
File.open(File.expand_path(path), "w", mode) { |f| f.write(content) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def append(path:, content:, mode: nil)
|
140
|
+
File.open(File.expand_path(path), "a", mode) { |f| f.write(content) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete(path:)
|
144
|
+
File.delete File.expand_path(path)
|
145
|
+
end
|
146
|
+
|
147
|
+
def exist(path:)
|
148
|
+
File.exist? File.expand_path(path)
|
149
|
+
end
|
150
|
+
|
151
|
+
def change_mode(path:, mode:)
|
152
|
+
File.chmod mode, File.expand_path(path)
|
153
|
+
end
|
154
|
+
|
155
|
+
protected
|
156
|
+
|
157
|
+
def permit(command, path:, mode: nil, **)
|
158
|
+
@entitlement.paths.permitted? path, mode:
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Directory < Base
|
163
|
+
File = ::File
|
164
|
+
|
165
|
+
def list(path:)
|
166
|
+
Dir.glob path
|
167
|
+
end
|
168
|
+
|
169
|
+
def create(path:)
|
170
|
+
FileUtils.mkdir_p File.expand_path(path)
|
171
|
+
rescue Errno::EEXIST
|
172
|
+
# Do nothing
|
173
|
+
end
|
174
|
+
|
175
|
+
def exist(path:)
|
176
|
+
Dir.exist? path
|
177
|
+
end
|
178
|
+
|
179
|
+
def delete(path:)
|
180
|
+
Dir.delete path
|
181
|
+
end
|
182
|
+
|
183
|
+
protected
|
184
|
+
|
185
|
+
def permit(command, path:, **)
|
186
|
+
@entitlement.paths.permitted? path
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class Browser < Base
|
191
|
+
def launch(url:)
|
192
|
+
Launchy.open(URI(url))
|
193
|
+
# TODO: This is a hack to get the `respond` method to work.
|
194
|
+
# Maybe explicitly call a `suceed` and `fail` method?
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
def permit(command, url:, **)
|
201
|
+
@entitlement.schemes.permitted? url
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "async/http/internet"
|
2
|
+
require "base64"
|
3
|
+
require "uri"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Terminalwire::Client
|
7
|
+
# Checkes the server for a license verification at `https://terminalwire.com/licenses/verifications/`
|
8
|
+
# and displays the message to the user, if necessary.
|
9
|
+
class ServerLicenseVerification
|
10
|
+
include Terminalwire::Logging
|
11
|
+
|
12
|
+
def initialize(url:)
|
13
|
+
@url = URI(url)
|
14
|
+
@internet = Async::HTTP::Internet.new
|
15
|
+
@cache_store = Terminalwire::Cache::File::Store.new(path: Terminalwire::Client.root_path.join("cache/licenses/verifications"))
|
16
|
+
end
|
17
|
+
|
18
|
+
def key
|
19
|
+
Base64.urlsafe_encode64 @url
|
20
|
+
end
|
21
|
+
|
22
|
+
def cache = @cache_store.entry key
|
23
|
+
|
24
|
+
def payload
|
25
|
+
if cache.miss?
|
26
|
+
logger.debug "Stale verification. Requesting new verification."
|
27
|
+
request do |response|
|
28
|
+
# Set the expiry on the file cache for the header.
|
29
|
+
if max_age = response.headers["cache-control"].max_age
|
30
|
+
logger.debug "Caching for #{max_age}"
|
31
|
+
cache.expires = Time.now + max_age
|
32
|
+
end
|
33
|
+
|
34
|
+
# Process based on the response code.
|
35
|
+
case response.status
|
36
|
+
in 200
|
37
|
+
logger.debug "License for #{@url} found."
|
38
|
+
data = self.class.unpack response.read
|
39
|
+
cache.value = data
|
40
|
+
return data
|
41
|
+
in 404
|
42
|
+
logger.debug "License for #{@url} not found."
|
43
|
+
return self.class.unpack response.read
|
44
|
+
end
|
45
|
+
end
|
46
|
+
else
|
47
|
+
return cache.value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def message
|
52
|
+
payload.dig(:shell, :output)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def verification_url
|
58
|
+
Terminalwire.url
|
59
|
+
.path("/licenses/verifications", key)
|
60
|
+
end
|
61
|
+
|
62
|
+
def request(&)
|
63
|
+
logger.debug "Requesting license verification from #{verification_url}."
|
64
|
+
response = @internet.get verification_url, {
|
65
|
+
"Accept" => "application/x-msgpack",
|
66
|
+
"User-Agent" => "Terminalwire/#{Terminalwire::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})",
|
67
|
+
}, &
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.unpack(pack)
|
71
|
+
MessagePack.unpack(pack, symbolize_keys: true)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'terminalwire'
|
2
|
+
|
3
|
+
require 'launchy'
|
4
|
+
require 'io/console'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
require 'forwardable'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
require 'async'
|
11
|
+
require 'async/http/endpoint'
|
12
|
+
require 'async/websocket/client'
|
13
|
+
require 'async/websocket/adapters/rack'
|
14
|
+
require 'uri-builder'
|
15
|
+
|
16
|
+
require "zeitwerk"
|
17
|
+
Zeitwerk::Loader.for_gem_extension(Terminalwire).tap do |loader|
|
18
|
+
loader.setup
|
19
|
+
end
|
20
|
+
|
21
|
+
module Terminalwire
|
22
|
+
module Client
|
23
|
+
ROOT_PATH = "~/.terminalwire".freeze
|
24
|
+
def self.root_path = Pathname.new(ENV.fetch("TERMINALWIRE_HOME", ROOT_PATH))
|
25
|
+
|
26
|
+
def self.websocket(url:, arguments: ARGV, &configuration)
|
27
|
+
ENV["TERMINALWIRE_HOME"] ||= root_path.to_s
|
28
|
+
|
29
|
+
url = URI(url)
|
30
|
+
|
31
|
+
Async do |task|
|
32
|
+
endpoint = Async::HTTP::Endpoint.parse(
|
33
|
+
url,
|
34
|
+
alpn_protocols: Async::HTTP::Protocol::HTTP11.names
|
35
|
+
)
|
36
|
+
|
37
|
+
Async::WebSocket::Client.connect(endpoint) do |adapter|
|
38
|
+
transport = Terminalwire::Transport::WebSocket.new(adapter)
|
39
|
+
adapter = Terminalwire::Adapter::Socket.new(transport)
|
40
|
+
Terminalwire::Client::Handler.new(adapter, arguments:, endpoint:, &configuration).connect
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terminalwire-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0.alpha1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brad Gessler
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: launchy
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: terminalwire-core
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.0.alpha1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.3.0.alpha1
|
41
|
+
description: Stream command-line apps from your server without a web API
|
42
|
+
email:
|
43
|
+
- brad@terminalwire.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- lib/terminalwire-client.rb
|
49
|
+
- lib/terminalwire/client.rb
|
50
|
+
- lib/terminalwire/client/entitlement.rb
|
51
|
+
- lib/terminalwire/client/entitlement/environment_variables.rb
|
52
|
+
- lib/terminalwire/client/entitlement/paths.rb
|
53
|
+
- lib/terminalwire/client/entitlement/policy.rb
|
54
|
+
- lib/terminalwire/client/entitlement/schemes.rb
|
55
|
+
- lib/terminalwire/client/exec.rb
|
56
|
+
- lib/terminalwire/client/handler.rb
|
57
|
+
- lib/terminalwire/client/resource.rb
|
58
|
+
- lib/terminalwire/client/server_license_verification.rb
|
59
|
+
homepage: https://terminalwire.com/ruby
|
60
|
+
licenses:
|
61
|
+
- Proprietary (https://terminalwire.com/license)
|
62
|
+
metadata:
|
63
|
+
allowed_push_host: https://rubygems.org/
|
64
|
+
homepage_uri: https://terminalwire.com/ruby
|
65
|
+
source_code_uri: https://github.com/terminalwire/ruby/tree/main/terminalwire-client
|
66
|
+
changelog_uri: https://github.com/terminalwire/ruby/tags
|
67
|
+
funding_uri: https://terminalwire.com/funding
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.5.9
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Ship a CLI for your web app. No API required.
|
87
|
+
test_files: []
|