terminalwire 0.1.17 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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.