terminalwire 0.1.7 → 0.1.8

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: 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