terminalwire 0.1.12 → 0.1.14

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: 0ffa1e8b55e7c09d41b9a57fc645e950e65f6658767022cf67cac994823fb760
4
- data.tar.gz: 32fb995746b753eeed71543b954aa2376c2af4b1e61721b14eac8704c9f555f6
3
+ metadata.gz: 75d01d183f7babe341773fad359c35f4ea478430792a2f292f3e1bce52b6b0ec
4
+ data.tar.gz: 4634c87d3d66e1553d7ae6ddf6198b9a5da72c1a9a2eb597e9bb1c9556effe8b
5
5
  SHA512:
6
- metadata.gz: cb3ca0210a84eb64c924522d4eeb04f75a59bf8d785581a232216c8c417ecc837a66001947449e4da91127ebe942c12bce6ea07f8de1be2fe1fc69e5dcfdc332
7
- data.tar.gz: e9e0ea45f77040fc503acf2c09f22671d2a09f26315cad69ce1de8e62d4d0578b96663db13414446b7e99ce4abd3dd792ccb6b9191462ae6f67ff8789750856a
6
+ metadata.gz: c0588f7064884a7e76b111aee494dd6ee0d694766afac82cf7aee8c420fd5b9b108991ecee6e57f5800fb54752e580ed240129aa2085b2f6134ac85478845ebe
7
+ data.tar.gz: afb0912cdc926020e32a6976562dfe59ff66365a90c17b3b3a35cf45ccd408a41182ba69403b07f10fdcd7538e7bc120379c58fc10f8fc64cca5576980e8b470
data/LICENSE.txt CHANGED
@@ -2,7 +2,7 @@ Copyright (c) 2024 Brad Gessler.
2
2
 
3
3
  License is propietary. Here's the deal:
4
4
 
5
- * You need to pay for Terminalwire if your business has more than $1m in assets or makes more than $100k/year in revenue.
5
+ * You need to pay for Terminalwire if your organization has more than $1m in assets or makes more than $100k/year in revenue.
6
6
  * Terminalwire is free for personal use, hobbyists, or businesses that are just starting out, but server licenses still need to be registered at https://terminalwire.com/developers/licenses.
7
7
  * You can only use the Terminalwire client with licensed Terminalwire servers.
8
8
 
@@ -28,13 +28,15 @@ module Terminalwire
28
28
  end
29
29
 
30
30
  def connect
31
- @adapter.write(event: "initialization",
32
- protocol: { version: VERSION },
33
- entitlement: @entitlement.serialize,
34
- program: {
35
- name: @program_name,
36
- arguments: @program_arguments
37
- })
31
+ @adapter.write(
32
+ event: "initialization",
33
+ protocol: { version: VERSION },
34
+ entitlement: @entitlement.serialize,
35
+ program: {
36
+ name: @program_name,
37
+ arguments: @program_arguments
38
+ }
39
+ )
38
40
 
39
41
  loop do
40
42
  handle @adapter.read
@@ -51,20 +53,6 @@ module Terminalwire
51
53
  end
52
54
  end
53
55
 
54
- def self.tcp(...)
55
- socket = TCPSocket.new(...)
56
- transport = Terminalwire::Transport::Socket.new(socket)
57
- adapter = Terminalwire::Adapter::Socket.new(transport)
58
- Terminalwire::Client::Handler.new(adapter)
59
- end
60
-
61
- def self.socket(...)
62
- socket = UNIXSocket.new(...)
63
- transport = Terminalwire::Transport::Socket.new(socket)
64
- adapter = Terminalwire::Adapter::Socket.new(transport)
65
- Terminalwire::Client::Handler.new(adapter)
66
- end
67
-
68
56
  # Extracted from HTTP. This is so we can
69
57
  def self.authority(url)
70
58
  if url.port == url.default_port
@@ -0,0 +1,187 @@
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
@@ -2,7 +2,7 @@ require 'logger'
2
2
 
3
3
  module Terminalwire
4
4
  module Logging
5
- DEVICE = Logger.new($stdout, level: ENV.fetch("LOG_LEVEL", "info"))
5
+ DEVICE = Logger.new($stdout, level: ENV.fetch("TERMINALWIRE_LOG_LEVEL", "info"))
6
6
  def logger = DEVICE
7
7
  end
8
8
  end
@@ -2,57 +2,16 @@ require "thor"
2
2
 
3
3
  module Terminalwire
4
4
  module Server
5
- class MyCLI < ::Thor
6
- include Terminalwire::Thor
7
-
8
- desc "greet NAME", "Greet a person"
9
- def greet(name)
10
- name = ask "What's your name?"
11
- say "Hello, #{name}!"
12
- end
13
- end
14
-
15
- class Socket
16
- include Logging
17
-
18
- def initialize(server_socket)
19
- @server_socket = server_socket
20
- end
21
-
22
- def listen
23
- logger.info "Socket: Listening..."
24
- loop do
25
- client_socket = @server_socket.accept
26
- logger.debug "Socket: Client #{client_socket.inspect} connected"
27
- handle_client(client_socket)
28
- end
29
- end
30
-
31
- private
32
-
33
- def handle_client(socket)
34
- transport = Transport::Socket.new(socket)
35
- adapter = Adapter.new(transport)
36
-
37
- Thread.new do
38
- handler = Handler.new(adapter)
39
- handler.run
40
- end
41
- end
42
- end
43
-
44
5
  class WebSocket
