terminalwire 0.1.17 → 0.2.3
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 +4 -4
- data/lib/terminalwire/adapter.rb +1 -1
- data/lib/terminalwire/cache.rb +1 -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 +3 -208
- data/lib/terminalwire/client/exec.rb +9 -0
- data/lib/terminalwire/client/handler.rb +67 -0
- data/lib/terminalwire/client/resource.rb +37 -9
- data/lib/terminalwire/client/server_license_verification.rb +53 -53
- data/lib/terminalwire/client.rb +5 -62
- data/lib/terminalwire/server/context.rb +41 -3
- data/lib/terminalwire/server/resource.rb +13 -0
- data/lib/terminalwire/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39232fa4bba65cc98bf87a3625e80a8c01ccee009277adeb53bd57efe28fd73c
|
4
|
+
data.tar.gz: 94ceb432cdd3f10fb3492cdfb5025eb3e71edbe352bdf0e2cd2a8438aef6f22d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a05475187aa41b8bb7b3e21ae13975a48f739ee95e00a2f099e7d13817a5192b9298a79236e008668416d8f385c31b02d716c404255e96148c385ff6268d4f10
|
7
|
+
data.tar.gz: 36fff6322f43fd783906e9e8a1878dd192b0b44695134cc6df12085da305f8cd34a758415ebb8d14a7d21ec4860172eb250c94508da27f5728149bf7cf76cbd8
|
data/lib/terminalwire/adapter.rb
CHANGED
data/lib/terminalwire/cache.rb
CHANGED
@@ -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
|
@@ -1,214 +1,9 @@
|
|
1
1
|
require "pathname"
|
2
2
|
|
3
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.
|
4
7
|
module Entitlement
|
5
|
-
class Paths
|
6
|
-
class Permit
|
7
|
-
attr_reader :path, :mode
|
8
|
-
# Ensure the default file mode is read/write for owner only. This ensures
|
9
|
-
# that if the server tries uploading an executable file, it won't be when it
|
10
|
-
# lands on the client.
|
11
|
-
#
|
12
|
-
# Eventually we'll move this into entitlements so the client can set maximum
|
13
|
-
# permissions for files and directories.
|
14
|
-
MODE = 0o600 # rw-------
|
15
|
-
|
16
|
-
# Constants for permission bit masks
|
17
|
-
OWNER_PERMISSIONS = 0o700 # rwx------
|
18
|
-
GROUP_PERMISSIONS = 0o070 # ---rwx---
|
19
|
-
OTHERS_PERMISSIONS = 0o007 # ------rwx
|
20
|
-
|
21
|
-
# We'll validate that modes are within this range.
|
22
|
-
MODE_RANGE = 0o000..0o777
|
23
|
-
|
24
|
-
def initialize(path:, mode: MODE)
|
25
|
-
@path = Pathname.new(path).expand_path
|
26
|
-
@mode = convert(mode)
|
27
|
-
end
|
28
|
-
|
29
|
-
def permitted_path?(path)
|
30
|
-
# This MUST be done via File.fnmatch because Pathname#fnmatch does not work. If you
|
31
|
-
# try changing this 🚨 YOU MAY CIRCUMVENT THE SECURITY MEASURES IN PLACE. 🚨
|
32
|
-
File.fnmatch @path.to_s, File.expand_path(path), File::FNM_PATHNAME
|
33
|
-
end
|
34
|
-
|
35
|
-
def permitted_mode?(value)
|
36
|
-
# Ensure the mode is at least as permissive as the permitted mode.
|
37
|
-
mode = convert(value)
|
38
|
-
|
39
|
-
# Extract permission bits for owner, group, and others
|
40
|
-
owner_bits = mode & OWNER_PERMISSIONS
|
41
|
-
group_bits = mode & GROUP_PERMISSIONS
|
42
|
-
others_bits = mode & OTHERS_PERMISSIONS
|
43
|
-
|
44
|
-
# Ensure that the mode doesn't grant more permissions than @mode in any class (owner, group, others)
|
45
|
-
(owner_bits <= @mode & OWNER_PERMISSIONS) &&
|
46
|
-
(group_bits <= @mode & GROUP_PERMISSIONS) &&
|
47
|
-
(others_bits <= @mode & OTHERS_PERMISSIONS)
|
48
|
-
end
|
49
|
-
|
50
|
-
def permitted?(path:, mode: @mode)
|
51
|
-
permitted_path?(path) && permitted_mode?(mode)
|
52
|
-
end
|
53
|
-
|
54
|
-
def serialize
|
55
|
-
{
|
56
|
-
location: @path.to_s,
|
57
|
-
mode: @mode
|
58
|
-
}
|
59
|
-
end
|
60
|
-
|
61
|
-
protected
|
62
|
-
def convert(value)
|
63
|
-
mode = Integer(value)
|
64
|
-
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)
|
65
|
-
mode
|
66
|
-
end
|
67
|
-
|
68
|
-
def format_octet(value)
|
69
|
-
format("0o%03o", value)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
include Enumerable
|
74
|
-
|
75
|
-
def initialize
|
76
|
-
@permitted = []
|
77
|
-
end
|
78
|
-
|
79
|
-
def each(&)
|
80
|
-
@permitted.each(&)
|
81
|
-
end
|
82
|
-
|
83
|
-
def permit(path, **)
|
84
|
-
@permitted.append Permit.new(path:, **)
|
85
|
-
end
|
86
|
-
|
87
|
-
def permitted?(path, mode: nil)
|
88
|
-
if mode
|
89
|
-
find { |it| it.permitted_path?(path) and it.permitted_mode?(mode) }
|
90
|
-
else
|
91
|
-
find { |it| it.permitted_path?(path) }
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def serialize
|
96
|
-
map(&:serialize)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
class Schemes
|
101
|
-
include Enumerable
|
102
|
-
|
103
|
-
def initialize
|
104
|
-
@permitted = Set.new
|
105
|
-
end
|
106
|
-
|
107
|
-
def each(&)
|
108
|
-
@permitted.each(&)
|
109
|
-
end
|
110
|
-
|
111
|
-
def permit(scheme)
|
112
|
-
@permitted << scheme.to_s
|
113
|
-
end
|
114
|
-
|
115
|
-
def permitted?(url)
|
116
|
-
include? URI(url).scheme
|
117
|
-
end
|
118
|
-
|
119
|
-
def serialize
|
120
|
-
@permitted.to_a.map(&:to_s)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
class Policy
|
125
|
-
attr_reader :paths, :authority, :schemes
|
126
|
-
|
127
|
-
ROOT_PATH = "~/.terminalwire".freeze
|
128
|
-
|
129
|
-
def initialize(authority:)
|
130
|
-
@authority = authority
|
131
|
-
@paths = Paths.new
|
132
|
-
|
133
|
-
# Permit the domain directory. This is necessary for basic operation of the client.
|
134
|
-
@paths.permit storage_path
|
135
|
-
@paths.permit storage_pattern
|
136
|
-
|
137
|
-
@schemes = Schemes.new
|
138
|
-
# Permit http & https by default.
|
139
|
-
@schemes.permit "http"
|
140
|
-
@schemes.permit "https"
|
141
|
-
end
|
142
|
-
|
143
|
-
def root_path
|
144
|
-
Pathname.new(ROOT_PATH)
|
145
|
-
end
|
146
|
-
|
147
|
-
def authority_path
|
148
|
-
root_path.join("authorities/#{authority}")
|
149
|
-
end
|
150
|
-
|
151
|
-
def storage_path
|
152
|
-
authority_path.join("storage")
|
153
|
-
end
|
154
|
-
|
155
|
-
def storage_pattern
|
156
|
-
storage_path.join("**/*")
|
157
|
-
end
|
158
|
-
|
159
|
-
def serialize
|
160
|
-
{
|
161
|
-
authority: @authority,
|
162
|
-
schemes: @schemes.serialize,
|
163
|
-
paths: @paths.serialize,
|
164
|
-
storage_path: storage_path.to_s,
|
165
|
-
}
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
class RootPolicy < Policy
|
170
|
-
AUTHORITY = "terminalwire.com".freeze
|
171
|
-
|
172
|
-
# Ensure the binary stubs are executable. This increases the
|
173
|
-
# file mode entitlement so that stubs created in ./bin are executable.
|
174
|
-
BINARY_PATH_FILE_MODE = 0o755
|
175
|
-
|
176
|
-
def initialize(*, **, &)
|
177
|
-
# Make damn sure the authority is set to Terminalwire.
|
178
|
-
super(*, authority: AUTHORITY, **, &)
|
179
|
-
|
180
|
-
# Now setup special permitted paths.
|
181
|
-
@paths.permit root_path
|
182
|
-
@paths.permit root_pattern
|
183
|
-
|
184
|
-
# Permit terminalwire to grant execute permissions to the binary stubs.
|
185
|
-
@paths.permit binary_pattern, mode: BINARY_PATH_FILE_MODE
|
186
|
-
end
|
187
|
-
|
188
|
-
# Grant access to the `~/.terminalwire/**/*` path so users can install
|
189
|
-
# terminalwire apps via `terminalwire install svbtle`, etc.
|
190
|
-
def root_pattern
|
191
|
-
root_path.join("**/*")
|
192
|
-
end
|
193
|
-
|
194
|
-
# Path where the terminalwire binary stubs are stored.
|
195
|
-
def binary_path
|
196
|
-
root_path.join("bin")
|
197
|
-
end
|
198
|
-
|
199
|
-
# Pattern for the binary path.
|
200
|
-
def binary_pattern
|
201
|
-
binary_path.join("*")
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def self.resolve(*, authority:, **, &)
|
206
|
-
case authority
|
207
|
-
when RootPolicy::AUTHORITY
|
208
|
-
RootPolicy.new(*, **, &)
|
209
|
-
else
|
210
|
-
Policy.new *, authority:, **, &
|
211
|
-
end
|
212
|
-
end
|
213
8
|
end
|
214
9
|
end
|
@@ -3,6 +3,15 @@ require "yaml"
|
|
3
3
|
require "uri"
|
4
4
|
|
5
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.
|
6
15
|
class Exec
|
7
16
|
attr_reader :arguments, :path, :configuration, :url
|
8
17
|
|
@@ -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
|
@@ -55,11 +55,30 @@ module Terminalwire::Client::Resource
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
+
protected
|
59
|
+
|
58
60
|
def permit(...)
|
59
61
|
false
|
60
62
|
end
|
61
63
|
end
|
62
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
|
+
|
63
82
|
class STDOUT < Base
|
64
83
|
def connect
|
65
84
|
@io = $stdout
|
@@ -73,6 +92,8 @@ module Terminalwire::Client::Resource
|
|
73
92
|
@io.puts(data)
|
74
93
|
end
|
75
94
|
|
95
|
+
protected
|
96
|
+
|
76
97
|
def permit(...)
|
77
98
|
true
|
78
99
|
end
|
@@ -97,6 +118,8 @@ module Terminalwire::Client::Resource
|
|
97
118
|
@io.getpass
|
98
119
|
end
|
99
120
|
|
121
|
+
protected
|
122
|
+
|
100
123
|
def permit(...)
|
101
124
|
true
|
102
125
|
end
|
@@ -118,7 +141,7 @@ module Terminalwire::Client::Resource
|
|
118
141
|
end
|
119
142
|
|
120
143
|
def delete(path:)
|
121
|
-
File.delete
|
144
|
+
File.delete File.expand_path(path)
|
122
145
|
end
|
123
146
|
|
124
147
|
def exist(path:)
|
@@ -126,10 +149,11 @@ module Terminalwire::Client::Resource
|
|
126
149
|
end
|
127
150
|
|
128
151
|
def change_mode(path:, mode:)
|
129
|
-
File.chmod
|
152
|
+
File.chmod mode, File.expand_path(path)
|
130
153
|
end
|
131
154
|
|
132
155
|
protected
|
156
|
+
|
133
157
|
def permit(command, path:, mode: nil, **)
|
134
158
|
@entitlement.paths.permitted? path, mode:
|
135
159
|
end
|
@@ -139,7 +163,7 @@ module Terminalwire::Client::Resource
|
|
139
163
|
File = ::File
|
140
164
|
|
141
165
|
def list(path:)
|
142
|
-
Dir.glob
|
166
|
+
Dir.glob path
|
143
167
|
end
|
144
168
|
|
145
169
|
def create(path:)
|
@@ -149,28 +173,32 @@ module Terminalwire::Client::Resource
|
|
149
173
|
end
|
150
174
|
|
151
175
|
def exist(path:)
|
152
|
-
Dir.exist?
|
176
|
+
Dir.exist? path
|
153
177
|
end
|
154
178
|
|
155
179
|
def delete(path:)
|
156
|
-
Dir.delete
|
180
|
+
Dir.delete path
|
157
181
|
end
|
158
182
|
|
183
|
+
protected
|
184
|
+
|
159
185
|
def permit(command, path:, **)
|
160
186
|
@entitlement.paths.permitted? path
|
161
187
|
end
|
162
188
|
end
|
163
189
|
|
164
190
|
class Browser < Base
|
165
|
-
def permit(command, url:, **)
|
166
|
-
@entitlement.schemes.permitted? url
|
167
|
-
end
|
168
|
-
|
169
191
|
def launch(url:)
|
170
192
|
Launchy.open(URI(url))
|
171
193
|
# TODO: This is a hack to get the `respond` method to work.
|
172
194
|
# Maybe explicitly call a `suceed` and `fail` method?
|
173
195
|
nil
|
174
196
|
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
def permit(command, url:, **)
|
201
|
+
@entitlement.schemes.permitted? url
|
202
|
+
end
|
175
203
|
end
|
176
204
|
end
|
@@ -3,72 +3,72 @@ require "base64"
|
|
3
3
|
require "uri"
|
4
4
|
require "fileutils"
|
5
5
|
|
6
|
-
module Terminalwire
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def key
|
19
|
+
Base64.urlsafe_encode64 @url
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
+
def cache = @cache_store.entry key
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
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
44
|
end
|
45
|
-
else
|
46
|
-
return cache.value
|
47
45
|
end
|
46
|
+
else
|
47
|
+
return cache.value
|
48
48
|
end
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
def message
|
52
|
+
payload.dig(:shell, :output)
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
+
protected
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
57
|
+
def verification_url
|
58
|
+
Terminalwire.url
|
59
|
+
.path("/licenses/verifications", key)
|
60
|
+
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
end
|
70
|
+
def self.unpack(pack)
|
71
|
+
MessagePack.unpack(pack, symbolize_keys: true)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
data/lib/terminalwire/client.rb
CHANGED
@@ -1,73 +1,16 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'launchy'
|
3
3
|
require 'io/console'
|
4
|
+
require 'pathname'
|
4
5
|
|
5
6
|
module Terminalwire
|
6
7
|
module Client
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
include Logging
|
11
|
-
|
12
|
-
attr_reader :adapter, :resources, :endpoint
|
13
|
-
attr_accessor :entitlement
|
14
|
-
|
15
|
-
def initialize(adapter, arguments: ARGV, program_name: $0, endpoint:)
|
16
|
-
@endpoint = endpoint
|
17
|
-
@adapter = adapter
|
18
|
-
@program_arguments = arguments
|
19
|
-
@program_name = program_name
|
20
|
-
@entitlement = Entitlement.resolve(authority: @endpoint.authority)
|
21
|
-
|
22
|
-
yield self if block_given?
|
23
|
-
|
24
|
-
@resources = Resource::Handler.new do |it|
|
25
|
-
it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
|
26
|
-
it << Resource::STDIN.new("stdin", @adapter, entitlement:)
|
27
|
-
it << Resource::STDERR.new("stderr", @adapter, entitlement:)
|
28
|
-
it << Resource::Browser.new("browser", @adapter, entitlement:)
|
29
|
-
it << Resource::File.new("file", @adapter, entitlement:)
|
30
|
-
it << Resource::Directory.new("directory", @adapter, entitlement:)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def verify_license
|
35
|
-
# Connect to the Terminalwire license server to verify the URL endpoint
|
36
|
-
# and displays a message to the user, if any are present.
|
37
|
-
$stdout.print Terminalwire::Client::ServerLicenseVerification.new(url: @endpoint.to_url).message
|
38
|
-
rescue
|
39
|
-
$stderr.puts "Failed to verify server license."
|
40
|
-
end
|
41
|
-
|
42
|
-
def connect
|
43
|
-
verify_license
|
44
|
-
|
45
|
-
@adapter.write(
|
46
|
-
event: "initialization",
|
47
|
-
protocol: { version: VERSION },
|
48
|
-
entitlement: @entitlement.serialize,
|
49
|
-
program: {
|
50
|
-
name: @program_name,
|
51
|
-
arguments: @program_arguments
|
52
|
-
}
|
53
|
-
)
|
54
|
-
|
55
|
-
loop do
|
56
|
-
handle @adapter.read
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def handle(message)
|
61
|
-
case message
|
62
|
-
in { event: "resource", action: "command", name:, parameters: }
|
63
|
-
@resources.dispatch(**message)
|
64
|
-
in { event: "exit", status: }
|
65
|
-
exit Integer(status)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
8
|
+
ROOT_PATH = "~/.terminalwire".freeze
|
9
|
+
def self.root_path = Pathname.new(ENV.fetch("TERMINALWIRE_HOME", ROOT_PATH))
|
69
10
|
|
70
11
|
def self.websocket(url:, arguments: ARGV, &configuration)
|
12
|
+
ENV["TERMINALWIRE_HOME"] ||= root_path.to_s
|
13
|
+
|
71
14
|
url = URI(url)
|
72
15
|
|
73
16
|
Async do |task|
|
@@ -1,26 +1,48 @@
|
|
1
1
|
require "fileutils"
|
2
2
|
|
3
3
|
module Terminalwire::Server
|
4
|
+
# Contains all of the resources that are accessible to the server on the client-side.
|
5
|
+
# It's the primary interface for the server to interact with the client and is integrated
|
6
|
+
# into other libraries like Thor, etc.
|
4
7
|
class Context
|
5
8
|
extend Forwardable
|
6
9
|
|
7
|
-
attr_reader
|
10
|
+
attr_reader \
|
11
|
+
:stdout, :stdin, :stderr,
|
12
|
+
:browser,
|
13
|
+
:file, :directory,
|
14
|
+
:environment_variable,
|
15
|
+
:authority,
|
16
|
+
:root_path,
|
17
|
+
:authority_path,
|
18
|
+
:storage_path
|
8
19
|
|
9
20
|
def_delegators :@stdout, :puts, :print
|
10
21
|
def_delegators :@stdin, :gets, :getpass
|
11
22
|
|
12
23
|
def initialize(adapter:, entitlement:)
|
13
24
|
@adapter = adapter
|
14
|
-
|
15
25
|
@entitlement = entitlement
|
16
|
-
@storage_path = Pathname.new(entitlement.fetch(:storage_path))
|
17
26
|
|
27
|
+
# Initialize resources
|
18
28
|
@stdout = Resource::STDOUT.new("stdout", @adapter)
|
19
29
|
@stdin = Resource::STDIN.new("stdin", @adapter)
|
20
30
|
@stderr = Resource::STDERR.new("stderr", @adapter)
|
21
31
|
@browser = Resource::Browser.new("browser", @adapter)
|
22
32
|
@file = Resource::File.new("file", @adapter)
|
23
33
|
@directory = Resource::Directory.new("directory", @adapter)
|
34
|
+
@environment_variable = Resource::EnvironmentVariable.new("environment_variable", @adapter)
|
35
|
+
|
36
|
+
# Authority is provided by the client.
|
37
|
+
@authority = @entitlement.fetch(:authority)
|
38
|
+
# The Terminalwire home path is provided by the client and set
|
39
|
+
# as an environment variable.
|
40
|
+
@root_path = Pathname.new(
|
41
|
+
@environment_variable.read("TERMINALWIRE_HOME")
|
42
|
+
)
|
43
|
+
# Now derive the rest of the paths from the Terminalwire home path.
|
44
|
+
@authority_path = @root_path.join("authorities", @authority)
|
45
|
+
@storage_path = @authority_path.join("storage")
|
24
46
|
|
25
47
|
if block_given?
|
26
48
|
begin
|
@@ -31,6 +53,22 @@ module Terminalwire::Server
|
|
31
53
|
end
|
32
54
|
end
|
33
55
|
|
56
|
+
# Wraps the environment variables in a hash-like object that can be accessed
|
57
|
+
# from client#ENV. This makes it look and feel just like the ENV object in Ruby.
|
58
|
+
class Env
|
59
|
+
def initialize(context:)
|
60
|
+
@context = context
|
61
|
+
end
|
62
|
+
|
63
|
+
def [](name)
|
64
|
+
@context.environment_variable.read(name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def ENV
|
69
|
+
@ENV ||= Env.new(context: self)
|
70
|
+
end
|
71
|
+
|
34
72
|
def exit(status = 0)
|
35
73
|
@adapter.write(event: "exit", status: status)
|
36
74
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
module Terminalwire::Server
|
2
|
+
# Representation of the resources avilable to the server on the client-side. These
|
3
|
+
# classes encapsulate the API alls to the client and provide a more Ruby-like interface.
|
2
4
|
module Resource
|
3
5
|
class Base < Terminalwire::Resource::Base
|
4
6
|
private
|
@@ -22,6 +24,17 @@ module Terminalwire::Server
|
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
27
|
+
class EnvironmentVariable < Base
|
28
|
+
# Accepts a list of environment variables to permit.
|
29
|
+
def read(name)
|
30
|
+
command("read", name:)
|
31
|
+
end
|
32
|
+
|
33
|
+
# def write(name:, value:)
|
34
|
+
# command("write", name:, value:)
|
35
|
+
# end
|
36
|
+
end
|
37
|
+
|
25
38
|
class STDOUT < Base
|
26
39
|
def puts(data)
|
27
40
|
command("print_line", data: data)
|
data/lib/terminalwire/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: terminalwire
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brad Gessler
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async-websocket
|
@@ -178,7 +178,12 @@ files:
|
|
178
178
|
- lib/terminalwire/cache.rb
|
179
179
|
- lib/terminalwire/client.rb
|
180
180
|
- lib/terminalwire/client/entitlement.rb
|
181
|
+
- lib/terminalwire/client/entitlement/environment_variables.rb
|
182
|
+
- lib/terminalwire/client/entitlement/paths.rb
|
183
|
+
- lib/terminalwire/client/entitlement/policy.rb
|
184
|
+
- lib/terminalwire/client/entitlement/schemes.rb
|
181
185
|
- lib/terminalwire/client/exec.rb
|
186
|
+
- lib/terminalwire/client/handler.rb
|
182
187
|
- lib/terminalwire/client/resource.rb
|
183
188
|
- lib/terminalwire/client/server_license_verification.rb
|
184
189
|
- lib/terminalwire/logging.rb
|
@@ -213,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
213
218
|
- !ruby/object:Gem::Version
|
214
219
|
version: '0'
|
215
220
|
requirements: []
|
216
|
-
rubygems_version: 3.5.
|
221
|
+
rubygems_version: 3.5.16
|
217
222
|
signing_key:
|
218
223
|
specification_version: 4
|
219
224
|
summary: Ship a CLI for your web app. No API required.
|