terminalwire 0.2.4 → 0.3.0.alpha1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/exe/terminalwire +1 -1
  3. data/exe/terminalwire-exec +1 -1
  4. metadata +9 -169
  5. data/.rspec +0 -3
  6. data/CHANGELOG.md +0 -5
  7. data/CODE_OF_CONDUCT.md +0 -132
  8. data/LICENSE.txt +0 -9
  9. data/README.md +0 -45
  10. data/Rakefile +0 -8
  11. data/examples/exec/localrails +0 -2
  12. data/lib/generators/terminalwire/install/USAGE +0 -9
  13. data/lib/generators/terminalwire/install/install_generator.rb +0 -38
  14. data/lib/generators/terminalwire/install/templates/application_terminal.rb.tt +0 -21
  15. data/lib/generators/terminalwire/install/templates/bin/terminalwire +0 -2
  16. data/lib/generators/terminalwire/install/templates/main_terminal.rb +0 -40
  17. data/lib/terminalwire/adapter.rb +0 -53
  18. data/lib/terminalwire/cache.rb +0 -114
  19. data/lib/terminalwire/client/entitlement/environment_variables.rb +0 -26
  20. data/lib/terminalwire/client/entitlement/paths.rb +0 -97
  21. data/lib/terminalwire/client/entitlement/policy.rb +0 -117
  22. data/lib/terminalwire/client/entitlement/schemes.rb +0 -26
  23. data/lib/terminalwire/client/entitlement.rb +0 -9
  24. data/lib/terminalwire/client/exec.rb +0 -44
  25. data/lib/terminalwire/client/handler.rb +0 -67
  26. data/lib/terminalwire/client/resource.rb +0 -204
  27. data/lib/terminalwire/client/server_license_verification.rb +0 -74
  28. data/lib/terminalwire/client.rb +0 -30
  29. data/lib/terminalwire/logging.rb +0 -8
  30. data/lib/terminalwire/rails.rb +0 -69
  31. data/lib/terminalwire/server/context.rb +0 -80
  32. data/lib/terminalwire/server/resource.rb +0 -120
  33. data/lib/terminalwire/server.rb +0 -63
  34. data/lib/terminalwire/thor.rb +0 -55
  35. data/lib/terminalwire/transport.rb +0 -58
  36. data/lib/terminalwire/version.rb +0 -5
  37. data/lib/terminalwire.rb +0 -56
  38. data/sig/terminalwire.rbs +0 -4
