terminalwire 0.1.1 → 0.1.2

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.
@@ -0,0 +1,69 @@
1
+ require 'jwt'
2
+ require 'pathname'
3
+ require 'forwardable'
4
+
5
+ module Terminalwire::Rails
6
+ class Session
7
+ # JWT file name for the session file.
8
+ FILENAME = "session.jwt"
9
+
10
+ # Empty dictionary the user can stash all their session data into.
11
+ EMPTY_SESSION = {}.freeze
12
+
13
+ extend Forwardable
14
+
15
+ # Delegate `dig` and `fetch` to the `read` method
16
+ def_delegators :read,
17
+ :dig, :fetch, :[]
18
+
19
+ def initialize(context:, path: nil, secret_key: self.class.secret_key)
20
+ @context = context
21
+ @path = path || context.storage_path
22
+ @config_file_path = @path.join(FILENAME)
23
+ @secret_key = secret_key
24
+
25
+ ensure_file
26
+ end
27
+
28
+ def read
29
+ jwt_token = @context.file.read(@config_file_path)
30
+ decoded_data = JWT.decode(jwt_token, @secret_key, true, algorithm: 'HS256')
31
+ decoded_data[0] # JWT payload is the first element in the array
32
+ rescue JWT::DecodeError => e
33
+ raise "Invalid or tampered file: #{e.message}"
34
+ end
35
+
36
+ def reset
37
+ @context.file.delete @config_file_path
38
+ end
39
+
40
+ def edit
41
+ config = read
42
+ yield config
43
+ write(config)
44
+ end
45
+
46
+ def []=(key, value)
47
+ edit { |config| config[key] = value }
48
+ end
49
+
50
+ def write(config)
51
+ token = JWT.encode(config, @secret_key, 'HS256')
52
+ @context.file.write(@config_file_path, token)
53
+ end
54
+
55
+ private
56
+
57
+ def ensure_file
58
+ return true if @context.file.exist? @config_file_path
59
+ # Create the path if it doesn't exist on the client.
60
+ @context.file.mkdir(@path) unless @context.file.exist?(@path)
61
+ # Write an empty configuration on initialization
62
+ write(EMPTY_SESSION)
63
+ end
64
+
65
+ def self.secret_key
66
+ Rails.application.secret_key_base
67
+ end
68
+ end
69
+ end
@@ -1,144 +1,101 @@
1
1
  module Terminalwire
2
2
  module Server
3
3
  module Resource
4
- class IO < Terminalwire::Resource::Base
4
+ class Base < Terminalwire::Resource::Base
5
+ private
6
+
7
+ def command(command, **parameters)
8
+ @adapter.write(
9
+ event: "resource",
10
+ name: @name,
11
+ action: "command",
12
+ command: command,
13
+ parameters: parameters
14
+ )
15
+ @adapter.recv&.fetch(:response)
16
+ end
17
+ end
18
+
19
+ class STDOUT < Base
5
20
  def puts(data)
6
- command("puts", data: data)
21
+ command("print_line", data: data)
7
22
  end
8
23
 
9
24
  def print(data)
10
25
  command("print", data: data)
11
26
  end
12
27
 
13
- def gets
14
- command("gets")
15
- end
16
-
17
28
  def flush
18
- # @connection.flush
19
- end
20
-
21
- private
22
-
23
- def command(command, data: nil)
24
- @connection.write(event: "device", id: @id, action: "command", command: command, data: data)
25
- @connection.recv&.fetch(:response)
29
+ # Do nothing
26
30
  end
27
31
  end
28
32
 
29
- class STDOUT < IO
33
+ class STDERR < STDOUT
30
34
  end
31
35
 
32
- class STDIN < IO
36
+ class STDIN < Base
33
37
  def getpass
34
- command("getpass")
38
+ command("read_password")
35
39
  end
36
- end
37
40
 
38
- class STDERR < IO
41
+ def gets
42
+ command("read_line")
43
+ end
39
44
  end
40
45
 
41
- class File < Terminalwire::Resource::Base
46
+ class File < Base
42
47
  def read(path)
43
- command("read", path.to_s)
48
+ command("read", path: path.to_s)
44
49
  end
45
50
 
46
51
  def write(path, content)
47
- command("write", { 'path' => path.to_s, 'content' => content })
52
+ command("write", path: path.to_s, content:)
48
53
  end