45
6
  include Logging
46
7
 
47
8
  def call(env)
48
9
  Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
49
- run(Adapter::Socket.new(Terminalwire::Transport::WebSocket.new(connection)))
10
+ handle(Adapter::Socket.new(Terminalwire::Transport::WebSocket.new(connection)))
50
11
  end or [200, { "Content-Type" => "text/plain" }, ["Connect via WebSockets"]]
51
12
  end
52
13
 
53
- private
54
-
55
- def run(adapter)
14
+ def handle(adapter)
56
15
  while message = adapter.read
57
16
  puts message
58
17
  end
@@ -76,7 +35,7 @@ module Terminalwire
76
35
  "An error occurred. Please try again."
77
36
  end
78
37
 
79
- def run(adapter)
38
+ def handle(adapter)
80
39
  logger.info "ThorServer: Running #{@cli_class.inspect}"
81
40
  while message = adapter.read
82
41
  case message
@@ -100,38 +59,5 @@ module Terminalwire
100
59
  end
101
60
  end
102
61
  end
103
-
104
- class Handler
105
- include Logging
106
-
107
- def initialize(adapter)
108
- @adapter = adapter
109
- end
110
-
111
- def run
112
- logger.info "Server Handler: Running"
113
- loop do
114
- message = @adapter.read
115
- case message
116
- in { event: "initialization", protocol:, program: { arguments: }, entitlement: }
117
- Context.new(adapter: @adapter) do |context|
118
- MyCLI.start(arguments, context:)
119
- end
120
- end
121
- end
122
- rescue EOFError, Errno::ECONNRESET
123
- logger.info "Server Handler: Client disconnected"
124
- ensure
125
- @adapter.close
126
- end
127
- end
128
-
129
- def self.tcp(...)
130
- Server::Socket.new(TCPServer.new(...))
131
- end
132
-
133
- def self.socket(...)
134
- Server::Socket.new(UNIXServer.new(...))
135
- end
136
62
  end
137
63
  end
@@ -1,5 +1,4 @@
1
1
  require 'uri'
2
- require 'socket'
3
2
  require 'async/websocket/client'
4
3
 
5
4
  module Terminalwire
@@ -26,70 +25,6 @@ module Terminalwire
26
25
  end
27
26
  end
28
27
 
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
42
- end
43
-
44
- def read
45
- length = @socket.read(4)
46
- return nil if length.nil?
47
- length = length.unpack('L>')[0]
48
- @socket.read(length)
49
- end
50
-
51
- def write(data)
52
- length = [data.bytesize].pack('L>')
53
- @socket.write(length + data)
54
- end
55
-
56
- def close
57
- @socket.close
58
- end
59
- end
60
-
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
-
72
- def initialize(socket)
73
- @socket = socket
74
- end
75
-
76
- def read
77
- length = @socket.read(4)
78
- return nil if length.nil?
79
- length = length.unpack('L>')[0]
80
- @socket.read(length)
81
- end
82
-
83
- def write(data)
84
- length = [data.bytesize].pack('L>')
85
- @socket.write(length + data)
86
- end
87
-
88
- def close
89
- @socket.close
90
- end
91
- end
92
-
93
28
  class WebSocket < Base
94
29
  def self.connect(url)
95
30
  uri = URI(url)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terminalwire
4
- VERSION = "0.1.12"
4
+ VERSION = "0.1.14"
5
5
  end
data/lib/terminalwire.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "terminalwire/version"
4
4
 
5
- require 'socket'
6
5
  require 'forwardable'
7
6
  require 'uri'
8
7
  require 'zeitwerk'
@@ -40,10 +39,6 @@ module Terminalwire
40
39
  respond(status: "success", response:, **data)
41
40
  end
42
41
 
43
- def self.protocol_key
44
- name.split("::").last.downcase
45
- end
46
-
47
42
  private
48
43
 
49
44
  def respond(**response)
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.12
4
+ version: 0.1.14
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-14 00:00:00.000000000 Z
11
+ date: 2024-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-websocket
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.25'
19
+ version: '0.30'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.25'
26
+ version: '0.30'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: zeitwerk
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -165,6 +165,7 @@ files:
165
165
  - lib/terminalwire/client/entitlement.rb
166
166
  - lib/terminalwire/client/exec.rb
167
167
  - lib/terminalwire/client/resource.rb
168
+ - lib/terminalwire/licensing.rb
168
169
  - lib/terminalwire/logging.rb
169
170
  - lib/terminalwire/rails.rb
170
171
  - lib/terminalwire/server.rb