terminalwire 0.1.7 → 0.1.8

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: 8bd8279b508f75e2d707b7dc94d58321694f3920de9781b65e45f44e3c83595e
4
- data.tar.gz: 9899e73daed242f8516a03c419f5c07c6f675399fff147125dca99d4515f847a
3
+ metadata.gz: 021c12812432f96320c0ba322f4aa71e1b1cfb60c52b77bc6cd78013da78c517
4
+ data.tar.gz: b5f13d773a7c7bc4d7f5e358c2fb0d498a9061fb6cb191f9ab55bc6fe3131767
5
5
  SHA512:
6
- metadata.gz: 9136ff6080599802796f2b96089785341128b3a2c814eb53527d47c993b7f21da000a716f4b7aad920bd2da91ff06bb88c34170db83636218d6b7351e2a7ce6d
7
- data.tar.gz: 114660aa187d13544186409f6b65c9ff2ff429050ab9df49c70440b326c0f1961b30571e81227cc8b44d9c23059915c93ef0bf821ef77eea150100da57542d3b
6
+ metadata.gz: f3206e529d8628243061e87cf4b1782e4d61a368333627e2526f5e251e25daefec1f8a4e24fb71e85c5aa3f1114138eb99100877c029668913fc93230d626eaa
7
+ data.tar.gz: c5ae063fc7eb0f17fe6a6da9b18d79cf5f5c039262849bbb24bc33add9276294f78335a262247c86cab2a8667494de9411532f4bcd983425cdeecc3e5d51d83e
@@ -1,8 +1,9 @@
1
1
  require 'msgpack'
2
2
 
3
- module Terminalwire
4
- class Adapter
5
- include Logging
3
+ module Terminalwire::Adapter
4
+ # Works with TCP, Unix, WebSocket, and other socket-like abstractions.
5
+ class Socket
6
+ include Terminalwire::Logging
6
7
 
7
8
  attr_reader :transport
8
9
 
@@ -29,4 +30,24 @@ module Terminalwire
29
30
  @transport.close
30
31
  end
31
32
  end
33
+
34
+ # This is a test adapter that can be used for testing purposes.
35
+ class Test
36
+ attr_reader :responses
37
+
38
+ def initialize(responses = [])
39
+ @responses = responses
40
+ end
41
+
42
+ def write(**data)
43
+ @responses << data
44
+ end
45
+
46
+ def response
47
+ @responses.pop
48
+ end
49
+
50
+ def close
51
+ end
52
+ end
32
53
  end
@@ -3,6 +3,73 @@ require "pathname"
3
3
  module Terminalwire::Client
4
4
  module Entitlement
5
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
+
6
73
  include Enumerable
7
74
 
8
75
  def initialize
@@ -13,23 +80,20 @@ module Terminalwire::Client
13
80
  @permitted.each(&)
14
81
  end
15
82
 
16
- def permit(path)
17
- @permitted.append Pathname.new(path).expand_path
83
+ def permit(path, **)
84
+ @permitted.append Permit.new(path:, **)
18
85
  end
19
86
 
20
- def permitted?(path)
21
- @permitted.find { |pattern| matches?(permitted: pattern, path:) }
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
22
93
  end
23
94
 
24
95
  def serialize