49
54
 
50
55
  def append(path, content)
51
- command("append", { 'path' => path.to_s, 'content' => content })
56
+ command("append", path: path.to_s, content:)
52
57
  end
53
58
 
54
59
  def mkdir(path)
55
- command("mkdir", { 'path' => path.to_s })
60
+ command("mkdir", path: path.to_s)
56
61
  end
57
62
 
58
- def exist?(path)
59
- command("exist", { 'path' => path.to_s })
63
+ def delete(path)
64
+ command("delete", path: path.to_s)
60
65
  end
61
66
 
62
- private
63
-
64
- def command(action, data)
65
- @connection.write(event: "device", id: @id, action: "command", command: action, data: data)
66
- response = @connection.recv
67
- response.fetch(:response)
67
+ def exist?(path)
68
+ command("exist", path: path.to_s)
68
69
  end
69
70
  end
70
71
 
71
- class Browser < Terminalwire::Resource::Base
72
+ class Browser < Base
72
73
  def launch(url)
73
- command("launch", data: url)
74
- end
75
-
76
- private
77
-
78
- def command(command, data: nil)
79
- @connection.write(event: "device", id: @id, action: "command", command: command, data: data)
80
- @connection.recv.fetch(:response)
81
- end
82
- end
83
- end
84
-
85
- class ResourceMapper
86
- include Logging
87
-
88
- def initialize(connection, resources = self.class.resources)
89
- @id = -1
90
- @resources = resources
91
- @devices = Hash.new { |h,k| h[Integer(k)] }
92
- @connection = connection
93
- end
94
-
95
- def connect_device(type)
96
- id = next_id
97
- logger.debug "Server: Requesting client to connect device #{type} with ID #{id}"
98
- @connection.write(event: "device", action: "connect", id: id, type: type)
99
- response = @connection.recv
100
- case response
101
- in { status: "success" }
102
- logger.debug "Server: Resource #{type} connected with ID #{id}."
103
- @devices[id] = @resources.find(type).new(id, @connection)
104
- else
105
- logger.debug "Server: Failed to connect device #{type} with ID #{id}."
106
- end
107
- end
108
-
109
- private
110
-
111
- def next_id
112
- @id += 1
113
- end
114
-
115
- def self.resources
116
- ResourceRegistry.new.tap do |resources|
117
- resources << Server::Resource::STDOUT
118
- resources << Server::Resource::STDIN
119
- resources << Server::Resource::STDERR
120
- resources << Server::Resource::Browser
121
- resources << Server::Resource::File
74
+ command("launch", url: url)
122
75
  end
123
76
  end
124
77
  end
125
78
 
126
- class Session
79
+ class Context
127
80
  extend Forwardable
128
81
 
129
- attr_reader :stdout, :stdin, :stderr, :browser, :file
82
+ attr_reader :stdout, :stdin, :stderr, :browser, :file, :storage_path
130
83
 
131
84
  def_delegators :@stdout, :puts, :print
132
85
  def_delegators :@stdin, :gets, :getpass
133
86
 
134
- def initialize(connection:)
135
- @connection = connection
136
- @devices = ResourceMapper.new(@connection)
137
- @stdout = @devices.connect_device("stdout")
138
- @stdin = @devices.connect_device("stdin")
139
- @stderr = @devices.connect_device("stderr")
140
- @browser = @devices.connect_device("browser")
141
- @file = @devices.connect_device("file")
87
+ def initialize(adapter:, entitlement:)
88
+ @adapter = adapter
89
+
90
+ # TODO: Encapsulate entitlement in a class instead of a hash.
91
+ @entitlement = entitlement
92
+ @storage_path = Pathname.new(entitlement.fetch(:storage_path))
93
+
94
+ @stdout = Server::Resource::STDOUT.new("stdout", @adapter)
95
+ @stdin = Server::Resource::STDIN.new("stdin", @adapter)
96
+ @stderr = Server::Resource::STDERR.new("stderr", @adapter)
97
+ @browser = Server::Resource::Browser.new("browser", @adapter)
98
+ @file = Server::Resource::File.new("file", @adapter)
142
99
 
143
100
  if block_given?
144
101
  begin
@@ -149,18 +106,12 @@ module Terminalwire
149
106
  end
150
107
  end
151
108
 
152
- def exec(&shell)
153
- instance_eval(&shell)
154
- ensure
155
- exit
156
- end
157
-
158
109
  def exit(status = 0)
