terminalwire 0.1.16 → 0.1.17

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: 20d25d091ee29d5953cfc70935227461ed75ec3d52c5b4cbff96db9e4b30ea7a
4
- data.tar.gz: 80325aea9d946a9050ab4d9a97003186aba8a826e04ccefa23783a6fcfeea4dc
3
+ metadata.gz: 3ab3f5780e6c3f7f544936130b288676ad60b29ef05944842771bcb3ca51192a
4
+ data.tar.gz: 4bd9a47eb0541ca81dd53cc19f6bf5ca02e388f22048b1fceaf7ac0f910e557d
5
5
  SHA512:
6
- metadata.gz: cc79939e4328e469df0a13f5ec34ed0e99c5f7206712a8480916b55005b95fcf544de98884d33f306fcb16a02fa4868aa09f302087e3015c02c80d42c8429a75
7
- data.tar.gz: 0e00b629b2ae76adddef05725f2f3fb8b1550e55b513fdd77317a978f2a416a0876af450ac47da4c0ee011fb8239623e37f7887bc0bd627ac2cb258b0779077a
6
+ metadata.gz: e37f803cf8777f66aa6f8f19cd72fb0ca778e06b1f39a7762b8e3ed74b494dee210f025af65063b6d60321d9d6e9c20fa00b81dcd61b1ca7efd9193a6bf5e310
7
+ data.tar.gz: 25b6c73db710026062340f779bee11738952bd86efb80bef53f4d92cdef9599e917e85a6d979ee559fa422c3f73b4f8ffd5d7e9d87d81f26a531bab8603c1e68
@@ -0,0 +1,113 @@
1
+ require "pathname"
2
+ require "msgpack"
3
+ require "base64"
4
+ require "time"
5
+ require "fileutils"
6
+
7
+ module Terminalwire::Cache
8
+ module File
9
+ # Hoist the File class to avoid conflicts with the standard library.
10
+ File = ::File
11
+
12
+ class Store
13
+ include Enumerable
14
+
15
+ def initialize(path:)
16
+ @path = Pathname.new(path).expand_path
17
+ FileUtils.mkdir_p(@path) unless @path.directory?
18
+ end
19
+
20
+ def entry(key)
21
+ Entry.new(path: @path.join(Entry.key_path(key)))
22
+ end
23
+ alias :[] :entry
24
+
25
+ def evict
26
+ each(&:evict)
27
+ end
28
+
29
+ def destroy
30
+ each(&:destroy)
31
+ end
32
+
33
+ def each
34
+ @path.each_child do |path|
35
+ yield Entry.new(path:)
36
+ end
37
+ end
38
+ end
39
+
40
+ class Entry
41
+ VERSION = "1.0"
42
+
43
+ def self.key_path(value)
44
+ Base64.urlsafe_encode64(value)
45
+ end
46
+
47
+ attr_accessor :value, :expires
48
+
49
+ def initialize(path:)
50
+ @path = path
51
+ deserialize if persisted?
52
+ end
53
+
54
+ def nil?
55
+ @value.nil?
56
+ end
57
+
58
+ def present?
59
+ not nil?
60
+ end
61
+
62
+ def persisted?
63
+ File.exist? @path
64
+ end
65
+
66
+ def expired?(time: Time.now)
67
+ @expires && @expires < time.utc
68
+ end
69
+
70
+ def fresh?(...)
71
+ not expired?(...)
72
+ end
73
+
74
+ def hit?
75
+ persisted? and fresh?
76
+ end
77
+
78
+ def miss?
79
+ not hit?
80
+ end
81
+
82
+ def save
83
+ File.write @path, serialize
84
+ end
85
+
86
+ def evict
87
+ destroy if expired?
88
+ end
89
+
90
+ def deserialize
91
+ case MessagePack.unpack(File.read(@path), symbolize_keys: true)
92
+ in { value:, expires:, version: VERSION }
93
+ @value = value
94
+ @expires = Time.parse(expires).utc if expires
95
+ end
96
+ end
97
+
98
+ def destroy
99
+ File.delete(@path)
100
+ end
101
+
102
+ private
103
+
104
+ def serialize
105
+ MessagePack.pack(
106
+ value: @value,
107
+ expires: @expires&.utc&.iso8601,
108
+ version: VERSION
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
@@ -167,7 +167,7 @@ module Terminalwire::Client
167
167
  end
168
168
 
169
169
  class RootPolicy < Policy
170
- HOST = "terminalwire.com".freeze
170
+ AUTHORITY = "terminalwire.com".freeze
171
171
 
172
172
  # Ensure the binary stubs are executable. This increases the
173
173
  # file mode entitlement so that stubs created in ./bin are executable.
@@ -175,7 +175,7 @@ module Terminalwire::Client
175
175
 
176
176
  def initialize(*, **, &)
177
177
  # Make damn sure the authority is set to Terminalwire.
178
- super(*, authority: HOST, **, &)
178
+ super(*, authority: AUTHORITY, **, &)
179
179
 
180
180
  # Now setup special permitted paths.
181
181
  @paths.permit root_path
@@ -202,24 +202,12 @@ module Terminalwire::Client
202
202
  end
203
203
  end
204
204
 
205
- def self.from_url(url)
206
- url = URI(url)
207
-
208
- case url.host
209
- when RootPolicy::HOST
210
- RootPolicy.new
211
- else
212
- Policy.new authority: url_authority(url)
213
- end
214
- end
215
-
216
- def self.url_authority(url)
217
- # I had to lift this from URI::HTTP because `ws://` doesn't
218
- # have an authority method.
219
- if url.port == url.default_port
220
- url.host
205
+ def self.resolve(*, authority:, **, &)
206
+ case authority
207
+ when RootPolicy::AUTHORITY
208
+ RootPolicy.new(*, **, &)
221
209
  else
222
- "#{url.host}:#{url.port}"
210
+ Policy.new *, authority:, **, &
223
211
  end
224
212
  end
225
213
  end
@@ -0,0 +1,74 @@
1
+ require "async/http/internet"
2
+ require "base64"
3
+ require "uri"
4
+ require "fileutils"
5
+
6
+ module Terminalwire
7
+ module Client
8
+ class ServerLicenseVerification
9
+ include Logging
10
+
11
+ def initialize(url:)
12
+ @url = URI(url)
13
+ @internet = Async::HTTP::Internet.new
14
+ @cache_store = Terminalwire::Cache::File::Store.new(path: "~/.terminalwire/cache/licenses/verifications")
15
+ end
16
+
17
+ def key
18
+ Base64.urlsafe_encode64 @url
19
+ end
20
+
21
+ def cache = @cache_store.entry key
22
+
23
+ def payload
24
+ if cache.miss?
25
+ logger.debug "Stale verification. Requesting new verification."
26
+ request do |response|
27
+ # Set the expiry on the file cache for the header.
28
+ if max_age = response.headers["cache-control"].max_age
29
+ logger.debug "Caching for #{max_age}"
30
+ cache.expires = Time.now + max_age
31
+ end
32
+
33
+ # Process based on the response code.
34
+ case response.status
35
+ in 200
36
+ logger.debug "License for #{@url} found."
37
+ data = self.class.unpack response.read
38
+ cache.value = data
39
+ return data
40
+ in 404
41
+ logger.debug "License for #{@url} not found."
42
+ return self.class.unpack response.read
43
+ end
44
+ end
45
+ else
46
+ return cache.value
47
+ end
48
+ end
49
+
50
+ def message
51
+ payload.dig(:shell, :output)
52
+ end
53
+
54
+ protected
55
+
56
+ def verification_url
57
+ Terminalwire.url
58
+ .path("/licenses/verifications", key)
59
+ end
60
+
61
+ def request(&)
62
+ logger.debug "Requesting license verification from #{verification_url}."
63
+ response = @internet.get verification_url, {
64
+ "Accept" => "application/x-msgpack",
65
+ "User-Agent" => "Terminalwire/#{Terminalwire::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})",
66
+ }, &
67
+ end
68
+
69
+ def self.unpack(pack)
70
+ MessagePack.unpack(pack, symbolize_keys: true)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -9,13 +9,17 @@ module Terminalwire
9
9
 
10
10
  include Logging
11
11
 
12
- attr_reader :adapter, :entitlement, :resources
12
+ attr_reader :adapter, :resources, :endpoint
13
+ attr_accessor :entitlement
13
14
 
14
- def initialize(adapter, arguments: ARGV, program_name: $0, entitlement:)
15
- @entitlement = entitlement
15
+ def initialize(adapter, arguments: ARGV, program_name: $0, endpoint:)
16
+ @endpoint = endpoint
16
17
  @adapter = adapter
17
18
  @program_arguments = arguments
18
19
  @program_name = program_name
20
+ @entitlement = Entitlement.resolve(authority: @endpoint.authority)
21
+
22
+ yield self if block_given?
19
23
 
20
24
  @resources = Resource::Handler.new do |it|
21
25
  it << Resource::STDOUT.new("stdout", @adapter, entitlement:)
@@ -27,7 +31,17 @@ module Terminalwire
27
31
  end
28
32
  end
29
33
 
34
+ def verify_license
35
+ # Connect to the Terminalwire license server to verify the URL endpoint
36
+ # and displays a message to the user, if any are present.
37
+ $stdout.print Terminalwire::Client::ServerLicenseVerification.new(url: @endpoint.to_url).message
38
+ rescue
39
+ $stderr.puts "Failed to verify server license."
40
+ end
41
+
30
42
  def connect
43
+ verify_license
44
+
31
45
  @adapter.write(
32
46
  event: "initialization",
33
47
  protocol: { version: VERSION },
@@ -53,16 +67,7 @@ module Terminalwire
53
67
  end
54
68
  end
55
69
 
56
- # Extracted from HTTP. This is so we can
57
- def self.authority(url)
58
- if url.port == url.default_port
59
- url.host
60
- else
61
- "#{url.host}:#{url.port}"
62
- end
63
- end
64
-
65
- def self.websocket(url:, arguments: ARGV, entitlement: nil)
70
+ def self.websocket(url:, arguments: ARGV, &configuration)
66
71
  url = URI(url)
67
72
 
68
73
  Async do |task|
@@ -74,8 +79,7 @@ module Terminalwire
74
79
  Async::WebSocket::Client.connect(endpoint) do |adapter|
75
80
  transport = Terminalwire::Transport::WebSocket.new(adapter)
76
81
  adapter = Terminalwire::Adapter::Socket.new(transport)
77
- entitlement ||= Entitlement.from_url(url)
78
- Terminalwire::Client::Handler.new(adapter, arguments:, entitlement:).connect
82
+ Terminalwire::Client::Handler.new(adapter, arguments:, endpoint:, &configuration).connect
79
83
  end
80
84
  end
81
85
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.16"
4
+ VERSION = "0.1.17"
5
5
  end
data/lib/terminalwire.rb CHANGED
@@ -10,15 +10,22 @@ require 'async'
10
10
  require 'async/http/endpoint'
11
11
  require 'async/websocket/client'
12
12
  require 'async/websocket/adapters/rack'
13
+ require 'uri-builder'
13
14
 
14
15
  module Terminalwire
15
16
  class Error < StandardError; end
16
17
 
18
+ # Zeitwerk loader for the Terminalwire gem.
17
19
  Loader = Zeitwerk::Loader.for_gem.tap do |loader|
18
20
  loader.ignore("#{__dir__}/generators")
19
21
  loader.setup
20
22
  end
21
23
 
24
+ # Used by Terminalwire client to connect to Terminalire.com for license
25
+ # validations, etc.
26
+ TERMINALWIRE_URL = "https://terminalwire.com".freeze
27
+ def self.url = URI.build(TERMINALWIRE_URL)
28
+
22
29
  module Resource
23
30
  class Base
24
31
  attr_reader :name, :adapter
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.16
4
+ version: 0.1.17
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-10-28 00:00:00.000000000 Z
11
+ date: 2024-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-websocket
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '2.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: uri-builder
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.1.9
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.1.9
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -161,12 +175,12 @@ files:
161
175
  - lib/generators/terminalwire/install/templates/main_terminal.rb
162
176
  - lib/terminalwire.rb
163
177
  - lib/terminalwire/adapter.rb
164
- - lib/terminalwire/authority.rb
178
+ - lib/terminalwire/cache.rb
165
179
  - lib/terminalwire/client.rb
166
180
  - lib/terminalwire/client/entitlement.rb
167
181
  - lib/terminalwire/client/exec.rb
168
182
  - lib/terminalwire/client/resource.rb
169
- - lib/terminalwire/licensing.rb
183
+ - lib/terminalwire/client/server_license_verification.rb
170
184
  - lib/terminalwire/logging.rb
171
185
  - lib/terminalwire/rails.rb
172
186
  - lib/terminalwire/server.rb
@@ -199,7 +213,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
213
  - !ruby/object:Gem::Version
200
214
  version: '0'
201
215
  requirements: []
202
- rubygems_version: 3.5.16
216
+ rubygems_version: 3.5.3
203
217
  signing_key:
204
218
  specification_version: 4
205
219
  summary: Ship a CLI for your web app. No API required.
@@ -1,44 +0,0 @@
1
- require "uri"
2
- require "base64"
3
-
4
- # Resolves domains into authorities, which are is used for access
5
- # identity control in Terminalwire.
6
- class Terminalwire::Authority
7
- # Used to seperate path keys in the URL.
8
- PATH_SEPERATOR = "/".freeze
9
-
10
- # Used to demark a URL string as authorative.
11
- SCHEME = "terminalwire://".freeze
12
-
13
- def initialize(url:)
14
- @url = URI(url)
15
- end
16
-
17
- # Extracted from HTTP. This is so we can
18
- def domain
19
- if @url.port == @url.default_port
20
- @url.host
21
- else
22
- "#{url.host}:#{url.port}"
23
- end
24
- end
25
-
26
- # Make sure there's always a / at the end of the path.
27
- def path
28
- path_keys.join(PATH_SEPERATOR).prepend(PATH_SEPERATOR)
29
- end
30
-
31
- def to_s
32
- [SCHEME, domain, path].join
33
- end
34
-
35
- def key
36
- Base64.urlsafe_encode64(to_s)
37
- end
38
-
39
- protected
40
-
41
- def path_keys
42
- @url.path.scan(/[^\/]+/)
43
- end
44
- end
@@ -1,187 +0,0 @@
1
- require "msgpack"
2
- require "openssl"
3
- require "base64"
4
- require "uri"
5
-
6
- module Terminalwire::Licensing
7
- PRIVATE_KEY_LENGTH = 2048
8
-
9
- def self.generate_private_key
10
- OpenSSL::PKey::RSA.new(PRIVATE_KEY_LENGTH)
11
- end
12
-
13
- def self.generate_private_pem
14
- generate_private_key.to_pem
15
- end
16
-
17
- def self.time
18
- Time.now.utc
19
- end
20
-
21
- # Handles encoding data into a license key with prefixes that can be packed and unpacked.
22
- module Key
23
- # Mix into classes that need to generate or read keys
24
- module Serialization
25
- # This is called when the module is included in a class
26
- def self.included(base)
27
- # Extend the class with the class methods when the module is included
28
- base.extend(ClassMethods)
29
- end
30
-
31
- def serialize(...)
32
- self.class.serialize(...)
33
- end
34
-
35
- # Define the class methods that will be available on the including class
36
- module ClassMethods
37
- def serializer
38
- Key::Serializer.new(prefix: self::PREFIX)
39
- end
40
-
41
- def serialize(...)
42
- serializer.serialize(...)
43
- end
44
-
45
- def deserialize(...)
46
- serializer.deserialize(...)
47
- end
48
- end
49
- end
50
-
51
- class Serializer
52
- attr_reader :prefix
53
-
54
- def initialize(prefix:)
55
- @prefix = prefix
56
- end
57
-
58
- def serialize(data)
59
- prepend_prefix Base64.urlsafe_encode64 MessagePack.pack data
60
- end
61
-
62
- def deserialize(data)
63
- MessagePack.unpack Base64.urlsafe_decode64 unshift_prefix data
64
- end
65
-
66
- protected
67
-
68
- def prepend_prefix(key)
69
- [prefix, key].join
70
- end
71
-
72
- def unshift_prefix(key)
73
- head, prefix, tail = key.partition(@prefix)
74
- # Check if partition successfully split the string with the correct prefix
75
- raise RuntimeError, "Expected prefix #{@prefix.inspect} on #{key.inspect}" if prefix.empty?
76
- tail
77
- end
78
- end
79
- end
80
-
81
- # This code all runs on Terminalwire servers.
82
- module Issuer
83
- # Generates license keys that developers use to activate their software.
84
- class ServerKeyGenerator
85
- include Key::Serialization
86
-
87
- PREFIX = "server_key_".freeze
88
-
89
- VERSION = "1.0".freeze
90
-
91
- def initialize(public_key:, license_url:, generated_at: Terminalwire::Licensing.time)
92
- @public_key = public_key
93
- @license_url = URI(license_url)
94
- @generated_at = generated_at
95
- end
96
-
97
- def to_h
98
- {
99
- version: VERSION,
100
- generated_at: @generated_at.iso8601,
101
- public_key: @public_key.to_pem,
102
- license_url: @license_url.to_s
103
- }
104
- end
105
-
106
- def server_key
107
- serialize to_h
108
- end
109
- alias :to_s :server_key
110
- end
111
-
112
- class ClientKeyVerifier
113
- # Time variance the server will tolerate from the client.
114
- DRIFT_SECONDS = 600 # 600 seconds, or 10 minutes.
115
-
116
- # This means the server will tolerate a 10 minute drift in the generated_at time.
117
- def self.drift
118
- now = Terminalwire::Licensing.time
119
- (now - DRIFT_SECONDS)...(now + DRIFT_SECONDS)
120
- end
121
-
122
- def initialize(client_key:, private_key:, drift: self.class.drift)
123
- @data = Server::ClientKeyGenerator.deserialize client_key
124
- @private_key = private_key
125
- @drift = drift
126
- end
127
-
128
- def server_attestation
129
- @server_attestation ||= decrypt @data.fetch("server_attestation")
130
- end
131
-
132
- def decrypt(data)
133
- MessagePack.unpack @private_key.private_decrypt Base64.urlsafe_decode64 data
134
- end
135
-
136
- def generated_at
137
- @generated_at ||= Time.parse(server_attestation.fetch("generated_at"))
138
- end
139
-
140
- def valid?
141
- @drift.include? generated_at
142
- end
143
- end
144
- end
145
-
146
- # Those code runs on customer servers
147
- module Server
148
- class ClientKeyGenerator
149
- VERSION = "1.0".freeze
150
-
151
- include Key::Serialization
152
- PREFIX = "client_key_".freeze
153
-
154
- def initialize(server_key:, generated_at: Terminalwire::Licensing.time)
155
- @data = Issuer::ServerKeyGenerator.deserialize server_key
156
- @license_url = URI(@data.fetch("license_url"))
157
- @generated_at = generated_at
158
- end
159
-
160
- def to_h
161
- {
162
- version: VERSION,
163
- license_url: @license_url.to_s,
164
- server_attestation: attest(
165
- version: VERSION,
166
- generated_at: @generated_at.iso8601,
167
- )
168
- }
169
- end
170
-
171
- def client_key
172
- serialize to_h
173
- end
174
- alias :to_s :client_key
175
-
176
- protected
177
-
178
- def attest(data)
179
- Base64.urlsafe_encode64 public_key.public_encrypt MessagePack.pack data
180
- end
181
-
182
- def public_key
183
- @public_key ||= OpenSSL::PKey::RSA.new(@data.fetch("public_key"))
184
- end
185
- end
186
- end
187
- end