25
- @permitted.to_a.map(&:to_s)
26
- end
27
-
28
- private
29
- def matches?(permitted:, 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 permitted.to_s, File.expand_path(path), File::FNM_PATHNAME
96
+ map(&:serialize)
33
97
  end
34
98
  end
35
99
 
@@ -105,6 +169,10 @@ module Terminalwire::Client
105
169
  class RootPolicy < Policy
106
170
  HOST = "terminalwire.com".freeze
107
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
+
108
176
  def initialize(*, **, &)
109
177
  # Make damn sure the authority is set to Terminalwire.
110
178
  super(*, authority: HOST, **, &)
@@ -112,6 +180,9 @@ module Terminalwire::Client
112
180
  # Now setup special permitted paths.
113
181
  @paths.permit root_path
114
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
115
186
  end
116
187
 
117
188
  # Grant access to the `~/.terminalwire/**/*` path so users can install
@@ -119,6 +190,16 @@ module Terminalwire::Client
119
190
  def root_pattern
120
191
  root_path.join("**/*")
121
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
122
203
  end
123
204
 
124
205
  def self.from_url(url)
@@ -102,28 +102,16 @@ module Terminalwire::Client::Resource
102
102
  class File < Base
103
103
  File = ::File
104
104
 
105
- # Ensure the default file mode is read/write for owner only. This ensures
106
- # that if the server tries uploading an executable file, it won't be when it
107
- # lands on the client.
108
- #
109
- # Eventually we'll move this into entitlements so the client can set maximum
110
- # permissions for files and directories.
111
- FILE_PERMISSIONS = 0o600 # rw-------
112
-
113
105
  def read(path:)
114
106
  File.read File.expand_path(path)
115
107
  end
116
108
 
117
- def write(path:, content:)
118
- File.open(File.expand_path(path), "w", FILE_PERMISSIONS) { |f| f.write(content) }
119
- end
120
-
121
- def append(path:, content:)
122
- File.open(File.expand_path(path), "a", FILE_PERMISSIONS) { |f| f.write(content) }
109
+ def write(path:, content:, mode: nil)
110
+ File.open(File.expand_path(path), "w", mode) { |f| f.write(content) }
123
111
  end
124
112
 
125
- def mkdir(path:)
126
- FileUtils.mkdir_p(File.expand_path(path))
113
+ def append(path:, content:, mode: nil)
114
+ File.open(File.expand_path(path), "a", mode) { |f| f.write(content) }
127
115
  end
128
116
 
129
117
  def delete(path:)
@@ -134,6 +122,37 @@ module Terminalwire::Client::Resource
134
122
  File.exist? File.expand_path(path)
135
123
  end
136
124
 
125
+ def change_mode(path:, mode:)
126
+ File.chmod(mode, File.expand_path(path))
127
+ end
128
+
129
+ protected
130
+ def permit(command, path:, mode: nil, **)
131
+ @entitlement.paths.permitted? path, mode:
132
+ end
133
+ end
134
+
135
+ class Directory < Base
136
+ File = ::File
137
+
138
+ def list(path:)
139
+ Dir.glob(File.expand_path(path))
140
+ end
141
+
142
+ def create(path:)
143
+ Dir.mkdir(File.expand_path(path))
144
+ rescue Errno::EEXIST
145
+ # Do nothing
146
+ end
147
+
148
+ def exist(path:)
149
+ Dir.exist? File.expand_path(path)
150
+ end
151
+
152
+ def delete(path:)
153
+ Dir.delete(File.expand_path(path))
154
+ end
155
+
137
156
  def permit(command, path:, **)
138
157
  @entitlement.paths.permitted? path
139
158
  end
@@ -23,6 +23,7 @@ module Terminalwire
23
23
  it << Resource::STDERR.new("stderr", @adapter, entitlement:)
24
24
  it << Resource::Browser.new("browser", @adapter, entitlement:)
25
25
  it << Resource::File.new("file", @adapter, entitlement:)
26
+ it << Resource::Directory.new("directory", @adapter, entitlement:)
26
27
  end
27
28
  end
28
29
 
@@ -53,14 +54,14 @@ module Terminalwire
53
54
  def self.tcp(...)
54
55
  socket = TCPSocket.new(...)
55
56
  transport = Terminalwire::Transport::Socket.new(socket)
56
- adapter = Terminalwire::Adapter.new(transport)
57
+ adapter = Terminalwire::Adapter::Socket.new(transport)
57
58
  Terminalwire::Client::Handler.new(adapter)
58
59
  end
59
60
 
60
61
  def self.socket(...)
61
62
  socket = UNIXSocket.new(...)
62
63
  transport = Terminalwire::Transport::Socket.new(socket)
63
- adapter = Terminalwire::Adapter.new(transport)
64
+ adapter = Terminalwire::Adapter::Socket.new(transport)
64
65
  Terminalwire::Client::Handler.new(adapter)
65
66
  end
66
67
 
@@ -84,7 +85,7 @@ module Terminalwire
84
85
 
85
86
  Async::WebSocket::Client.connect(endpoint) do |adapter|
86
87
  transport = Terminalwire::Transport::WebSocket.new(adapter)
87
- adapter = Terminalwire::Adapter.new(transport)
88
+ adapter = Terminalwire::Adapter::Socket.new(transport)
88
89
  entitlement ||= Entitlement.from_url(url)
89
90
  Terminalwire::Client::Handler.new(adapter, arguments:, entitlement:).connect
90
91
  end
@@ -0,0 +1,42 @@
1
+ require "fileutils"
2
+
3
+ module Terminalwire::Server
4
+ class Context
5
+ extend Forwardable
6
+
7
+ attr_reader :stdout, :stdin, :stderr, :browser, :file, :directory, :storage_path
8
+
9
+ def_delegators :@stdout, :puts, :print
10
+ def_delegators :@stdin, :gets, :getpass
11
+
12
+ def initialize(adapter:, entitlement:)
13
+ @adapter = adapter
14
+
15
+ @entitlement = entitlement
16
+ @storage_path = Pathname.new(entitlement.fetch(:storage_path))
17
+
18
+ @stdout = Resource::STDOUT.new("stdout", @adapter)
19
+ @stdin = Resource::STDIN.new("stdin", @adapter)
20
+ @stderr = Resource::STDERR.new("stderr", @adapter)
21
+ @browser = Resource::Browser.new("browser", @adapter)
22
+ @file = Resource::File.new("file", @adapter)
23
+ @directory = Resource::Directory.new("directory", @adapter)
24
+
25
+ if block_given?
26
+ begin
27
+ yield self
28
+ ensure
29
+ exit
30
+ end
31
+ end
32
+ end
33
+
34
+ def exit(status = 0)
35
+ @adapter.write(event: "exit", status: status)
36
+ end
37
+
38
+ def close
39
+ @adapter.close
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ module Terminalwire::Server
2
+ module Resource
3
+ class Base < Terminalwire::Resource::Base
4
+ private
5
+
6
+ def command(command, **parameters)
7
+ @adapter.write(
8
+ event: "resource",
9
+ name: @name,
10
+ action: "command",
11
+ command: command,
12
+ parameters: parameters
13
+ )
14
+ @adapter.recv&.fetch(:response)
15
+ end
16
+ end
17
+
18
+ class STDOUT < Base
19
+ def puts(data)
20
+ command("print_line", data: data)
21
+ end
22
+
23
+ def print(data)
24
+ command("print", data: data)
25
+ end
26
+
27
+ def flush
28
+ # Do nothing
29
+ end
30
+ end
31
+
32
+ class STDERR < STDOUT
33
+ end
34
+
35
+ class STDIN < Base
36
+ def getpass
37
+ command("read_password")
38
+ end
39
+
40
+ def gets
41
+ command("read_line")
42
+ end
43
+ end
44
+
45
+ class File < Base
46
+ def read(path)
47
+ command("read", path: path.to_s)
48
+ end
49
+
50
+ def write(path, content)
51
+ command("write", path: path.to_s, content:)
52
+ end
53
+
54
+ def append(path, content)
55
+ command("append", path: path.to_s, content:)
56
+ end
57
+
58
+ def delete(path)
59
+ command("delete", path: path.to_s)
60
+ end
61
+ alias :rm :delete
62
+
63
+ def exist?(path)
64
+ command("exist", path: path.to_s)
65
+ end
66
+
67
+ def change_mode(path, mode)
68
+ command("change_mode", path: path.to_s, mode:)
69
+ end
70
+ alias :chmod :change_mode
71
+ end
72
+
73
+ class Directory < Base
74
+ def list(path)
75
+ command("list", path: path.to_s)
76
+ end
77
+ alias :ls :list
78
+
79
+ def create(path)
80
+ command("create", path: path.to_s)
81
+ end
82
+ alias :mkdir :create
83
+
84
+ def exist?(path)
85
+ command("exist", path: path.to_s)
86
+ end
87
+
88
+ def delete(path)
89
+ command("delete", path: path.to_s)
90
+ end
91
+ alias :rm :delete
92
+ end
93
+
94
+ class Browser < Base
95
+ def launch(url)
96
+ command("launch", url: url)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -2,121 +2,6 @@ require "thor"
2
2
 
3
3
  module Terminalwire
4
4
  module Server
5
- module Resource
6
- class Base < Terminalwire::Resource::Base
7
- private
8
-
9
- def command(command, **parameters)
10
- @adapter.write(
11
- event: "resource",
12
- name: @name,
13
- action: "command",
14
- command: command,
15
- parameters: parameters
16
- )
17
- @adapter.recv&.fetch(:response)
18
- end
19
- end
20
-
21
- class STDOUT < Base
22
- def puts(data)
23
- command("print_line", data: data)
24
- end
25
-
26
- def print(data)
27
- command("print", data: data)
28
- end
29
-
30
- def flush
31
- # Do nothing
32
- end
33
- end
34
-
35
- class STDERR < STDOUT
36
- end
37
-
38
- class STDIN < Base
39
- def getpass
40
- command("read_password")
41
- end
42
-
43
- def gets
44
- command("read_line")
45
- end
46
- end
47
-
48
- class File < Base
49
- def read(path)
50
- command("read", path: path.to_s)
51
- end
52
-
53
- def write(path, content)
54
- command("write", path: path.to_s, content:)
55
- end
56
-
57
- def append(path, content)
58
- command("append", path: path.to_s, content:)
59
- end
60
-
61
- def mkdir(path)
62
- command("mkdir", path: path.to_s)
63
- end
64
-
65
- def delete(path)
66
- command("delete", path: path.to_s)
67
- end
68
-
69
- def exist?(path)
70
- command("exist", path: path.to_s)
71
- end
72
- end
73
-
74
- class Browser < Base
75
- def launch(url)
76
- command("launch", url: url)
77
- end
78
- end
79
- end
80
-
81
- class Context
82
- extend Forwardable
83
-
84
- attr_reader :stdout, :stdin, :stderr, :browser, :file, :storage_path
85
-
86
- def_delegators :@stdout, :puts, :print
87
- def_delegators :@stdin, :gets, :getpass
88
-
89
- def initialize(adapter:, entitlement:)
90
- @adapter = adapter
91
-
92
- # TODO: Encapsulate entitlement in a class instead of a hash.
93
- @entitlement = entitlement
94
- @storage_path = Pathname.new(entitlement.fetch(:storage_path))
95
-
96
- @stdout = Server::Resource::STDOUT.new("stdout", @adapter)
97
- @stdin = Server::Resource::STDIN.new("stdin", @adapter)
98
- @stderr = Server::Resource::STDERR.new("stderr", @adapter)
99
- @browser = Server::Resource::Browser.new("browser", @adapter)
100
- @file = Server::Resource::File.new("file", @adapter)
101
-
102
- if block_given?
103
- begin
104
- yield self
105
- ensure
106
- exit
107
- end
108
- end
109
- end
110
-
111
- def exit(status = 0)
112
- @adapter.write(event: "exit", status: status)
113
- end
114
-
115
- def close
116
- @adapter.close
117
- end
118
- end
119
-
120
5
  class MyCLI < ::Thor
121
6
  include Terminalwire::Thor
122
7
 
@@ -161,7 +46,7 @@ module Terminalwire
161
46
 
162
47
  def call(env)
163
48
  Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
164
- run(Adapter.new(Terminalwire::Transport::WebSocket.new(connection)))
49
+ run(Adapter::Socket.new(Terminalwire::Transport::WebSocket.new(connection)))
165
50
  end or [200, { "Content-Type" => "text/plain" }, ["Connect via WebSockets"]]
166
51
  end
167
52
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
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.7
4
+ version: 0.1.8
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-09-20 00:00:00.000000000 Z
11
+ date: 2024-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-websocket
@@ -167,6 +167,8 @@ files:
167
167
  - lib/terminalwire/logging.rb
168
168
  - lib/terminalwire/rails.rb
169
169
  - lib/terminalwire/server.rb
170
+ - lib/terminalwire/server/context.rb
171
+ - lib/terminalwire/server/resource.rb
170
172
  - lib/terminalwire/thor.rb
171
173
  - lib/terminalwire/transport.rb
172
174
  - lib/terminalwire/version.rb