terminalwire 0.1.7 → 0.1.9

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: a0eeaa4d2a6f23524e964bbd0fa0e507eca1415f591e71a565a47553411cf916
4
+ data.tar.gz: 30d9f26ce42e8e5a4f301bd4d99ca5ead398554f737b0ffe693aa921fd8e8bb2
5
5
  SHA512:
6
- metadata.gz: 9136ff6080599802796f2b96089785341128b3a2c814eb53527d47c993b7f21da000a716f4b7aad920bd2da91ff06bb88c34170db83636218d6b7351e2a7ce6d
7
- data.tar.gz: 114660aa187d13544186409f6b65c9ff2ff429050ab9df49c70440b326c0f1961b30571e81227cc8b44d9c23059915c93ef0bf821ef77eea150100da57542d3b
6
+ metadata.gz: fb69226b00de7e49c73bcc1383aa5d31645cddd49d40c945131cb2e5b7c8c962a206c97127812278642c08371641e01a9a5d223f01c75551689d50a76257f691
7
+ data.tar.gz: 62412bf6431e84e49895887ca879045038bc046470b32863056db319f4e69e0f8ecfdfac2e9141543d3ecf0dcbc5d3ba9821046828bd05a40653ab60512db0a5
@@ -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)
@@ -1,3 +1,5 @@
1
+ require "fileutils"
2
+
1
3
  module Terminalwire::Client::Resource
2
4
  # Dispatches messages from the Client::Handler to the appropriate resource.
3
5
  class Handler
@@ -102,28 +104,16 @@ module Terminalwire::Client::Resource
102
104
  class File < Base
103
105
  File = ::File
104
106
 
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
107
  def read(path:)
114
108
  File.read File.expand_path(path)
115
109
  end
116
110
 
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) }
111
+ def write(path:, content:, mode: nil)
112
+ File.open(File.expand_path(path), "w", mode) { |f| f.write(content) }
123
113
  end
124
114
 
125
- def mkdir(path:)
126
- FileUtils.mkdir_p(File.expand_path(path))
115
+ def append(path:, content:, mode: nil)
116
+ File.open(File.expand_path(path), "a", mode) { |f| f.write(content) }
127
117
  end
128
118
 
129
119
  def delete(path:)
@@ -134,6 +124,37 @@ module Terminalwire::Client::Resource
134
124
  File.exist? File.expand_path(path)
135
125
  end
136
126
 
127
+ def change_mode(path:, mode:)
128
+ File.chmod(mode, File.expand_path(path))
129
+ end
130
+
131
+ protected
132
+ def permit(command, path:, mode: nil, **)
133
+ @entitlement.paths.permitted? path, mode:
134
+ end
135
+ end
136
+
137
+ class Directory < Base
138
+ File = ::File
139
+
140
+ def list(path:)
141
+ Dir.glob File.expand_path(path)
142
+ end
143
+
144
+ def create(path:)
145
+ FileUtils.mkdir_p File.expand_path(path)
146
+ rescue Errno::EEXIST
147
+ # Do nothing
148
+ end
149
+
150
+ def exist(path:)
151
+ Dir.exist? File.expand_path(path)
152
+ end
153
+
154
+ def delete(path:)
155
+ Dir.delete(File.expand_path(path))
156
+ end
157
+
137
158
  def permit(command, path:, **)
138
159
  @entitlement.paths.permitted? path
139
160
  end
@@ -17,12 +17,15 @@ module Terminalwire
17
17
  @program_arguments = arguments
18
18
  @program_name = program_name
19
19
 
20
+ FileUtils.mkdir_p entitlement.storage_path
21
+
20
22
  @resources = Resource::Handler.new do |it|
21
23
  it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
22
24
  it << Resource::STDIN.new("stdin", @adapter, entitlement:)
23
25
  it << Resource::STDERR.new("stderr", @adapter, entitlement:)
24
26
  it << Resource::Browser.new("browser", @adapter, entitlement:)
25
27
  it << Resource::File.new("file", @adapter, entitlement:)
28
+ it << Resource::Directory.new("directory", @adapter, entitlement:)
26
29
  end
27
30
  end
28
31
 
@@ -53,14 +56,14 @@ module Terminalwire
53
56
  def self.tcp(...)
54
57
  socket = TCPSocket.new(...)
55
58
  transport = Terminalwire::Transport::Socket.new(socket)
56
- adapter = Terminalwire::Adapter.new(transport)
59
+ adapter = Terminalwire::Adapter::Socket.new(transport)
57
60
  Terminalwire::Client::Handler.new(adapter)