159
- @connection.write(event: "exit", status: status)
110
+ @adapter.write(event: "exit", status: status)
160
111
  end
161
112
 
162
113
  def close
163
- @connection.close
114
+ @adapter.close
164
115
  end
165
116
  end
166
117
 
@@ -182,7 +133,7 @@ module Terminalwire
182
133
  end
183
134
 
184
135
  def listen
185
- logger.info "Socket: Sistening..."
136
+ logger.info "Socket: Listening..."
186
137
  loop do
187
138
  client_socket = @server_socket.accept
188
139
  logger.debug "Socket: Client #{client_socket.inspect} connected"
@@ -194,37 +145,79 @@ module Terminalwire
194
145
 
195
146
  def handle_client(socket)
196
147
  transport = Transport::Socket.new(socket)
197
- connection = Connection.new(transport)
148
+ adapter = Adapter.new(transport)
198
149
 
199
150
  Thread.new do
200
- handler = Handler.new(connection)
151
+ handler = Handler.new(adapter)
201
152
  handler.run
202
153
  end
203
154
  end
204
155
  end
205
156
 
157
+ class WebSocket
158
+ include Logging
159
+
160
+ def call(env)
161
+ Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
162
+ run(Adapter.new(Terminalwire::Transport::WebSocket.new(connection)))
163
+ end or [200, { "Content-Type" => "text/plain" }, ["Connect via WebSockets"]]
164
+ end
165
+
166
+ private
167
+
168
+ def run(adapter)
169
+ while message = adapter.recv
170
+ puts message
171
+ end
172
+ end
173
+ end
174
+
175
+ class Thor < WebSocket
176
+ include Logging
177
+
178
+ def initialize(cli_class)
179
+ @cli_class = cli_class
180
+
181
+ unless @cli_class.included_modules.include?(Terminalwire::Thor)
182
+ raise 'Add `include Terminalwire::Thor` to the #{@cli_class.inspect} class.'
183
+ end
184
+ end
185
+
186
+ def run(adapter)
187
+ logger.info "ThorServer: Running #{@cli_class.inspect}"
188
+ while message = adapter.recv
189
+ case message
190
+ in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
191
+ Terminalwire::Server::Context.new(adapter:, entitlement:) do |context|
192
+ @cli_class.start(arguments, context:)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+
206
199
  class Handler
207
200
  include Logging
208
201
 
209
- def initialize(connection)
210
- @connection = connection
202
+ def initialize(adapter)
203
+ @adapter = adapter
211
204
  end
212
205
 
213
206
  def run
214
207
  logger.info "Server Handler: Running"
215
208
  loop do
216
- message = @connection.recv
209
+ message = @adapter.recv
217
210
  case message
218
- in { event: "initialize", arguments:, program_name: }
219
- Session.new(connection: @connection) do |session|
220
- MyCLI.start(arguments, session: session)
211
+ in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
212
+ Context.new(adapter: @adapter) do |context|
213
+ MyCLI.start(arguments, context:)
221
214
  end
222
215
  end
223
216
  end
224
217
  rescue EOFError, Errno::ECONNRESET
225
218
  logger.info "Server Handler: Client disconnected"
226
219
  ensure
227
- @connection.close
220
+ @adapter.close
228
221
  end
229
222
  end
230
223
 
@@ -236,4 +229,4 @@ module Terminalwire
236
229
  Server::Socket.new(UNIXServer.new(...))
237
230
  end
238
231
  end
239
- end
232
+ end
@@ -1,16 +1,20 @@
1
+ require 'thor'
2
+
1
3
  module Terminalwire
2
4
  module Thor
3
5
  class Shell < ::Thor::Shell::Basic
4
6
  extend Forwardable
5
7
 
6
- # Encapsulates all of the IO devices for a Terminalwire connection.
7
- attr_reader :session
8
+ # Encapsulates all of the IO resources for a Terminalwire adapter.
9
+ attr_reader :context, :session
8
10
 
9
- def_delegators :@session, :stdin, :stdout, :stderr
11
+ def_delegators :context,
12
+ :stdin, :stdout, :stderr
10
13
 
11
- def initialize(session)
12
- @session = session
13
- super()
14
+ def initialize(context, *, **, &)
15
+ @context = context
16
+ @session = Terminalwire::Rails::Session.new(context:)
17
+ super(*,**,&)
14
18
  end