@@ -1,114 +0,0 @@
1
- require "pathname"
2
- require "msgpack"
3
- require "base64"
4
- require "time"
5
- require "fileutils"
6
-
7
- # Caches used on the client side for licesens, HTTP requests, etc.
8
- module Terminalwire::Cache
9
- module File
10
- # Hoist the File class to avoid conflicts with the standard library.
11
- File = ::File
12
-
13
- class Store
14
- include Enumerable
15
-
16
- def initialize(path:)
17
- @path = Pathname.new(path).expand_path
18
- FileUtils.mkdir_p(@path) unless @path.directory?
19
- end
20
-
21
- def entry(key)
22
- Entry.new(path: @path.join(Entry.key_path(key)))
23
- end
24
- alias :[] :entry
25
-
26
- def evict
27
- each(&:evict)
28
- end
29
-
30
- def destroy
31
- each(&:destroy)
32
- end
33
-
34
- def each
35
- @path.each_child do |path|
36
- yield Entry.new(path:)
37
- end
38
- end
39
- end
40
-
41
- class Entry
42
- VERSION = "1.0"
43
-
44
- def self.key_path(value)
45
- Base64.urlsafe_encode64(value)
46
- end
47
-
48
- attr_accessor :value, :expires
49
-
50
- def initialize(path:)
51
- @path = path
52
- deserialize if persisted?
53
- end
54
-
55
- def nil?
56
- @value.nil?
57
- end
58
-
59
- def present?
60
- not nil?
61
- end
62
-
63
- def persisted?
64
- File.exist? @path
65
- end
66
-
67
- def expired?(time: Time.now)
68
- @expires && @expires < time.utc
69
- end
70
-
71
- def fresh?(...)
72
- not expired?(...)
73
- end
74
-
75
- def hit?
76
- persisted? and fresh?
77
- end
78
-
79
- def miss?
80
- not hit?
81
- end
82
-
83
- def save
84
- File.write @path, serialize
85
- end
86
-
87
- def evict
88
- destroy if expired?
89
- end
90
-
91
- def deserialize
92
- case MessagePack.unpack(File.read(@path), symbolize_keys: true)
93
- in { value:, expires:, version: VERSION }
94
- @value = value
95
- @expires = Time.parse(expires).utc if expires
96
- end
97
- end
98
-
99
- def destroy
100
- File.delete(@path)
101
- end
102
-
103
- private
104
-
105
- def serialize
106
- MessagePack.pack(
107
- value: @value,
108
- expires: @expires&.utc&.iso8601,
109
- version: VERSION
110
- )
111
- end
112
- end
113
- end
114
- end
@@ -1,26 +0,0 @@
1
- module Terminalwire::Client::Entitlement
2
- # ENV vars that the server can access on the client.
3
- class EnvironmentVariables
4
- include Enumerable
5
-
6
- def initialize
7
- @permitted = Set.new
8
- end
9
-
10
- def each(&)
11
- @permitted.each(&)
12
- end
13
-
14
- def permit(variable)
15
- @permitted << variable.to_s
16
- end
17
-
18
- def permitted?(key)
19
- include? key.to_s
20
- end
21
-
22
- def serialize
23
- map { |name| { name: } }
24
- end
25
- end
26
- end
@@ -1,97 +0,0 @@
1
- module Terminalwire::Client::Entitlement
2
- # A list of paths and permissions that server has to write on the client workstation.
3
- class Paths
4
- class Permit
5
- attr_reader :path, :mode
6
- # Ensure the default file mode is read/write for owner only. This ensures
7
- # that if the server tries uploading an executable file, it won't be when it
8
- # lands on the client.
9
- #
10
- # Eventually we'll move this into entitlements so the client can set maximum
11
- # permissions for files and directories.
12
- MODE = 0o600 # rw-------
13
-
14
- # Constants for permission bit masks
15
- OWNER_PERMISSIONS = 0o700 # rwx------
16
- GROUP_PERMISSIONS = 0o070 # ---rwx---
17
- OTHERS_PERMISSIONS = 0o007 # ------rwx
18
-
19
- # We'll validate that modes are within this range.
20
- MODE_RANGE = 0o000..0o777
21
-
22
- def initialize(path:, mode: MODE)
23
- @path = Pathname.new(path)
24
- @mode = convert(mode)
25
- end
26
-
27
- def permitted_path?(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 @path.to_s, path.to_s, File::FNM_PATHNAME
31
- end
32
-
33
- def permitted_mode?(value)
34
- # Ensure the mode is at least as permissive as the permitted mode.
35
- mode = convert(value)
36
-
37
- # Extract permission bits for owner, group, and others
38
- owner_bits = mode & OWNER_PERMISSIONS
39
- group_bits = mode & GROUP_PERMISSIONS
40
- others_bits = mode & OTHERS_PERMISSIONS
41
-
42
- # Ensure that the mode doesn't grant more permissions than @mode in any class (owner, group, others)
43
- (owner_bits <= @mode & OWNER_PERMISSIONS) &&
44
- (group_bits <= @mode & GROUP_PERMISSIONS) &&
45
- (others_bits <= @mode & OTHERS_PERMISSIONS)
46
- end
47
-
48
- def permitted?(path:, mode: @mode)
49
- permitted_path?(path) && permitted_mode?(mode)
50
- end
51
-
52
- def serialize
53
- {
54
- location: @path.to_s,
55
- mode: @mode
56
- }
57
- end
58
-
59
- protected
60
- def convert(value)
61
- mode = Integer(value)
62
- 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)
63
- mode
64
- end
65
-
66
- def format_octet(value)
67
- format("0o%03o", value)
68
- end
69
- end
70
-
71
- include Enumerable
72
-
73
- def initialize
74
- @permitted = []
75
- end
76
-
77
- def each(&)
78
- @permitted.each(&)
79
- end
80
-
81
- def permit(path, **)
82
- @permitted.append Permit.new(path:, **)
83
- end
84
-
85
- def permitted?(path, mode: nil)
86
- if mode
87
- find { |it| it.permitted_path?(path) and it.permitted_mode?(mode) }
88
- else
89
- find { |it| it.permitted_path?(path) }
90
- end
91
- end
92
-
93
- def serialize
94
- map(&:serialize)
95
- end
96
- end
97
- end
@@ -1,117 +0,0 @@
1
- module Terminalwire::Client::Entitlement
2
- module Policy
3
- # A policy has the authority, paths, and schemes that the server is allowed to access.
4
- class Base
5
- attr_reader :paths, :authority, :schemes, :environment_variables
6
-
7
- def initialize(authority:)
8
- @authority = authority
9
- @paths = Paths.new
10
-
11
- # Permit the domain directory. This is necessary for basic operation of the client.
12
- @paths.permit storage_path
13
- @paths.permit storage_pattern
14
-
15
- @schemes = Schemes.new
16
- # Permit http & https by default.
17
- @schemes.permit "http"
18
- @schemes.permit "https"
19
-
20
- @environment_variables = EnvironmentVariables.new
21
- # Permit the HOME and TERMINALWIRE_HOME environment variables.
22
- @environment_variables.permit "TERMINALWIRE_HOME"
23
- end
24
-
25
- def root_path
26
- # TODO: This needs to be passed into the Policy so that it can be set by the client.
27
- Terminalwire::Client.root_path
28
- end
29
-
30
- def authority_path
31
- root_path.join("authorities/#{authority}")
32
- end
33
-
34
- def storage_path
35
- authority_path.join("storage")
36
- end
37
-
38
- def storage_pattern
39
- storage_path.join("**/*")
40
- end
41
-
42
- def serialize
43
- {
44
- authority: @authority,
45
- schemes: @schemes.serialize,
46
- paths: @paths.serialize,
47
- environment_variables: @environment_variables.serialize
48
- }
49
- end
50
- end
51
-
52
- class Root < Base
53
- AUTHORITY = "terminalwire.com".freeze
54
-
55
- # Terminalwire checks these to install the binary stubs path.
56
- SHELL_INITIALIZATION_FILE_PATHS = %w[
57
- ~/.bash_profile
58
- ~/.bashrc
59
- ~/.zprofile
60
- ~/.zshrc
61
- ~/.profile
62
- ~/.config/fish/config.fish
63
- ~/.bash_login
64
- ~/.cshrc
65
- ~/.tcshrc
66
- ].freeze
67
-
68
- # Ensure the binary stubs are executable. This increases the
69
- # file mode entitlement so that stubs created in ./bin are executable.
70
- BINARY_PATH_FILE_MODE = 0o755
71
-
72
- def initialize(*, **, &)
73
- # Make damn sure the authority is set to Terminalwire.
74
- super(*, authority: AUTHORITY, **, &)
75
-
76
- # Now setup special permitted paths.
77
- @paths.permit root_path
78
- @paths.permit root_pattern
79
- # Permit the dotfiles so terminalwire can install the binary stubs.
80
- SHELL_INITIALIZATION_FILE_PATHS.each do |path|
81
- @paths.permit path
82
- end
83
-
84
- # Permit terminalwire to grant execute permissions to the binary stubs.
85
- @paths.permit binary_pattern, mode: BINARY_PATH_FILE_MODE
86
-
87
- # Used to check if terminalwire is setup in the user's PATH environment variable.
88
- @environment_variables.permit "PATH"
89
- end
90
-
91
- # Grant access to the `~/.terminalwire/**/*` path so users can install
92
- # terminalwire apps via `terminalwire install svbtle`, etc.
93
- def root_pattern
94
- root_path.join("**/*").freeze
95
- end
96
-
97
- # Path where the terminalwire binary stubs are stored.
98
- def binary_path
99
- root_path.join("bin").freeze
100
- end
101
-
102
- # Pattern for the binary path.
103
- def binary_pattern
104
- binary_path.join("*").freeze
105
- end
106
- end
107
-
108
- def self.resolve(*, authority:, **, &)
109
- case authority
110
- when Policy::Root::AUTHORITY
111
- Root.new(*, **, &)
112
- else
113
- Base.new *, authority:, **, &
114
- end
115
- end
116
- end
117
- end
@@ -1,26 +0,0 @@
1
- module Terminalwire::Client::Entitlement
2
- # URLs the server can open on the client.
3
- class Schemes
4
- include Enumerable
5
-
6
- def initialize
7
- @permitted = Set.new
8
- end
9
-
10
- def each(&)
11
- @permitted.each(&)
12
- end
13
-
14
- def permit(scheme)
15
- @permitted << scheme.to_s
16
- end
17
-
18
- def permitted?(url)
19
- include? URI(url).scheme
20
- end
21
-
22
- def serialize
23
- map { |scheme| { scheme: } }
24
- end
25
- end
26
- end
@@ -1,9 +0,0 @@
1
- require "pathname"
2
-
3
- module Terminalwire::Client
4
- # Entitlements are the security boundary between the server and the client that lives on the client.
5
- # The server might request a file or directory from the client, and the client will check the entitlements
6
- # to see if the server is authorized to access the requested resource.
7
- module Entitlement
8
- end
9
- end
@@ -1,44 +0,0 @@
1
- require "pathname"
2
- require "yaml"
3
- require "uri"
4
-
5
- module Terminalwire::Client
6
- # Called by the `terminalwire-exec` shebang in scripts. This makes it easy for people
7
- # to create their own scripts that use Terminalwire that look like this:
8
- #
9
- # ```sh
10
- # #!/usr/bin/env terminalwire-exec
11
- # url: "https://terminalwire.com/terminal"
12
- # ```
13
- #
14
- # These files are saved, then `chmod + x` is run on them and they become executable.
15
- class Exec
16
- attr_reader :arguments, :path, :configuration, :url
17
-
18
- def initialize(path:, arguments:)
19
- @arguments = arguments
20
- @path = Pathname.new(path)
21
- @configuration = YAML.safe_load_file(@path)
22
- @url = URI(@configuration.fetch("url"))
23
- rescue Errno::ENOENT => e
24
- raise Terminalwire::Error, "File not found: #{@path}"
25
- rescue URI::InvalidURIError => e
26
- raise Terminalwire::Error, "Invalid URI: #{@url}"
27
- rescue KeyError => e
28
- raise Terminalwire::Error, "Missing key in configuration: #{e}"
29
- end
30
-
31
- def start
32
- Terminalwire::Client.websocket(url:, arguments:)
33
- end
34
-
35
- def self.start
36
- case ARGV
37
- in path, *arguments
38
- new(path:, arguments:).start
39
- end
40
- rescue NoMatchingPatternError => e
41
- raise Terminalwire::Error, "Launched with incorrect arguments: #{ARGV}"
42
- end
43
- end
44
- end
@@ -1,67 +0,0 @@
1
- module Terminalwire::Client
2
- # The handler is the main class that connects to the Terminalwire server and
3
- # dispatches messages to the appropriate resources.
4
- class Handler
5
- VERSION = "0.1.0".freeze
6
-
7
- include Terminalwire::Logging
8
-
9
- attr_reader :adapter, :resources, :endpoint
10
- attr_accessor :entitlement
11
-
12
- def initialize(adapter, arguments: ARGV, program_name: $0, endpoint:)
13
- @endpoint = endpoint
14
- @adapter = adapter
15
- @program_arguments = arguments
16
- @program_name = program_name
17
- @entitlement = Entitlement::Policy.resolve(authority: @endpoint.authority)
18
-
19
- yield self if block_given?
20
-
21
- @resources = Resource::Handler.new do |it|
22
- it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
23
- it << Resource::STDIN.new("stdin", @adapter, entitlement:)
24
- it << Resource::STDERR.new("stderr", @adapter, entitlement:)
25
- it << Resource::Browser.new("browser", @adapter, entitlement:)
26
- it << Resource::File.new("file", @adapter, entitlement:)
27
- it << Resource::Directory.new("directory", @adapter, entitlement:)
28
- it << Resource::EnvironmentVariable.new("environment_variable", @adapter, entitlement:)
29
- end
30
- end
31
-
32
- def verify_license
33
- # Connect to the Terminalwire license server to verify the URL endpoint
34
- # and displays a message to the user, if any are present.
35
- $stdout.print ServerLicenseVerification.new(url: @endpoint.to_url).message
36
- rescue
37
- $stderr.puts "Failed to verify server license."
38
- end
39
-
40
- def connect
41
- verify_license
42
-
43
- @adapter.write(
44
- event: "initialization",
45
- protocol: { version: VERSION },
46
- entitlement: @entitlement.serialize,
47
- program: {
48
- name: @program_name,
49
- arguments: @program_arguments
50
- }
51
- )
52
-
53
- loop do
54
- handle @adapter.read
55
- end
56
- end
57
-
58
- def handle(message)
59
- case message
60
- in { event: "resource", action: "command", name:, parameters: }
61
- @resources.dispatch(**message)
62
- in { event: "exit", status: }
63
- exit Integer(status)
64
- end
65
- end
66
- end
67
- end
@@ -1,204 +0,0 @@
1
- require "fileutils"
2
- require "io/console"
3
-
4
- module Terminalwire::Client::Resource
5
- # Dispatches messages from the Client::Handler to the appropriate resource.
6
- class Handler
7
- include Enumerable
8
-
9
- def initialize
10
- @resources = {}
11
- yield self if block_given?
12
- end
13
-
14
- def each(&block)
15
- @resources.values.each(&block)
16
- end
17
-
18
- def add(resource)
19
- # Detect if the resource is already registered and throw an error
20
- if @resources.key?(resource.name)
21
- raise "Resource #{resource.name} already registered"
22
- else
23
- @resources[resource.name] = resource
24
- end
25
- end
26
- alias :<< :add
27
-
28
- def dispatch(**message)
29
- case message
30
- in { event:, action:, name:, command:, parameters: }
31
- resource = @resources.fetch(name)
32
- resource.command(command, **parameters)
33
- end
34
- end
35
- end
36
-
37
- # Dispatcher, security, and response macros for resources.
38
- class Base < Terminalwire::Resource::Base
39
- def initialize(*, entitlement:, **)
40
- super(*, **)
41
- @entitlement = entitlement
42
- connect
43
- end
44
-
45
- def command(command, **parameters)
46
- begin
47
- if permit(command, **parameters)
48
- succeed self.public_send(command, **parameters)
49
- else
50
- fail "Client denied #{command}", command:, parameters:
51
- end
52
- rescue => e
53
- fail e.message, command:, parameters:
54
- raise
55
- end
56
- end
57
-
58
- protected
59
-
60
- def permit(...)
61
- false
62
- end
63
- end
64
-
65
- class EnvironmentVariable < Base
66
- # Accepts a list of environment variables to permit.
67
- def read(name:)
68
- ENV[name]
69
- end
70
-
71
- # def write(name:, value:)
72
- # ENV[name] = value
73
- # end
74
-
75
- protected
76
-
77
- def permit(command, name:, **)
78
- @entitlement.environment_variables.permitted? name
79
- end
80
- end
81
-
82
- class STDOUT < Base
83
- def connect
84
- @io = $stdout
85
- end
86
-
87
- def print(data:)
88
- @io.print(data)
89
- end
90
-
91
- def print_line(data:)
92
- @io.puts(data)
93
- end
94
-
95
- protected
96
-
97
- def permit(...)
98
- true
99
- end
100
- end
101
-
102
- class STDERR < STDOUT
103
- def connect
104
- @io = $stderr
105
- end
106
- end
107
-
108
- class STDIN < Base
109
- def connect
110
- @io = $stdin
111
- end
112
-
113
- def read_line
114
- @io.gets
115
- end
116
-
117
- def read_password
118
- @io.getpass
119
- end
120
-
121
- protected
122
-
123
- def permit(...)
124
- true
125
- end
126
- end
127
-
128
- class File < Base
129
- File = ::File
130
-
131
- def read(path:)
132
- File.read File.expand_path(path)
133
- end
134
-
135
- def write(path:, content:, mode: nil)
136
- File.open(File.expand_path(path), "w", mode) { |f| f.write(content) }
137
- end
138
-
139
- def append(path:, content:, mode: nil)
140
- File.open(File.expand_path(path), "a", mode) { |f| f.write(content) }
141
- end
142
-
143
- def delete(path:)
144
- File.delete File.expand_path(path)
145
- end
146
-
147
- def exist(path:)
148
- File.exist? File.expand_path(path)
149
- end
150
-
151
- def change_mode(path:, mode:)
152
- File.chmod mode, File.expand_path(path)
153
- end
154
-
155
- protected
156
-
157
- def permit(command, path:, mode: nil, **)
158
- @entitlement.paths.permitted? path, mode:
159
- end
160
- end
161
-
162
- class Directory < Base
163
- File = ::File
164
-
165
- def list(path:)
166
- Dir.glob path
167
- end
168
-
169
- def create(path:)
170
- FileUtils.mkdir_p File.expand_path(path)
171
- rescue Errno::EEXIST
172
- # Do nothing
173
- end
174
-
175
- def exist(path:)
176
- Dir.exist? path
177
- end
178
-
179
- def delete(path:)
180
- Dir.delete path
181
- end
182
-
183
- protected
184
-
185
- def permit(command, path:, **)
186
- @entitlement.paths.permitted? path
187
- end
188
- end
189
-
190
- class Browser < Base
191
- def launch(url:)
192
- Launchy.open(URI(url))
193
- # TODO: This is a hack to get the `respond` method to work.
194
- # Maybe explicitly call a `suceed` and `fail` method?
195
- nil
196
- end
197
-
198
- protected
199
-
200
- def permit(command, url:, **)
201
- @entitlement.schemes.permitted? url
202
- end
203
- end
204
- end