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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ab3f5780e6c3f7f544936130b288676ad60b29ef05944842771bcb3ca51192a
4
- data.tar.gz: 4bd9a47eb0541ca81dd53cc19f6bf5ca02e388f22048b1fceaf7ac0f910e557d
3
+ metadata.gz: 39232fa4bba65cc98bf87a3625e80a8c01ccee009277adeb53bd57efe28fd73c
4
+ data.tar.gz: 94ceb432cdd3f10fb3492cdfb5025eb3e71edbe352bdf0e2cd2a8438aef6f22d
5
5
  SHA512:
6
- metadata.gz: e37f803cf8777f66aa6f8f19cd72fb0ca778e06b1f39a7762b8e3ed74b494dee210f025af65063b6d60321d9d6e9c20fa00b81dcd61b1ca7efd9193a6bf5e310
7
- data.tar.gz: 25b6c73db710026062340f779bee11738952bd86efb80bef53f4d92cdef9599e917e85a6d979ee559fa422c3f73b4f8ffd5d7e9d87d81f26a531bab8603c1e68
6
+ metadata.gz: a05475187aa41b8bb7b3e21ae13975a48f739ee95e00a2f099e7d13817a5192b9298a79236e008668416d8f385c31b02d716c404255e96148c385ff6268d4f10
7
+ data.tar.gz: 36fff6322f43fd783906e9e8a1878dd192b0b44695134cc6df12085da305f8cd34a758415ebb8d14a7d21ec4860172eb250c94508da27f5728149bf7cf76cbd8
@@ -1,7 +1,7 @@
1
1
  require 'msgpack'
2
2
 
3
3
  module Terminalwire::Adapter
4
- # Works with TCP, Unix, WebSocket, and other socket-like abstractions.
4
+ # Works with Test, TCP, Unix, WebSocket, and other socket-like abstractions.
5
5
  class Socket
6
6
  include Terminalwire::Logging
7
7
 
@@ -4,6 +4,7 @@ require "base64"
4
4
  require "time"
5
5
  require "fileutils"
6
6
 
7
+ # Caches used on the client side for licesens, HTTP requests, etc.
7
8
  module Terminalwire::Cache
8
9
  module File
9
10
  # Hoist the File class to avoid conflicts with the standard library.
@@ -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(File.expand_path(path))
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(mode, File.expand_path(path))
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 File.expand_path(path)
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? File.expand_path(path)
176
+ Dir.exist? path
153
177
  end
154
178
 
155
179
  def delete(path:)
156
- Dir.delete(File.expand_path(path))
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
- module Client
8
- class ServerLicenseVerification
9
- include Logging
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
- def initialize(url:)
12
- @url = URI(url)
13
- @internet = Async::HTTP::Internet.new
14
- @cache_store = Terminalwire::Cache::File::Store.new(path: "~/.terminalwire/cache/licenses/verifications")
15
- end
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
- def key
18
- Base64.urlsafe_encode64 @url
19
- end
18
+ def key
19
+ Base64.urlsafe_encode64 @url
20
+ end
20
21
 
21
- def cache = @cache_store.entry key
22
+ def cache = @cache_store.entry key
22
23
 
23
- def payload
24
- if cache.miss?
25
- logger.debug "Stale verification. Requesting new verification."
26
- request do |response|
27
- # Set the expiry on the file cache for the header.
28
- if max_age = response.headers["cache-control"].max_age
29
- logger.debug "Caching for #{max_age}"
30
- cache.expires = Time.now + max_age
31
- end
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
- # Process based on the response code.
34
- case response.status
35
- in 200
36
- logger.debug "License for #{@url} found."
37
- data = self.class.unpack response.read
38
- cache.value = data
39
- return data
40
- in 404
41
- logger.debug "License for #{@url} not found."
42
- return self.class.unpack response.read
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
- def message
51
- payload.dig(:shell, :output)
52
- end
51
+ def message
52
+ payload.dig(:shell, :output)
53
+ end
53
54
 
54
- protected
55
+ protected
55
56
 
56
- def verification_url
57
- Terminalwire.url
58
- .path("/licenses/verifications", key)
59
- end
57
+ def verification_url
58
+ Terminalwire.url
59
+ .path("/licenses/verifications", key)
60
+ end
60
61
 
61
- def request(&)
62
- logger.debug "Requesting license verification from #{verification_url}."
63
- response = @internet.get verification_url, {
64
- "Accept" => "application/x-msgpack",
65
- "User-Agent" => "Terminalwire/#{Terminalwire::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})",
66
- }, &
67
- end
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
- def self.unpack(pack)
70
- MessagePack.unpack(pack, symbolize_keys: true)
71
- end
70
+ def self.unpack(pack)
71
+ MessagePack.unpack(pack, symbolize_keys: true)
72
72
  end
73
73
  end
74
74
  end
@@ -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
- class Handler
8
- VERSION = "0.1.0".freeze
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 :stdout, :stdin, :stderr, :browser, :file, :directory, :storage_path
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.17"
4
+ VERSION = "0.2.3"
5
5
  end
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.1.17
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-04 00:00:00.000000000 Z
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.3
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.