terminalwire 0.1.1 → 0.1.2

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