terminalwire 0.1.0 → 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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +14 -6
- data/examples/exec/localrails +2 -0
- data/exe/terminalwire-exec +9 -0
- data/lib/generators/terminalwire/install/USAGE +9 -0
- data/lib/generators/terminalwire/install/install_generator.rb +37 -0
- data/lib/generators/terminalwire/install/templates/application_terminal.rb.tt +54 -0
- data/lib/generators/terminalwire/install/templates/bin/terminalwire +2 -0
- data/lib/terminalwire/adapter.rb +32 -0
- data/lib/terminalwire/client/entitlement.rb +107 -0
- data/lib/terminalwire/client/exec.rb +35 -0
- data/lib/terminalwire/client/resource.rb +154 -0
- data/lib/terminalwire/client.rb +91 -0
- data/lib/terminalwire/logging.rb +8 -0
- data/lib/terminalwire/rails.rb +69 -0
- data/lib/terminalwire/server.rb +232 -0
- data/lib/terminalwire/thor.rb +51 -0
- data/lib/terminalwire/transport.rb +123 -0
- data/lib/terminalwire/version.rb +1 -1
- data/lib/terminalwire.rb +14 -655
- metadata +79 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8eacca5d9e410385fee49a9115cdc9d92ac1276b0666af88b8c343f11306a05f
|
4
|
+
data.tar.gz: c94050aef56399743f202c13c3813bb66a05cfffd2333ea9431bccded43234f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a25a33121a603fdcb11c5c664d17772f999d4a379379390ad578ed434fa75428f45deda6d2023e490ecf2bedfad61cf58a9fd4d7c880e17fff714978075159c
|
7
|
+
data.tar.gz: 76e5bf2edd88ad0ca092328514ebce7bc22ee64ff13aa4ebf83c77319849fe2564515cc89bf27364f3135ca2456143869066ca6eaef70daae15dabedb006b3e3
|
data/LICENSE.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Copyright (c) 2024 Brad Gessler. Email brad@terminalwire.com to discuss
|
1
|
+
Copyright (c) 2024 Brad Gessler. Email brad@terminalwire.com to discuss licensing.
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Terminalwire
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/terminalwire`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
Unlike most command-line tools for web services that require an API, Terminalwire streams terminal I/O between a web server and client over WebSockets. This means you can use your preferred command-line parser within your favorite web server framework to deliver a delightful CLI experience to your users.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -14,9 +12,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
12
|
|
15
13
|
$ gem install terminalwire
|
16
14
|
|
17
|
-
##
|
15
|
+
## Rails
|
16
|
+
|
17
|
+
Run the intallation command:
|
18
|
+
|
19
|
+
$ rails g terminalwire:install my-app
|
20
|
+
|
21
|
+
This generates the `./bin/my-app` file. Run it to verify that it connects to the server.
|
22
|
+
|
23
|
+
$ bin/my-app
|
24
|
+
Commands:
|
25
|
+
my-app help [COMMAND] # Describe available commands or one specific command
|
18
26
|
|
19
|
-
|
27
|
+
To edit the command-line, open `./app/cli/main_cli.rb` and make changes to the `MainCLI` class.
|
20
28
|
|
21
29
|
## Development
|
22
30
|
|
@@ -30,7 +38,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/termin
|
|
30
38
|
|
31
39
|
## License
|
32
40
|
|
33
|
-
The gem is available as a propietary license. Email brad@terminalwire.com to discuss
|
41
|
+
The gem is available as a propietary license. Email brad@terminalwire.com to discuss licensing.
|
34
42
|
|
35
43
|
## Code of Conduct
|
36
44
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "bundler"
|
2
|
+
|
3
|
+
class Terminalwire::InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
argument :binary_name, type: :string, required: true, banner: "binary_name"
|
7
|
+
|
8
|
+
def create_terminal_files
|
9
|
+
template "application_terminal.rb.tt", Rails.root.join("app/terminal/application_terminal.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_binary_files
|
13
|
+
copy_file "bin/terminalwire", binary_path
|
14
|
+
chmod binary_path, 0755, verbose: false
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_route
|
18
|
+
route <<~ROUTE
|
19
|
+
match "/terminal",
|
20
|
+
to: Terminalwire::Server::Thor.new(ApplicationTerminal),
|
21
|
+
via: [:get, :connect]
|
22
|
+
ROUTE
|
23
|
+
end
|
24
|
+
|
25
|
+
def print_post_install_message
|
26
|
+
say ""
|
27
|
+
say "Terminalwire has been successfully installed!", :green
|
28
|
+
say "Run `#{binary_path.relative_path_from(Rails.root)}` to verify everything is in working order. For support visit https://terminalwire.com."
|
29
|
+
say ""
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def binary_path
|
35
|
+
Rails.root.join("bin", binary_name)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Learn how to use Thor at http://whatisthor.com.
|
2
|
+
class ApplicationTerminal < Thor
|
3
|
+
include Terminalwire::Thor
|
4
|
+
|
5
|
+
def self.basename = "<%= binary_name %>"
|
6
|
+
|
7
|
+
desc "hello NAME", "say hello to NAME"
|
8
|
+
def hello(name)
|
9
|
+
puts "Hello #{name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "login", "Login to your account"
|
13
|
+
def login
|
14
|
+
print "Email: "
|
15
|
+
email = gets
|
16
|
+
|
17
|
+
print "Password: "
|
18
|
+
password = getpass
|
19
|
+
|
20
|
+
if self.current_user = User.authenticate(email, password)
|
21
|
+
puts "Successfully logged in as #{user.email}."
|
22
|
+
else
|
23
|
+
puts "Could not find a user with that email and password."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "whoami", "Displays current user information."
|
28
|
+
def whoami
|
29
|
+
if self.current_user
|
30
|
+
puts "Logged in as #{user.email}."
|
31
|
+
else
|
32
|
+
puts "Not logged in. Run `#{self.class.basename} login` to login."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "logout", "Logout of your account"
|
37
|
+
def logout
|
38
|
+
session.reset
|
39
|
+
puts "Successfully logged out."
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def current_user=(user)
|
45
|
+
# The Session object is a hash-like object that encrypts and signs a hash that's
|
46
|
+
# stored on the client's file sytem. Conceptually, it's similar to Rails signed
|
47
|
+
# and encrypted client-side cookies.
|
48
|
+
session["user_id"] = user.id
|
49
|
+
end
|
50
|
+
|
51
|
+
def current_user
|
52
|
+
@current_user ||= User.find(session.fetch("user_id"))
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module Terminalwire
|
4
|
+
class Adapter
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
attr_reader :transport
|
8
|
+
|
9
|
+
def initialize(transport)
|
10
|
+
@transport = transport
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(data)
|
14
|
+
logger.debug "Adapter: Sending #{data.inspect}"
|
15
|
+
packed_data = MessagePack.pack(data, symbolize_keys: true)
|
16
|
+
@transport.write(packed_data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def recv
|
20
|
+
logger.debug "Adapter: Reading"
|
21
|
+
packed_data = @transport.read
|
22
|
+
return nil if packed_data.nil?
|
23
|
+
data = MessagePack.unpack(packed_data, symbolize_keys: true)
|
24
|
+
logger.debug "Adapter: Received #{data.inspect}"
|
25
|
+
data
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
@transport.close
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Terminalwire::Client
|
2
|
+
class Entitlement
|
3
|
+
class Paths
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@permitted = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(&)
|
11
|
+
@permitted.each(&)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permit(path)
|
15
|
+
@permitted.append Pathname.new(path).expand_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def permitted?(path)
|
19
|
+
@permitted.find { |pattern| matches?(permitted: pattern, path:) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def serialize
|
23
|
+
@permitted.to_a.map(&:to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def matches?(permitted:, path:)
|
28
|
+
# This MUST be done via File.fnmatch because Pathname#fnmatch does not work. If you
|
29
|
+
# try changing this 🚨 YOU MAY CIRCUMVENT THE SECURITY MEASURES IN PLACE. 🚨
|
30
|
+
File.fnmatch permitted.to_s, File.expand_path(path), File::FNM_PATHNAME
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Schemes
|
35
|
+
include Enumerable
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@permitted = Set.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&)
|
42
|
+
@permitted.each(&)
|
43
|
+
end
|
44
|
+
|
45
|
+
def permit(scheme)
|
46
|
+
@permitted << scheme.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def permitted?(url)
|
50
|
+
include? URI(url).scheme
|
51
|
+
end
|
52
|
+
|
53
|
+
def serialize
|
54
|
+
@permitted.to_a.map(&:to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :paths, :authority, :schemes
|
59
|
+
|
60
|
+
def initialize(authority:)
|
61
|
+
@authority = authority
|
62
|
+
@paths = Paths.new
|
63
|
+
|
64
|
+
# Permit the domain directory. This is necessary for basic operation of the client.
|
65
|
+
@paths.permit storage_path
|
66
|
+
@paths.permit storage_pattern
|
67
|
+
|
68
|
+
@schemes = Schemes.new
|
69
|
+
# Permit http & https by default.
|
70
|
+
@schemes.permit "http"
|
71
|
+
@schemes.permit "https"
|
72
|
+
end
|
73
|
+
|
74
|
+
def domain_path
|
75
|
+
Pathname.new("~/.terminalwire/authorities/#{@authority}").expand_path
|
76
|
+
end
|
77
|
+
|
78
|
+
def storage_path
|
79
|
+
domain_path.join("storage")
|
80
|
+
end
|
81
|
+
|
82
|
+
def storage_pattern
|
83
|
+
storage_path.join("**/*")
|
84
|
+
end
|
85
|
+
|
86
|
+
def serialize
|
87
|
+
{
|
88
|
+
authority: @authority,
|
89
|
+
schemes: @schemes.serialize,
|
90
|
+
paths: @paths.serialize,
|
91
|
+
storage_path: storage_path.to_s,
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.from_url(url)
|
96
|
+
# I had to lift this from URI::HTTP because `ws://` doesn't
|
97
|
+
# have an authority method.
|
98
|
+
authority = if url.port == url.default_port
|
99
|
+
url.host
|
100
|
+
else
|
101
|
+
"#{url.host}:#{url.port}"
|
102
|
+
end
|
103
|
+
|
104
|
+
new authority:
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "yaml"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Terminalwire::Client
|
6
|
+
class Exec
|
7
|
+
attr_reader :arguments, :path, :configuration, :url
|
8
|
+
|
9
|
+
def initialize(path:, arguments:)
|
10
|
+
@arguments = arguments
|
11
|
+
@path = Pathname.new(path)
|
12
|
+
@configuration = YAML.load_file(@path)
|
13
|
+
@url = URI(@configuration.fetch("url"))
|
14
|
+
rescue Errno::ENOENT => e
|
15
|
+
raise Terminalwire::Error, "File not found: #{@path}"
|
16
|
+
rescue URI::InvalidURIError => e
|
17
|
+
raise Terminalwire::Error, "Invalid URI: #{@url}"
|
18
|
+
rescue KeyError => e
|
19
|
+
raise Terminalwire::Error, "Missing key in configuration: #{e}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
Terminalwire::Client.websocket(url:, arguments:)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.start
|
27
|
+
case ARGV
|
28
|
+
in path, *arguments
|
29
|
+
new(path:, arguments:).start
|
30
|
+
end
|
31
|
+
rescue NoMatchingPatternError => e
|
32
|
+
raise Terminalwire::Error, "Launched with incorrect arguments: #{ARGV}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Terminalwire::Client::Resource
|
2
|
+
# Dispatches messages from the Client::Handler to the appropriate resource.
|
3
|
+
class Handler
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@resources = {}
|
8
|
+
yield self if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
@resources.values.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(resource)
|
16
|
+
# Detect if the resource is already registered and throw an error
|
17
|
+
if @resources.key?(resource.name)
|
18
|
+
raise "Resource #{resource.name} already registered"
|
19
|
+
else
|
20
|
+
@resources[resource.name] = resource
|
21
|
+
end
|
22
|
+
end
|
23
|
+
alias :<< :add
|
24
|
+
|
25
|
+
def dispatch(**message)
|
26
|
+
case message
|
27
|
+
in { event:, action:, name:, command:, parameters: }
|
28
|
+
resource = @resources.fetch(name)
|
29
|
+
resource.command(command, **parameters)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Dispatcher, security, and response macros for resources.
|
35
|
+
class Base < Terminalwire::Resource::Base
|
36
|
+
def initialize(*, entitlement:, **)
|
37
|
+
super(*, **)
|
38
|
+
@entitlement = entitlement
|
39
|
+
connect
|
40
|
+
end
|
41
|
+
|
42
|
+
def command(command, **parameters)
|
43
|
+
begin
|
44
|
+
if permit(command, **parameters)
|
45
|
+
succeed self.public_send(command, **parameters)
|
46
|
+
else
|
47
|
+
fail "Client denied #{command}", command:, parameters:
|
48
|
+
end
|
49
|
+
rescue => e
|
50
|
+
fail e.message, command:, parameters:
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def permit(...)
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class STDOUT < Base
|
61
|
+
def connect
|
62
|
+
@io = $stdout
|
63
|
+
end
|
64
|
+
|
65
|
+
def print(data:)
|
66
|
+
@io.print(data)
|
67
|
+
end
|
68
|
+
|
69
|
+
def print_line(data:)
|
70
|
+
@io.puts(data)
|
71
|
+
end
|
72
|
+
|
73
|
+
def permit(...)
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class STDERR < STDOUT
|
79
|
+
def connect
|
80
|
+
@io = $stderr
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class STDIN < Base
|
85
|
+
def connect
|
86
|
+
@io = $stdin
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_line
|
90
|
+
@io.gets
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_password
|
94
|
+
@io.getpass
|
95
|
+
end
|
96
|
+
|
97
|
+
def permit(...)
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class File < Base
|
103
|
+
File = ::File
|
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
|
+
def read(path:)
|
114
|
+
File.read File.expand_path(path)
|
115
|
+
end
|
116
|
+
|
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) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def mkdir(path:)
|
126
|
+
FileUtils.mkdir_p(File.expand_path(path))
|
127
|
+
end
|
128
|
+
|
129
|
+
def delete(path:)
|
130
|
+
File.delete(File.expand_path(path))
|
131
|
+
end
|
132
|
+
|
133
|
+
def exist(path:)
|
134
|
+
File.exist? File.expand_path(path)
|
135
|
+
end
|
136
|
+
|
137
|
+
def permit(command, path:, **)
|
138
|
+
@entitlement.paths.permitted? path
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class Browser < Base
|
143
|
+
def permit(command, url:, **)
|
144
|
+
@entitlement.schemes.permitted? url
|
145
|
+
end
|
146
|
+
|
147
|
+
def launch(url:)
|
148
|
+
Launchy.open(URI(url))
|
149
|
+
# TODO: This is a hack to get the `respond` method to work.
|
150
|
+
# Maybe explicitly call a `suceed` and `fail` method?
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'launchy'
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
module Terminalwire
|
6
|
+
module Client
|
7
|
+
class Handler
|
8
|
+
VERSION = "0.1.0".freeze
|
9
|
+
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
attr_reader :adapter, :entitlement, :resources
|
13
|
+
|
14
|
+
def initialize(adapter, arguments: ARGV, program_name: $0, entitlement:)
|
15
|
+
@entitlement = entitlement
|
16
|
+
@adapter = adapter
|
17
|
+
@program_arguments = arguments
|
18
|
+
@program_name = program_name
|
19
|
+
|
20
|
+
@resources = Resource::Handler.new do |it|
|
21
|
+
it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
|
22
|
+
it << Resource::STDIN.new("stdin", @adapter, entitlement:)
|
23
|
+
it << Resource::STDERR.new("stderr", @adapter, entitlement:)
|
24
|
+
it << Resource::Browser.new("browser", @adapter, entitlement:)
|
25
|
+
it << Resource::File.new("file", @adapter, entitlement:)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def connect
|
30
|
+
@adapter.write(event: "initialization",
|
31
|
+
protocol: { version: VERSION },
|
32
|
+
entitlement: @entitlement.serialize,
|
33
|
+
program: {
|
34
|
+
name: @program_name,
|
35
|
+
arguments: @program_arguments
|
36
|
+
})
|
37
|
+
|
38
|
+
loop do
|
39
|
+
handle @adapter.recv
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle(message)
|
44
|
+
case message
|
45
|
+
in { event: "resource", action: "command", name:, parameters: }
|
46
|
+
@resources.dispatch(**message)
|
47
|
+
in { event: "exit", status: }
|
48
|
+
exit Integer(status)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.tcp(...)
|
54
|
+
socket = TCPSocket.new(...)
|
55
|
+
transport = Terminalwire::Transport::Socket.new(socket)
|
56
|
+
adapter = Terminalwire::Adapter.new(transport)
|
57
|
+
Terminalwire::Client::Handler.new(adapter)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.socket(...)
|
61
|
+
socket = UNIXSocket.new(...)
|
62
|
+
transport = Terminalwire::Transport::Socket.new(socket)
|
63
|
+
adapter = Terminalwire::Adapter.new(transport)
|
64
|
+
Terminalwire::Client::Handler.new(adapter)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Extracted from HTTP. This is so we can
|
68
|
+
def self.authority(url)
|
69
|
+
if url.port == url.default_port
|
70
|
+
url.host
|
71
|
+
else
|
72
|
+
"#{url.host}:#{url.port}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.websocket(url:, arguments: ARGV)
|
77
|
+
url = URI(url)
|
78
|
+
|
79
|
+
Async do |task|
|
80
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
81
|
+
|
82
|
+
Async::WebSocket::Client.connect(endpoint) do |adapter|
|
83
|
+
transport = Terminalwire::Transport::WebSocket.new(adapter)
|
84
|
+
adapter = Terminalwire::Adapter.new(transport)
|
85
|
+
entitlement = Entitlement.from_url(url)
|
86
|
+
Terminalwire::Client::Handler.new(adapter, arguments:, entitlement:).connect
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -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
|