terminalwire 0.1.16 → 0.1.17

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