15
19
  end
16
20
 
@@ -24,20 +28,24 @@ module Terminalwire
24
28
  protected
25
29
 
26
30
  no_commands do
27
- def_delegators :shell, :session
28
- def_delegators :session, :stdout, :stdin, :stderr, :browser
29
- def_delegators :stdout, :puts, :print
30
- def_delegators :stdin, :gets
31
+ def_delegators :shell,
32
+ :context, :session
33
+ def_delegators :context,
34
+ :stdout, :stdin, :stderr, :browser
35
+ def_delegators :stdout,
36
+ :puts, :print
37
+ def_delegators :stdin,
38
+ :gets
31
39
  end
32
40
  end
33
41
  end
34
42
 
35
43
  module ClassMethods
36
44
  def start(given_args = ARGV, config = {})
37
- session = config.delete(:session)
38
- config[:shell] = Shell.new(session) if session
45
+ context = config.delete(:context)
46
+ config[:shell] = Shell.new(context) if context
39
47
  super(given_args, config)
40
48
  end
41
49
  end
42
50
  end
43
- end
51
+ end
@@ -1,8 +1,16 @@
1
+ require 'uri'
2
+ require 'socket'
3
+ require 'async/websocket/client'
4
+
1
5
  module Terminalwire
2
6
  module Transport
3
7
  class Base
4
- def initialize
5
- raise NotImplementedError, "This is an abstract base class"
8
+ def self.connect(url)
9
+ raise NotImplementedError, "Subclass must implement .connect"
10
+ end
11
+
12
+ def self.listen(url)
13
+ raise NotImplementedError, "Subclass must implement .listen"
6
14
  end
7
15
 
8
16
  def read
@@ -18,25 +26,49 @@ module Terminalwire
18
26
  end
19
27
  end
20
28
 
21
- class WebSocket
22
- def initialize(websocket)
23
- @websocket = websocket
29
+ class TCP < Base
30
+ def self.connect(url)
31
+ uri = URI(url)
32
+ new(TCPSocket.new(uri.host, uri.port))
33
+ end
34
+
35
+ def self.listen(url)
36
+ uri = URI(url)
37
+ new(TCPServer.new(uri.host, uri.port))
38
+ end
39
+
40
+ def initialize(socket)
41
+ @socket = socket
24
42
  end
25
43
 
26
44
  def read
27
- @websocket.read&.buffer
45
+ length = @socket.read(4)
46
+ return nil if length.nil?
47
+ length = length.unpack('L>')[0]
48
+ @socket.read(length)
28
49
  end
29
50
 
30
51
  def write(data)
31
- @websocket.write(data)
52
+ length = [data.bytesize].pack('L>')
53
+ @socket.write(length + data)
32
54
  end
33
55
 
34
56
  def close
35
- @websocket.close
57
+ @socket.close
36
58
  end
37
59
  end
38
60
 
39
- class Socket < Base
61
+ class Unix < Base
62
+ def self.connect(url)
63
+ uri = URI(url)
64
+ new(UNIXSocket.new(uri.path))
65
+ end
66
+
67
+ def self.listen(url)
68
+ uri = URI(url)
69
+ new(UNIXServer.new(uri.path))
70
+ end
71
+
40
72
  def initialize(socket)
41
73
  @socket = socket
42
74
  end
@@ -57,5 +89,35 @@ module Terminalwire
57
89
  @socket.close
58
90
  end
59
91
  end
92
+
93
+ class WebSocket < Base
94
+ def self.connect(url)
95
+ uri = URI(url)
96
+ endpoint = Async::HTTP::Endpoint.parse(uri)
97
+ adapter = Async::WebSocket::Client.connect(endpoint)
98
+ new(adapter)
99
+ end
100
+
101
+ def self.listen(url)
102
+ # This would need to be implemented with a WebSocket server library
103
+ raise NotImplementedError, "WebSocket server not implemented"
104
+ end
105
+
106
+ def initialize(websocket)
107
+ @websocket = websocket
108
+ end
109
+
110
+ def read
111
+ @websocket.read&.buffer
112
+ end
113
+
114
+ def write(data)
115
+ @websocket.write(data)
116
+ end
117
+
118
+ def close
119
+ @websocket.close
120
+ end
121
+ end
60
122
  end
61
- end
123
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end