58
61
  end
59
62
 
60
63
  def self.socket(...)
61
64
  socket = UNIXSocket.new(...)
62
65
  transport = Terminalwire::Transport::Socket.new(socket)
63
- adapter = Terminalwire::Adapter.new(transport)
66
+ adapter = Terminalwire::Adapter::Socket.new(transport)
64
67
  Terminalwire::Client::Handler.new(adapter)
65
68
  end
66
69
 
@@ -84,7 +87,7 @@ module Terminalwire
84
87
 
85
88
  Async::WebSocket::Client.connect(endpoint) do |adapter|
86
89
  transport = Terminalwire::Transport::WebSocket.new(adapter)
87
- adapter = Terminalwire::Adapter.new(transport)
90
+ adapter = Terminalwire::Adapter::Socket.new(transport)
88
91
  entitlement ||= Entitlement.from_url(url)
89
92
  Terminalwire::Client::Handler.new(adapter, arguments:, entitlement:).connect
90
93
  end
@@ -18,7 +18,7 @@ module Terminalwire::Rails
18
18
 
19
19
  def initialize(context:, path: nil, secret_key: self.class.secret_key)
20
20
  @context = context
21
- @path = path || context.storage_path
21
+ @path = Pathname.new(path || context.storage_path)
22
22
  @config_file_path = @path.join(FILENAME)
23
23
  @secret_key = secret_key
24
24
 
@@ -57,7 +57,7 @@ module Terminalwire::Rails
57
57
  def ensure_file
58
58
  return true if @context.file.exist? @config_file_path
59
59
  # Create the path if it doesn't exist on the client.
60
- @context.file.mkdir(@path) unless @context.file.exist?(@path)
60
+ @context.directory.create @path
61
61
  # Write an empty configuration on initialization
62
62
  write(EMPTY_SESSION)
63
63
  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,107 @@
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
+
15
+ response = @adapter.recv
16
+ case response.fetch(:status)
17
+ when "success"
18
+ response.fetch(:response)
19
+ when "failure"
20
+ raise Terminalwire::Error, response.inspect
21
+ end
22
+ end
23
+ end
24
+
25
+ class STDOUT < Base
26
+ def puts(data)
27
+ command("print_line", data: data)
28
+ end
29
+
30
+ def print(data)
31
+ command("print", data: data)
32
+ end
33
+
34
+ def flush
35
+ # Do nothing
36
+ end
37
+ end
38
+
39
+ class STDERR < STDOUT
40
+ end
41
+
42
+ class STDIN < Base
43
+ def getpass
44
+ command("read_password")
45
+ end
46
+
47
+ def gets
48
+ command("read_line")
49
+ end
50
+ end
51
+
52
+ class File < Base
53
+ def read(path)
54
+ command("read", path: path.to_s)
55
+ end
56
+
57
+ def write(path, content)
58
+ command("write", path: path.to_s, content:)
59
+ end
60
+
61
+ def append(path, content)
62
+ command("append", path: path.to_s, content:)
63
+ end
64
+
65
+ def delete(path)
66
+ command("delete", path: path.to_s)
67
+ end
68
+ alias :rm :delete
69
+
70
+ def exist?(path)
71
+ command("exist", path: path.to_s)
72
+ end
73
+
74
+ def change_mode(path, mode)
75
+ command("change_mode", path: path.to_s, mode:)
76
+ end
77
+ alias :chmod :change_mode
78
+ end
79
+
80
+ class Directory < Base
81
+ def list(path)
82
+ command("list", path: path.to_s)
83
+ end
84
+ alias :ls :list
85
+
86
+ def create(path)
87
+ command("create", path: path.to_s)
88
+ end
89
+ alias :mkdir :create
90
+
91
+ def exist?(path)
92
+ command("exist", path: path.to_s)
93
+ end
94
+
95
+ def delete(path)
96
+ command("delete", path: path.to_s)
97
+ end
98
+ alias :rm :delete
99
+ end
100
+
101
+ class Browser < Base
102
+ def launch(url)
103
+ command("launch", url: url)
104
+ end
105
+ end
106
+ end
107
+ 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.9"
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.9
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-28 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
@@ -194,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
196
  - !ruby/object:Gem::Version
195
197
  version: '0'
196
198
  requirements: []
197
- rubygems_version: 3.5.3
199
+ rubygems_version: 3.5.9
198
200
  signing_key:
199
201
  specification_version: 4
200
202
  summary: Ship a CLI for your web app. No API required.