licentivo-launch 0.1.1 → 0.1.3
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/README.md +18 -6
- data/lib/licentivo_launch/vendor/licentivo.rb +17 -5
- data/lib/licentivo_launch.rb +164 -58
- data/vendor/VERSION +1 -1
- data/vendor/c/demo.c +13 -1
- data/vendor/c/device_logout.h +92 -0
- data/vendor/c/licentivo.c +97 -31
- data/vendor/c/licentivo.h +4 -0
- data/vendor/curl/bin/libcurl-x64.dll +0 -0
- data/vendor/curl/include/curl/curl.h +3 -4
- data/vendor/curl/include/curl/curlver.h +4 -4
- data/vendor/curl/include/curl/multi.h +0 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a541ba332c2a6d9adb50c359917ace42ef33495b30af65407e84a521ea081343
|
|
4
|
+
data.tar.gz: 868bda7f957341cc376c8b82377d7af7bab54b500c3cc3432fa190dfb2fe9029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 917ab9655d0e640d9e459d96e262521e4d711bbd62806e58bd521c241aedaefad3356e35d9f81f509b3ebcf15e24465cb7168b390519bd75d84a577b5c3dc205
|
|
7
|
+
data.tar.gz: 7bbc9bf5c8cb4a59094d44dc07a2a116be40344720ce1787b946ed94945161564fa3a944ec2773fe5ddd72f876ece9aa3ace604678a20964578ae270bcfb3fa8
|
data/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
# Launch Ruby 0.1.
|
|
1
|
+
# Launch Ruby 0.1.3
|
|
2
|
+
|
|
3
|
+
> Local account-exchange changes are unpublished. Login uses a short-lived identity assertion; Launch calculates purchased dates and Licentivo owns runtime permission. See the repository guide docs/account-purchase-boundary.md.
|
|
2
4
|
|
|
3
5
|
Ruby 3.1+. Install only the Launch gem matching the platform:
|
|
4
6
|
|
|
5
7
|
```sh
|
|
6
|
-
gem install
|
|
8
|
+
gem install licentivo-launch --version 0.1.3
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
SDK 0.11.
|
|
11
|
+
SDK 0.11.2 Ruby code, its private native core and Windows curl DLL are included. RubyGems resolves the ordinary base64 dependency. The generic source gem builds its own embedded core automatically and needs normal C11/curl/OpenSSL tools for the platform. No official SDK gem or external native-library override is needed.
|
|
10
12
|
|
|
11
13
|
## Direct initialization and manual lifecycle
|
|
12
14
|
|
|
@@ -26,10 +28,20 @@ restore, logout and release_device must handle errors. CancellationToken is coop
|
|
|
26
28
|
|
|
27
29
|
Launch automatically supplies the current merchant URL, existing product mapping and signing public keys when you select the software in its integration page. Copy its connection function, or use the values already shown there when writing code manually. Do not ask the integrator to type two internal product IDs. No launch-config.json or product.json file is required. The merchant URL is mandatory; the official licensing origin is fixed inside the package to https://www.licentivo.com.
|
|
28
30
|
|
|
29
|
-
The connection function
|
|
31
|
+
The connection function creates the account adapter. Login obtains a short-lived identity assertion, which is passed only in memory to the official SDK with the connection and environment. Remembered account records contain public routing context and the Launch account session; they never store the assertion, license key or signed runtime permission. Every protected action still passes the official SDK.
|
|
30
32
|
|
|
31
|
-
Normal close
|
|
33
|
+
Normal close preserves remembered login and device binding. Explicit Logout first marks the protected account record as signed out, delegates device logout to the official SDK and stops local use. Until Launch confirms sign-out (HTTP 200 or an already-invalid session via HTTP 401), a protected account logout task is retained. Login, Restore and subsequent Logout retry that task; they never restore it as a login. Process-restart recovery requires a persistent protected Store; MemoryStore lasts only for the current process. ReleaseDevice follows the same flow. Keep the official SDK cache and .logout.json journal intact; that SDK alone manages its device-release tickets.
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Launch Restore confirms the account online and obtains a new identity assertion. It never selects offline permission during a Launch outage. Applications requiring offline startup use the official SDK recovery contract directly. The official SDK alone owns signed permission, device credentials, offline expiry and logout recovery.
|
|
34
36
|
|
|
35
37
|
Manual development is supported without AI. The combined source bundle includes docs/manual-integration.md with all nine language flows, secure storage, error handling and acceptance procedures. Local package/runtime checks are distinct from external payment and licensing acceptance. Check the current publication status at https://github.com/spf86/licentivo-launch-sdk/blob/main/docs/registry-status.json before using public registry installation commands.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
<!-- account-store-ownership-2026-10-24 -->
|
|
41
|
+
## Local account logout and session ownership (unpublished)
|
|
42
|
+
|
|
43
|
+
Account logout tasks use a separate key in the configured secure store. Custom stores must support multiple opaque keys, atomic value replacement and isolation to the current OS user and host. Account lifecycle operations are coordinated across local instances/processes, independently of the licensing cache directory.
|
|
44
|
+
|
|
45
|
+
Logout also records a local, session-specific account exit intent using hashes of the account scope and bearer; it stores no bearer or licensing grant there. Restore checks that intent before requesting identity, so a failed vault write or lifecycle-lock timeout cannot turn an interrupted logout into a remembered login. A storage failure is reported explicitly. If both vault persistence and local intent storage are unavailable, handle the failure and retry before terminating; the SDK does not claim durable completion.
|
|
46
|
+
|
|
47
|
+
An old instance only changes its own account record and logout task. Repeating a completed logout does not adopt a newer remembered account. Retrying other sessions' account tasks never opens their licensing runtime. Licentivo owns device revocation, signed leases, offline permission and its shared cache files; ordinary close still preserves remembered login.
|
|
@@ -4,7 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'monitor'
|
|
5
5
|
|
|
6
6
|
module Licentivo
|
|
7
|
-
VERSION = '0.11.
|
|
7
|
+
VERSION = '0.11.2'
|
|
8
8
|
|
|
9
9
|
class Error < StandardError
|
|
10
10
|
attr_reader :code, :http_status, :request_id, :retryable, :retry_after_seconds
|
|
@@ -71,6 +71,7 @@ module Licentivo
|
|
|
71
71
|
resolve_metered_feature: ['ln_resolve_metered_feature', [p,p,i], i],
|
|
72
72
|
validate: ['ln_validate', [p], i], heartbeat: ['ln_heartbeat', [p], i],
|
|
73
73
|
refresh_online: ['ln_refresh_online', [p], i], deactivate: ['ln_deactivate', [p], i],
|
|
74
|
+
logout: ['ln_logout', [p], i], retry_pending_logouts: ['ln_retry_pending_logouts', [p], i], logout_pending: ['ln_logout_pending', [p], i],
|
|
74
75
|
reserve: ['ln_reserve', [p,p,q,p], p], consume: ['ln_consume', [p,p,q,p], p],
|
|
75
76
|
commit: ['ln_commit', [p,p,p], p], cancel: ['ln_cancel', [p,p,p], p],
|
|
76
77
|
offline_request: ['ln_offline_request', [p,p], p], import_offline: ['ln_import_offline', [p,p,p], i],
|
|
@@ -154,9 +155,15 @@ module Licentivo
|
|
|
154
155
|
def validate = checked(:validate)
|
|
155
156
|
def heartbeat = checked(:heartbeat)
|
|
156
157
|
def refresh_online = checked(:refresh_online)
|
|
157
|
-
def deactivate =
|
|
158
|
-
def
|
|
159
|
-
|
|
158
|
+
def deactivate = logout
|
|
159
|
+
def logout
|
|
160
|
+
@signed_out = true
|
|
161
|
+
checked(:logout)
|
|
162
|
+
end
|
|
163
|
+
def retry_pending_logouts = checked(:retry_pending_logouts)
|
|
164
|
+
def logout_pending? = @lock.synchronize { Native.call(:logout_pending, active) != 0 }
|
|
165
|
+
def status = @signed_out ? signed_out_status : read_json(:status)
|
|
166
|
+
def feature_status(feature) = @signed_out ? signed_out_status : read_json(:feature_status, Native.input(feature))
|
|
160
167
|
def reserve(feature, quantity, job_id) = read_json(:reserve, Native.input(feature), integer(quantity), Native.input(job_id))
|
|
161
168
|
def consume(feature, quantity, job_id) = read_json(:consume, Native.input(feature), integer(quantity), Native.input(job_id))
|
|
162
169
|
def commit(reservation_id, job_id) = read_json(:commit, Native.input(reservation_id), Native.input(job_id))
|
|
@@ -190,6 +197,7 @@ module Licentivo
|
|
|
190
197
|
end
|
|
191
198
|
|
|
192
199
|
private
|
|
200
|
+
def signed_out_status = { 'state' => 'released', 'allowed' => false, 'code' => 'LAUNCH_LOGIN_REQUIRED', 'request_id' => '', 'retryable' => false, 'lease_valid_until' => 0 }
|
|
193
201
|
def integer(value)
|
|
194
202
|
raise ArgumentError, 'Quantity must be a positive signed 64-bit integer' unless value.is_a?(Integer) && value.positive? && value <= 0x7fff_ffff_ffff_ffff
|
|
195
203
|
value
|
|
@@ -210,7 +218,10 @@ module Licentivo
|
|
|
210
218
|
retryable: info[212,4].unpack1('i') != 0, retry_after_seconds: info[216,4].unpack1('i'))
|
|
211
219
|
end
|
|
212
220
|
def checked(name, *arguments)
|
|
213
|
-
@lock.synchronize
|
|
221
|
+
@lock.synchronize do
|
|
222
|
+
raise Error.new('Complete a fresh login', code: 'LAUNCH_LOGIN_REQUIRED') if @signed_out && ![:logout, :deactivate, :retry_pending_logouts].include?(name)
|
|
223
|
+
raise error if Native.call(name, active, *arguments) == 0
|
|
224
|
+
end
|
|
214
225
|
true
|
|
215
226
|
end
|
|
216
227
|
def read_json(name, *arguments)
|
|
@@ -221,6 +232,7 @@ module Licentivo
|
|
|
221
232
|
end
|
|
222
233
|
end
|
|
223
234
|
def run_callback(name, *arguments)
|
|
235
|
+
raise Error.new('Complete a fresh login', code: 'LAUNCH_LOGIN_REQUIRED') if @signed_out
|
|
224
236
|
raise ArgumentError, 'Supply your business operation as a block' unless block_given?
|
|
225
237
|
@lock.synchronize do
|
|
226
238
|
raise Error.new('A business callback is already running', code: 'WORKFLOW_BUSY') if @in_callback
|
data/lib/licentivo_launch.rb
CHANGED
|
@@ -11,7 +11,7 @@ require 'time'
|
|
|
11
11
|
require 'monitor'
|
|
12
12
|
|
|
13
13
|
module LicentivoLaunch
|
|
14
|
-
VERSION = '0.1.
|
|
14
|
+
VERSION = '0.1.3'
|
|
15
15
|
LIMIT = 1 << 20
|
|
16
16
|
UUID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/
|
|
17
17
|
class Error < StandardError
|
|
@@ -163,12 +163,13 @@ module LicentivoLaunch
|
|
|
163
163
|
@store, @transport = store, transport || LicentivoLaunch.method(:default_transport)
|
|
164
164
|
@factory = runtime_factory || lambda do |config|
|
|
165
165
|
require_relative 'licentivo_launch/vendor/licentivo'
|
|
166
|
-
raise Error.new('SDK_VERSION_MISMATCH') unless Licentivo::VERSION == '0.11.
|
|
166
|
+
raise Error.new('SDK_VERSION_MISMATCH') unless Licentivo::VERSION == '0.11.2'
|
|
167
167
|
Licentivo::Client.new(config)
|
|
168
168
|
end
|
|
169
169
|
@root = File.expand_path(cache_dir || File.join(Dir.home, '.cache', 'LicentivoLaunch'))
|
|
170
|
-
@scope = ['launch-
|
|
170
|
+
@scope = ['launch-account-v2', @launch['merchant_origin'], @launch['launch_product_id'], @launch['licentivo_product_id'], @launch['environment']].join("\n")
|
|
171
171
|
@lock, @closed, @in_callback, @attempts = Monitor.new, false, false, pending_attempts
|
|
172
|
+
@account_cleared = false
|
|
172
173
|
@wait = sleep || lambda { |seconds, cancellation| (seconds * 10).ceil.times { cancellation&.check!; Kernel.sleep(0.1) }; cancellation&.check! }
|
|
173
174
|
end
|
|
174
175
|
def self.connect(merchant_origin, product_id, licentivo_product_id, trusted_keys, environment: 'live', **options)
|
|
@@ -177,23 +178,33 @@ module LicentivoLaunch
|
|
|
177
178
|
end
|
|
178
179
|
def self.from_files(launch_path, product_path, **options) = new(*LicentivoLaunch.load_config(launch_path, product_path), **options)
|
|
179
180
|
def login(email, password, cancellation: nil)
|
|
180
|
-
|
|
181
|
-
cancellation&.check!; logout_internal(cancellation)
|
|
181
|
+
account_serial do
|
|
182
|
+
cancellation&.check!; raise Error.new('LOGOUT_PENDING') unless logout_internal(cancellation)
|
|
182
183
|
grant = call('/auth/login', method: 'POST', body: { email: email, password: password, kind: 'desktop' }, cancellation: cancellation)
|
|
183
|
-
@record = { 'version' =>
|
|
184
|
+
@record = { 'version' => 2, 'scope' => @scope, 'user' => grant['user'], 'token' => grant['access_token'], 'expires_at' => grant['expires_at'] }
|
|
185
|
+
@account_cleared = false
|
|
184
186
|
begin
|
|
185
187
|
raise Error.new('INVALID_SESSION') unless valid_identity?(@record) && expiry(@record) > Time.now
|
|
186
188
|
@record['credential'] = credential(cancellation)
|
|
187
|
-
state = start_runtime(false, cancellation); @store.save(@scope, @record); state
|
|
188
|
-
rescue StandardError
|
|
189
|
-
logout_internal(nil);
|
|
189
|
+
state = start_runtime(false, cancellation); @store.save(@scope, @record.merge('credential' => @record['credential'].select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })); state
|
|
190
|
+
rescue StandardError => original
|
|
191
|
+
begin; logout_internal(nil); rescue StandardError; end
|
|
192
|
+
raise original
|
|
190
193
|
end
|
|
191
194
|
end
|
|
192
195
|
end
|
|
193
196
|
def restore(cancellation: nil)
|
|
194
|
-
|
|
195
|
-
cancellation&.check!; stop; @record = saved
|
|
197
|
+
account_serial do
|
|
198
|
+
cancellation&.check!; stop; @record = @record&.dig('logout_pending') ? @record : saved
|
|
199
|
+
task = pending_accounts.find { |record| !@record || record['token'] == @record['token'] }
|
|
200
|
+
@record = task if task
|
|
196
201
|
next nil unless @record
|
|
202
|
+
@record = @record.merge('version'=>3,'logout_pending'=>true) if has_intent?(@record)
|
|
203
|
+
if @record['logout_pending']
|
|
204
|
+
raise Error.new('ACCOUNT_LOGOUT_PENDING') unless logout_internal(cancellation)
|
|
205
|
+
next nil
|
|
206
|
+
end
|
|
207
|
+
retry_other_accounts(@record['token'], cancellation)
|
|
197
208
|
if expiry(@record) <= Time.now
|
|
198
209
|
clear; raise Error.new('SESSION_EXPIRED')
|
|
199
210
|
end
|
|
@@ -203,12 +214,12 @@ module LicentivoLaunch
|
|
|
203
214
|
@record['credential'] = credential(cancellation); @record['user'] = me['user']
|
|
204
215
|
rescue Error => e
|
|
205
216
|
cancellation&.check!
|
|
206
|
-
|
|
217
|
+
|
|
207
218
|
clear if e.code != 'ENTITLEMENT_PENDING' && ([401, 403, 404].include?(e.status) || %w[SESSION_EXPIRED SESSION_MISMATCH ENTITLEMENT_EXPIRED ENTITLEMENT_SUSPENDED ENTITLEMENT_NOT_PURCHASED].include?(e.code))
|
|
208
219
|
raise
|
|
209
220
|
end
|
|
210
221
|
begin
|
|
211
|
-
state = start_runtime(false, cancellation); @store.save(@scope, @record); state
|
|
222
|
+
state = start_runtime(false, cancellation); @store.save(@scope, @record.merge('credential' => @record['credential'].select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })); state
|
|
212
223
|
rescue StandardError
|
|
213
224
|
clear; raise
|
|
214
225
|
end
|
|
@@ -226,28 +237,50 @@ module LicentivoLaunch
|
|
|
226
237
|
end
|
|
227
238
|
end
|
|
228
239
|
end
|
|
229
|
-
def logout(cancellation: nil)
|
|
240
|
+
def logout(cancellation: nil)
|
|
241
|
+
account_serial(true) { if @account_cleared && !@record; retry_other_accounts(nil,cancellation);true;else;logout_internal(cancellation);end }
|
|
242
|
+
end
|
|
230
243
|
def release_device(cancellation: nil)
|
|
231
|
-
|
|
232
|
-
cancellation&.check!
|
|
233
|
-
raise Error.new('LOGIN_REQUIRED') unless @runtime
|
|
234
|
-
begin
|
|
235
|
-
@runtime.deactivate; cancellation&.check!
|
|
236
|
-
ensure
|
|
237
|
-
logout_internal(cancellation)
|
|
238
|
-
end
|
|
239
|
-
end
|
|
244
|
+
logout(cancellation: cancellation)
|
|
240
245
|
end
|
|
241
246
|
def close
|
|
247
|
+
pending = false
|
|
242
248
|
@lock.synchronize do
|
|
243
249
|
raise Error.new('WORKFLOW_BUSY') if @in_callback
|
|
244
250
|
return if @closed
|
|
245
251
|
@closed = true
|
|
252
|
+
pending = @record&.dig('logout_pending')
|
|
246
253
|
begin; stop; ensure; @record = nil; end
|
|
247
254
|
end
|
|
248
255
|
end
|
|
249
256
|
|
|
250
257
|
private
|
|
258
|
+
def account_serial(logout = false)
|
|
259
|
+
serial do
|
|
260
|
+
begin
|
|
261
|
+
begin; remember_intent(@record) if logout && @record; rescue SystemCallError, IOError; end
|
|
262
|
+
root = File.join(Dir.home, '.cache', 'LicentivoLaunch', 'account-locks', 'ruby')
|
|
263
|
+
FileUtils.mkdir_p(root, mode: 0o700)
|
|
264
|
+
File.open(File.join(root, Digest::SHA256.hexdigest(@scope) + '.lock'), File::RDWR | File::CREAT, 0o600) do |file|
|
|
265
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
266
|
+
until file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
267
|
+
raise Error.new('ACCOUNT_LOCK_BUSY') if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
268
|
+
Kernel.sleep(0.02)
|
|
269
|
+
end
|
|
270
|
+
begin; yield; ensure; file.flock(File::LOCK_UN); end
|
|
271
|
+
end
|
|
272
|
+
rescue StandardError
|
|
273
|
+
if logout
|
|
274
|
+
if @runtime
|
|
275
|
+
begin; @runtime.respond_to?(:logout) ? @runtime.logout : @runtime.deactivate; rescue StandardError; end
|
|
276
|
+
end
|
|
277
|
+
stop
|
|
278
|
+
@record = @record.merge('version' => 3, 'logout_pending' => true) if @record
|
|
279
|
+
end
|
|
280
|
+
raise
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
251
284
|
def serial
|
|
252
285
|
@lock.synchronize do
|
|
253
286
|
raise Error.new('APPLICATION_CLOSED') if @closed
|
|
@@ -258,18 +291,19 @@ module LicentivoLaunch
|
|
|
258
291
|
def cache(record) = File.join(@root, Digest::SHA256.hexdigest(@scope), Digest::SHA256.hexdigest(record.dig('user', 'id')), 'lease.json')
|
|
259
292
|
def expiry(record) = Time.iso8601(record['expires_at'])
|
|
260
293
|
def valid_identity?(record)
|
|
261
|
-
record.is_a?(Hash) && record['version'] ==
|
|
294
|
+
record.is_a?(Hash) && record['version'] == 2 && record['scope'] == @scope && record['user'].is_a?(Hash) && UUID.match?(record.dig('user', 'id').to_s) && record['token'].is_a?(String) && record['token'].match?(/\A[A-Za-z0-9_-]{43}\z/) && record['expires_at'].is_a?(String) && record['expires_at'].match?(/(?:Z|[+-]\d\d:\d\d)\z/) && expiry(record)
|
|
262
295
|
rescue ArgumentError, TypeError
|
|
263
296
|
false
|
|
264
297
|
end
|
|
265
298
|
def saved
|
|
266
299
|
record = @store.load(@scope)
|
|
267
|
-
if record
|
|
300
|
+
if record.is_a?(Hash) && record['logout_pending'] == true && [2, 3].include?(record['version']) && record['scope'] == @scope && record['token'].is_a?(String) && !record['token'].empty?
|
|
301
|
+
return record
|
|
302
|
+
end
|
|
303
|
+
if record && !(valid_identity?(record) && record['credential'].is_a?(Hash) && record.dig('credential', 'product_id') == @launch['launch_product_id'] && UUID.match?(record.dig('credential', 'launch_connection_id').to_s) && !record.dig('credential', 'identity_token'))
|
|
268
304
|
clear(record); raise Error.new('INVALID_SAVED_SESSION')
|
|
269
305
|
end
|
|
270
306
|
record
|
|
271
|
-
rescue StandardError
|
|
272
|
-
@store.delete(@scope); raise
|
|
273
307
|
end
|
|
274
308
|
def call(path, method: 'GET', body: nil, cancellation: nil, token: @record&.fetch('token', nil))
|
|
275
309
|
cancellation&.check!
|
|
@@ -295,29 +329,18 @@ module LicentivoLaunch
|
|
|
295
329
|
data['data']
|
|
296
330
|
end
|
|
297
331
|
def credential(cancellation)
|
|
298
|
-
@
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
raise Error.new('CREDENTIAL_MISMATCH') unless value['product_id'] == @launch['launch_product_id'] && value['license_key'].is_a?(String) && !value['license_key'].empty? && value['version'].is_a?(Integer) && value['version'] >= 1
|
|
302
|
-
return value
|
|
303
|
-
rescue Error => e
|
|
304
|
-
raise unless e.status == 403 && e.code == 'ENTITLEMENT_PENDING' && attempt + 1 < @attempts && e.retry_after_seconds <= 30
|
|
305
|
-
@wait.call([3, e.retry_after_seconds].max, cancellation); cancellation&.check!
|
|
306
|
-
end
|
|
307
|
-
end
|
|
332
|
+
value = call("/software/products/#{@launch['launch_product_id']}/identity", method: 'POST', body: { environment: @launch['environment'] }, cancellation: cancellation)
|
|
333
|
+
raise Error.new('IDENTITY_MISMATCH') unless value['product_id'] == @launch['launch_product_id'] && value['licentivo_product_id'] == @product['product_id'] && value['environment'] == @launch['environment'] && UUID.match?(value['launch_connection_id'].to_s) && value['identity_token'].is_a?(String) && value['identity_token'].length.between?(32, 12288)
|
|
334
|
+
value
|
|
308
335
|
end
|
|
309
336
|
def start_runtime(offline, cancellation)
|
|
310
337
|
cancellation&.check!
|
|
311
338
|
path = cache(@record); FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
312
|
-
|
|
339
|
+
context = @record.fetch('credential')
|
|
340
|
+
runtime = @factory.call(@product.merge('license_key' => '', 'identity_token' => context.fetch('identity_token', ''), 'launch_connection_id' => context.fetch('launch_connection_id'), 'environment' => @launch['environment'], 'cache_path' => path))
|
|
313
341
|
begin
|
|
314
|
-
cancellation&.check
|
|
315
|
-
|
|
316
|
-
state = runtime.status
|
|
317
|
-
raise Error.new('OFFLINE_UNAVAILABLE') unless state['allowed'] == true && state['state'] == 'offline_valid'
|
|
318
|
-
end
|
|
319
|
-
runtime.start; cancellation&.check!; @runtime = runtime
|
|
320
|
-
{ user: JSON.parse(JSON.generate(@record['user'])), expires_at: @record['expires_at'], offline_fallback: offline }
|
|
342
|
+
runtime.start; cancellation&.check!; @runtime = runtime; @account_cleared=false
|
|
343
|
+
{ user: JSON.parse(JSON.generate(@record['user'])), expires_at: @record['expires_at'] }
|
|
321
344
|
rescue StandardError
|
|
322
345
|
runtime.close; raise
|
|
323
346
|
end
|
|
@@ -327,26 +350,109 @@ module LicentivoLaunch
|
|
|
327
350
|
runtime&.close
|
|
328
351
|
end
|
|
329
352
|
def clear(record = @record)
|
|
330
|
-
begin
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
begin; stop; ensure; @record = nil; @account_cleared=true;delete_owned(record) if record; end
|
|
354
|
+
end
|
|
355
|
+
def queue_key = @scope + "\naccount-logout-queue-v1"
|
|
356
|
+
def intent_path(record)=File.join(Dir.home,'.cache','LicentivoLaunch','account-intents','ruby',Digest::SHA256.hexdigest(@scope)+'.'+Digest::SHA256.hexdigest(record['token'])+'.signedout')
|
|
357
|
+
def remember_intent(record)
|
|
358
|
+
path=intent_path(record);FileUtils.mkdir_p(File.dirname(path),mode:0o700)
|
|
359
|
+
File.open(path,File::WRONLY|File::CREAT,0o600){|file|file.write('1');file.flush;file.fsync}
|
|
360
|
+
end
|
|
361
|
+
def has_intent?(record)
|
|
362
|
+
File.stat(intent_path(record));true
|
|
363
|
+
rescue Errno::ENOENT
|
|
364
|
+
false
|
|
365
|
+
end
|
|
366
|
+
def pending_accounts
|
|
367
|
+
queue = @store.load(queue_key)
|
|
368
|
+
return [] unless queue
|
|
369
|
+
unless queue.is_a?(Hash) && queue['version'] == 1 && queue['records'].is_a?(Array) && queue['records'].length <= 64 && queue['records'].all? { |record| record.is_a?(Hash) && record['logout_pending'] == true && record['version'] == 3 && record['scope'] == @scope && record['token'].is_a?(String) && !record['token'].empty? && !record.dig('credential', 'identity_token') }
|
|
370
|
+
raise Error.new('INVALID_SAVED_SESSION')
|
|
371
|
+
end
|
|
372
|
+
queue['records']
|
|
373
|
+
end
|
|
374
|
+
def update_pending(record, remove: false)
|
|
375
|
+
records = pending_accounts.reject { |value| value['token'] == record['token'] }
|
|
376
|
+
unless remove
|
|
377
|
+
public_record = record.merge('version' => 3, 'logout_pending' => true, 'credential' => (record['credential'] || {}).select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })
|
|
378
|
+
records << public_record
|
|
379
|
+
end
|
|
380
|
+
raise Error.new('ACCOUNT_LOGOUT_QUEUE_FULL') if records.length > 64 || JSON.generate({ 'version' => 1, 'records' => records }).bytesize > 60000
|
|
381
|
+
records.empty? ? @store.delete(queue_key) : @store.save(queue_key, { 'version' => 1, 'records' => records })
|
|
382
|
+
end
|
|
383
|
+
def owns?(record)
|
|
384
|
+
active = @store.load(@scope)
|
|
385
|
+
record && active && active['token'] == record['token']
|
|
386
|
+
end
|
|
387
|
+
def delete_owned(record)
|
|
388
|
+
@store.delete(@scope) if owns?(record)
|
|
389
|
+
end
|
|
390
|
+
def mark_logout(record)
|
|
391
|
+
begin;remember_intent(record);rescue SystemCallError, IOError;end
|
|
392
|
+
queue_failure = nil
|
|
393
|
+
begin; update_pending(record); rescue StandardError => error; queue_failure = error; end
|
|
394
|
+
if owns?(record)
|
|
334
395
|
begin
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
stored = record.merge('credential' => (record['credential'] || {}).select { |key, _| %w[product_id licentivo_product_id launch_connection_id environment expires_at].include?(key) })
|
|
397
|
+
@store.save(@scope, stored)
|
|
398
|
+
return
|
|
399
|
+
rescue StandardError
|
|
400
|
+
begin; delete_owned(record); rescue StandardError; end
|
|
338
401
|
end
|
|
339
402
|
end
|
|
403
|
+
raise Error.new('ACCOUNT_LOGOUT_STORAGE_FAILED') if queue_failure
|
|
404
|
+
end
|
|
405
|
+
def finish_account(record)
|
|
406
|
+
delete_owned(record)
|
|
407
|
+
update_pending(record, remove: true)
|
|
408
|
+
begin;File.unlink(intent_path(record));rescue Errno::ENOENT;end
|
|
409
|
+
end
|
|
410
|
+
def retry_other_accounts(skip, cancellation)
|
|
411
|
+
pending_accounts.each do |record|
|
|
412
|
+
next if record['token'] == skip
|
|
413
|
+
begin; call('/auth/logout', method: 'POST', body: {}, cancellation: cancellation, token: record['token'])
|
|
414
|
+
rescue StandardError => error
|
|
415
|
+
raise Error.new('ACCOUNT_LOGOUT_PENDING') unless error.is_a?(Error) && error.status == 401
|
|
416
|
+
end
|
|
417
|
+
finish_account(record)
|
|
418
|
+
end
|
|
340
419
|
end
|
|
341
420
|
def logout_internal(cancellation)
|
|
342
|
-
record = @record || saved
|
|
343
|
-
|
|
344
|
-
|
|
421
|
+
record = @record || saved || pending_accounts.first
|
|
422
|
+
failure, persist_failure, other_failure, pending, confirmed = nil, nil, nil, false, true
|
|
423
|
+
begin; retry_other_accounts(record && record['token'], cancellation); rescue StandardError => error; other_failure = error; end
|
|
424
|
+
if record
|
|
425
|
+
@record = record.merge('version' => 3, 'logout_pending' => true)
|
|
426
|
+
record = @record
|
|
427
|
+
begin; mark_logout(record); rescue StandardError => error; persist_failure = error; end
|
|
428
|
+
end
|
|
345
429
|
begin
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
430
|
+
active = @store.load(@scope)
|
|
431
|
+
if !@runtime && record && (!active || active['token'] == record['token']) && record['credential'].is_a?(Hash) && record['credential']['launch_connection_id']
|
|
432
|
+
context = record.fetch('credential')
|
|
433
|
+
@runtime = @factory.call(@product.merge('license_key' => '', 'identity_token' => '', 'launch_connection_id' => context.fetch('launch_connection_id'), 'environment' => @launch['environment'], 'cache_path' => cache(record)))
|
|
434
|
+
end
|
|
435
|
+
if @runtime
|
|
436
|
+
result = @runtime.respond_to?(:logout) ? @runtime.logout : @runtime.deactivate
|
|
437
|
+
pending = (@runtime.respond_to?(:logout_pending?) && @runtime.logout_pending?) || (result.is_a?(Hash) && (result[:revocation_pending] || result['revocation_pending']))
|
|
438
|
+
raise Error.new('SDK_LOGOUT_STORAGE_FAILED') if result.is_a?(Hash) && (result[:storage_error] || result['storage_error'])
|
|
439
|
+
end
|
|
440
|
+
rescue StandardError => error
|
|
441
|
+
failure = error
|
|
442
|
+
ensure
|
|
443
|
+
stop
|
|
444
|
+
end
|
|
445
|
+
if record
|
|
446
|
+
begin; call('/auth/logout', method: 'POST', body: {}, cancellation: cancellation, token: record['token']); rescue StandardError => error; confirmed = error.is_a?(Error) && error.status == 401; end
|
|
447
|
+
end
|
|
448
|
+
if confirmed
|
|
449
|
+
finish_account(record) if record
|
|
450
|
+
clear(record)
|
|
349
451
|
end
|
|
452
|
+
raise persist_failure if !confirmed && persist_failure
|
|
453
|
+
raise failure if failure
|
|
454
|
+
raise other_failure if other_failure
|
|
455
|
+
confirmed && !pending
|
|
350
456
|
end
|
|
351
457
|
end
|
|
352
458
|
end
|
data/vendor/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.11.
|
|
1
|
+
0.11.2
|
data/vendor/c/demo.c
CHANGED
|
@@ -28,6 +28,18 @@ int main(int argc, char **argv) {
|
|
|
28
28
|
for (int i = 1; i < argc; i++) if (!strcmp(argv[i], "--cache-test")) cache_test = 1;
|
|
29
29
|
ln_client *client = ln_init_file(path);
|
|
30
30
|
if (!client) { fprintf(stderr, "C: invalid configuration\n"); return 1; }
|
|
31
|
+
for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--logout-wait")){
|
|
32
|
+
char ready[4096],gate[4096];snprintf(ready,sizeof(ready),"%s.ready",path);snprintf(gate,sizeof(gate),"%s.continue",path);FILE*f=ln_check_feature(client,"export")?fopen(ready,"wb"):NULL;if(!f){ln_destroy(client);return 1;}fputs("ready",f);fclose(f);double deadline=clock_seconds()+30;
|
|
33
|
+
for(;;){f=fopen(gate,"rb");if(f){fclose(f);break;}if(clock_seconds()>deadline){ln_destroy(client);return 1;}
|
|
34
|
+
#ifdef _WIN32
|
|
35
|
+
Sleep(10);
|
|
36
|
+
#else
|
|
37
|
+
struct timespec pause={0,10000000L};nanosleep(&pause,NULL);
|
|
38
|
+
#endif
|
|
39
|
+
}
|
|
40
|
+
int ok=ln_logout(client)&&!ln_check_feature(client,"export");ln_destroy(client);return ok?0:1;
|
|
41
|
+
}
|
|
42
|
+
for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--logout-retry")){int ok=ln_retry_pending_logouts(client)&&!ln_check_feature(client,"export");ln_destroy(client);if(ok)puts("C: pending logout completed");return ok?0:1;}
|
|
31
43
|
for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--improvements-test")){int ok=improvements(client,path);if(!ok)fprintf(stderr,"C: %s\n",ln_last_error(client));ln_destroy(client);if(ok)puts("C: credential / reservation / typed status OK");return ok?0:1;}
|
|
32
44
|
for(int i=1;i<argc;i++)if(!strcmp(argv[i],"--startup-test")){double start=clock_seconds();int ok=ln_start(client)&&clock_seconds()-start<.3;start=clock_seconds();ok=ok&&ln_check_feature(client,"export")&&clock_seconds()-start<.3;ln_destroy(client);if(ok)puts("C: cached startup and local feature checks stay responsive");return ok?0:1;}
|
|
33
45
|
int advanced=0,offline_export=0,offline_import=0,floating=0;
|
|
@@ -37,7 +49,7 @@ int main(int argc, char **argv) {
|
|
|
37
49
|
int ok=1;if(!strcmp(argv[i],"--transfer-import")){char file[4096];snprintf(file,sizeof(file),"%s.request.json",path);char*req=read_text(file);snprintf(file,sizeof(file),"%s.response.json",path);char*res=read_text(file);ok=req&&res&&ln_import_offline(client,req,res);free(req);free(res);}else ok=ln_start(client);
|
|
38
50
|
ok=ok&&ln_validate(client)&&ln_heartbeat(client)&&ln_check_feature(client,"export");if(!ok)fprintf(stderr,"C: %s\n",ln_last_error(client));ln_destroy(client);if(ok)puts("C: transferred device credential OK");return ok?0:1;
|
|
39
51
|
}
|
|
40
|
-
if(floating){int ok=ln_start(client);ln_client*other=ln_init_file(path);ok=ok&&other&&!ln_check_feature(other,"export")
|
|
52
|
+
if(floating){int ok=ln_start(client);ln_client*other=ln_init_file(path);ok=ok&&other&&!ln_check_feature(other,"export")&&ln_start(other);ln_destroy(client);ok=ok&&ln_start(other)&&ln_check_feature(other,"export");ln_destroy(other);if(ok)puts("C: floating seat release OK");return ok?0:1;}
|
|
41
53
|
if(advanced||offline_import){int ok=1;
|
|
42
54
|
if(offline_import){char file[4096];snprintf(file,sizeof(file),"%s.request.json",path);char*req=read_text(file);snprintf(file,sizeof(file),"%s.response.json",path);char*res=read_text(file);ok=req&&res&&ln_import_offline(client,req,res);free(req);free(res);}else ok=ln_start(client);
|
|
43
55
|
ok=ok&&ln_check_feature(client,"export");char*r=ok?ln_consume(client,"export",1,"business-job-1"):NULL;ok=ok&&used_is(r,1,0);ln_free_string(r);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* Internal implementation, included after the transport helpers in licentivo.c. */
|
|
2
|
+
static char*logout_path(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p)return NULL;char*out=malloc(strlen(p)+13);if(out)sprintf(out,"%s.logout.json",p);return out;}
|
|
3
|
+
static cJSON*pending_logouts(ln_client*c){return (cJSON*)get(c->logout_state,"pending");}
|
|
4
|
+
static int has_pending_logouts(ln_client*c){return cJSON_GetArraySize(pending_logouts(c))>0;}
|
|
5
|
+
static void new_logout_state(ln_client*c){c->logout_state=cJSON_CreateObject();cJSON_AddNumberToObject(c->logout_state,"version",1);cJSON_AddStringToObject(c->logout_state,"source_hash","");cJSON_AddBoolToObject(c->logout_state,"signed_out",0);cJSON_AddArrayToObject(c->logout_state,"pending");}
|
|
6
|
+
static const char*logout_text(const cJSON*s,const char*key){const char*v=text(s,key);return v?v:"";}
|
|
7
|
+
static void logout_binding(const cJSON*t,char out[80]){int64_t generation=0;const char*a=text(t,"activation_id");if(a&&num(t,"expected_generation",&generation))snprintf(out,80,"%s:%lld",a,(long long)generation);else out[0]=0;}
|
|
8
|
+
static char*logout_directory(ln_client*c){
|
|
9
|
+
const char*root=getenv("LOCALAPPDATA");if(!root||!root[0])root=getenv("XDG_CACHE_HOME");char*owned=NULL;
|
|
10
|
+
if(!root||!root[0]){const char*home=getenv("HOME");if(!home||!home[0])return NULL;owned=malloc(strlen(home)+8);if(!owned)return NULL;sprintf(owned,"%s/.cache",home);root=owned;}
|
|
11
|
+
const char*cache=text(c->cfg,"cache_path");char*absolute=NULL;
|
|
12
|
+
#ifdef _WIN32
|
|
13
|
+
wchar_t*wide=state_windows_path(cache);if(wide){const wchar_t*start=!wcsncmp(wide,L"\\\\?\\",4)?wide+4:wide;int n=WideCharToMultiByte(CP_UTF8,0,start,-1,NULL,0,NULL,NULL);absolute=malloc((size_t)n);if(absolute)WideCharToMultiByte(CP_UTF8,0,start,-1,absolute,n,NULL,NULL);free(wide);}
|
|
14
|
+
#else
|
|
15
|
+
if(cache[0]=='/')absolute=strdup(cache);else{char*cwd=getcwd(NULL,0);if(cwd){absolute=malloc(strlen(cwd)+strlen(cache)+2);if(absolute)sprintf(absolute,"%s/%s",cwd,cache);free(cwd);}}
|
|
16
|
+
#endif
|
|
17
|
+
if(!absolute){free(owned);return NULL;}size_t n=strlen(absolute)+strlen(text(c->cfg,"base_url"))+strlen(text(c->cfg,"product_id"))+strlen(text(c->cfg,"device_id"))+4;char*context=malloc(n);char scope[65];
|
|
18
|
+
if(!context){free(absolute);free(owned);return NULL;}snprintf(context,n,"%s\n%s\n%s\n%s",absolute,text(c->cfg,"base_url"),text(c->cfg,"product_id"),text(c->cfg,"device_id"));int ok=hash(context,scope);free(absolute);free(context);char*out=ok?malloc(strlen(root)+strlen(scope)+20):NULL;if(out)sprintf(out,"%s/Licentivo/logout/%s",root,scope);free(owned);return out;
|
|
19
|
+
}
|
|
20
|
+
static char*logout_scope(const cJSON*s){const char*a=logout_text(s,"source_hash"),*b=logout_text(s,"binding_key");char*out=malloc(strlen(a)+strlen(b)+2);if(out)sprintf(out,"%s\n%s",a,b);return out;}
|
|
21
|
+
static int save_logout_state(ln_client*c){
|
|
22
|
+
int64_t revision=0;num(c->logout_state,"revision",&revision);int64_t now=(int64_t)time(NULL)*1000;revision=revision+1>now?revision+1:now;cJSON_DeleteItemFromObjectCaseSensitive(c->logout_state,"revision");cJSON_AddNumberToObject(c->logout_state,"revision",(double)revision);
|
|
23
|
+
char*dir=logout_directory(c),*scope=logout_scope(c->logout_state),h[65];int fallback=0;if(dir&&scope&&hash(scope,h)){char*p=malloc(strlen(dir)+72);if(p){sprintf(p,"%s/%s.json",dir,h);fallback=write_state(p,c->logout_state);free(p);}}free(dir);free(scope);
|
|
24
|
+
char*p=logout_path(c);int primary=p&&write_state(p,c->logout_state);free(p);return fallback||(cJSON_IsTrue(get(c->logout_state,"signed_out"))&&primary);
|
|
25
|
+
}
|
|
26
|
+
static void read_logout_record(ln_client*c,cJSON*records,const char*path){
|
|
27
|
+
errno=0;char*b=readfile(path,65536);int absent=errno==ENOENT;if(!b){if(!absent)set_denied(c);return;}cJSON*s=ln_json(b);free(b);int64_t version;
|
|
28
|
+
if(!s||!num(s,"version",&version)||version!=1||!cJSON_IsArray(get(s,"pending"))){cJSON_Delete(s);set_denied(c);return;}char*scope=logout_scope(s);if(scope){const cJSON*old=get(records,scope);int64_t before=0,after=0;num(old,"revision",&before);num(s,"revision",&after);if(!old||after>before||(after==before&&(cJSON_IsTrue(get(s,"signed_out"))||!cJSON_IsTrue(get(old,"signed_out"))))){cJSON_DeleteItemFromObjectCaseSensitive(records,scope);cJSON_AddItemToObject(records,scope,s);}else cJSON_Delete(s);free(scope);}else cJSON_Delete(s);
|
|
29
|
+
}
|
|
30
|
+
static cJSON*logout_records(ln_client*c){
|
|
31
|
+
cJSON*records=cJSON_CreateObject();char*p=logout_path(c);if(p){read_logout_record(c,records,p);free(p);}else set_denied(c);char*dir=logout_directory(c);
|
|
32
|
+
if(dir){
|
|
33
|
+
#ifdef _WIN32
|
|
34
|
+
char*pattern=malloc(strlen(dir)+8);if(pattern){sprintf(pattern,"%s/*.json",dir);wchar_t*wide=state_windows_path(pattern);free(pattern);WIN32_FIND_DATAW data;HANDLE search=wide?FindFirstFileW(wide,&data):INVALID_HANDLE_VALUE;free(wide);if(search!=INVALID_HANDLE_VALUE){do{char name[256];int n=WideCharToMultiByte(CP_UTF8,0,data.cFileName,-1,name,sizeof(name),NULL,NULL);if(n>0){p=malloc(strlen(dir)+strlen(name)+2);if(p){sprintf(p,"%s/%s",dir,name);read_logout_record(c,records,p);free(p);}}}while(FindNextFileW(search,&data));FindClose(search);}else if(GetLastError()!=ERROR_FILE_NOT_FOUND&&GetLastError()!=ERROR_PATH_NOT_FOUND)set_denied(c);}
|
|
35
|
+
#else
|
|
36
|
+
DIR*entries=opendir(dir);if(entries){struct dirent*entry;while((entry=readdir(entries))){size_t n=strlen(entry->d_name);if(n>5&&!strcmp(entry->d_name+n-5,".json")){p=malloc(strlen(dir)+n+2);if(p){sprintf(p,"%s/%s",dir,entry->d_name);read_logout_record(c,records,p);free(p);}}}closedir(entries);}else if(errno!=ENOENT)set_denied(c);
|
|
37
|
+
#endif
|
|
38
|
+
free(dir);
|
|
39
|
+
}
|
|
40
|
+
return records;
|
|
41
|
+
}
|
|
42
|
+
static void load_logout_state(ln_client*c){
|
|
43
|
+
new_logout_state(c);c->logout_bindings=cJSON_CreateObject();cJSON*records=logout_records(c);
|
|
44
|
+
char h[65]="",binding[80];source_hash(c,h);logout_binding(get(c->auth,"logout_ticket"),binding);cJSON*tickets=cJSON_CreateArray();
|
|
45
|
+
for(cJSON*s=records->child;s;s=s->next){const char*b=logout_text(s,"binding_key");if(b[0]&&cJSON_IsTrue(get(s,"signed_out"))&&!get(c->logout_bindings,b))cJSON_AddBoolToObject(c->logout_bindings,b,1);}
|
|
46
|
+
for(cJSON*s=records->child;s;s=s->next){const char*b=logout_text(s,"binding_key");if((!strcmp(h,logout_text(s,"source_hash"))||(logout_text(c->cfg,"launch_connection_id")[0]&&binding[0]&&!strcmp(b,binding))||(!c->auth&&!source_input(c)[0]))&&(!b[0]||!binding[0]||!strcmp(b,binding))){cJSON_Delete(c->logout_state);c->logout_state=cJSON_Duplicate(s,1);if(cJSON_IsTrue(get(s,"signed_out"))&&!text(c->cfg,"license_key")[0])set_denied(c);}for(const cJSON*t=get(s,"pending")->child;t;t=t->next){int found=0;for(cJSON*x=tickets->child;x;x=x->next)if(!strcmp(logout_text(t,"operation_id"),logout_text(x,"operation_id")))found=1;if(!found)cJSON_AddItemToArray(tickets,cJSON_Duplicate(t,1));}}
|
|
47
|
+
cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"pending",tickets);cJSON_Delete(records);
|
|
48
|
+
if(is_denied(c)){cJSON_Delete(c->auth);c->auth=NULL;}
|
|
49
|
+
}
|
|
50
|
+
static int acknowledge_logout(ln_client*c,const char*operation){
|
|
51
|
+
cJSON*records=logout_records(c);char*dir=logout_directory(c);int ok=dir!=NULL;
|
|
52
|
+
for(cJSON*s=records->child;s&&ok;s=s->next){cJSON*pending=(cJSON*)get(s,"pending");int changed=0;for(int i=cJSON_GetArraySize(pending)-1;i>=0;i--){if(!strcmp(logout_text(cJSON_GetArrayItem(pending,i),"operation_id"),operation)){cJSON_DeleteItemFromArray(pending,i);changed=1;}}
|
|
53
|
+
if(changed){char*scope=logout_scope(s),h[65];ok=scope&&hash(scope,h);if(ok){char*p=malloc(strlen(dir)+72);ok=p!=NULL;if(p){sprintf(p,"%s/%s.json",dir,h);ok=write_state(p,s);free(p);}}free(scope);}
|
|
54
|
+
}
|
|
55
|
+
free(dir);cJSON_Delete(records);return ok;
|
|
56
|
+
}
|
|
57
|
+
static int revocation_error(const char*code){const char*codes[]={"LICENSE_REVOKED","LICENSE_SUSPENDED","LICENSE_EXPIRED","DEVICE_RELEASED","DEVICE_NOT_ACTIVE","DEVICE_CREDENTIAL_REVOKED","DEVICE_CREDENTIAL_INVALID","AUTHORIZATION_SESSION_REVOKED","AUTHORIZATION_SESSION_EXPIRED","LAUNCH_LOGIN_REQUIRED","LAUNCH_ENTITLEMENT_REVOKED","LAUNCH_ENTITLEMENT_EXPIRED"};for(size_t i=0;i<sizeof(codes)/sizeof(codes[0]);i++)if(!strcmp(code,codes[i]))return 1;return 0;}
|
|
58
|
+
static int deny_runtime(ln_client*c){
|
|
59
|
+
set_denied(c);if(!c->logout_state)new_logout_state(c);char h[65];
|
|
60
|
+
if((source_input(c)[0]||c->auth)&&source_hash(c,h))cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"source_hash",cJSON_CreateString(h));
|
|
61
|
+
cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"signed_out",cJSON_CreateBool(1));
|
|
62
|
+
const cJSON*ticket=get(c->auth,"logout_ticket");const char*operation=text(ticket,"operation_id");int found=0;
|
|
63
|
+
char binding[80];logout_binding(ticket,binding);if(binding[0]){cJSON_DeleteItemFromObjectCaseSensitive(c->logout_state,"binding_key");cJSON_AddStringToObject(c->logout_state,"binding_key",binding);if(c->logout_bindings&&!get(c->logout_bindings,binding))cJSON_AddBoolToObject(c->logout_bindings,binding,1);}
|
|
64
|
+
if(operation){for(cJSON*p=pending_logouts(c)->child;p;p=p->next)if(text(p,"operation_id")&&!strcmp(operation,text(p,"operation_id")))found=1;if(!found)cJSON_AddItemToArray(pending_logouts(c),cJSON_Duplicate(ticket,1));}
|
|
65
|
+
int ok=save_logout_state(c);invalidate(c);cJSON_Delete(c->auth);c->auth=NULL;c->auth_dirty=0;
|
|
66
|
+
/* Keep the immutable configuration graph stable for concurrent local checks. */
|
|
67
|
+
const char*private_names[]={"license_key","identity_token"};for(int i=0;i<2;i++){char*secret=(char*)text(c->cfg,private_names[i]);if(secret){size_t count=strlen(secret);volatile unsigned char*p=(volatile unsigned char*)secret;while(count--)*p++=0;}}return ok;
|
|
68
|
+
}
|
|
69
|
+
static int logout_binding_blocked(ln_client*c,const cJSON*result){
|
|
70
|
+
const char*connection=text(c->cfg,"launch_connection_id");char binding[80];const cJSON*ticket=get(result,"logout_ticket");logout_binding(ticket,binding);
|
|
71
|
+
if(!connection||!connection[0]||!cJSON_IsTrue(get(c->logout_bindings,binding)))return 0;
|
|
72
|
+
if(c->auth){cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(ticket,1));}deny_runtime(c);strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");return 1;
|
|
73
|
+
}
|
|
74
|
+
static int retry_logouts_unlocked(ln_client*c){
|
|
75
|
+
while(has_pending_logouts(c)){
|
|
76
|
+
cJSON*ticket=cJSON_GetArrayItem(pending_logouts(c),0);char*body=cJSON_PrintUnformatted(ticket);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+32);if(!body||!url){free(body);free(url);return 0;}sprintf(url,"%s/api/v1/sessions/logout",base);
|
|
77
|
+
CURL*curl=curl_easy_init();if(!curl){free(body);free(url);return 0;}struct buffer b={{0},0,0};struct curl_slist*headers=curl_slist_append(NULL,"Content-Type: application/json");
|
|
78
|
+
curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&b);curl_easy_setopt(curl,CURLOPT_TIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1L);curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);
|
|
79
|
+
const char*proxy=text(c->cfg,"proxy_url"),*ca=getenv("LICENTIVO_CA_BUNDLE");if(proxy&&proxy[0])curl_easy_setopt(curl,CURLOPT_PROXY,proxy);if(ca&&ca[0])curl_easy_setopt(curl,CURLOPT_CAINFO,ca);
|
|
80
|
+
CURLcode error=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);curl_slist_free_all(headers);free(body);free(url);
|
|
81
|
+
cJSON*envelope=error==CURLE_OK&&!b.oversized?ln_json(b.data):NULL;const cJSON*ack=get(envelope,"data");const char*state=text(ack,"status"),*operation=text(ack,"operation_id"),*activation=text(ack,"activation_id");int64_t generation=0,expected=0;
|
|
82
|
+
int ok=status==200&&state&&(!strcmp(state,"succeeded")||!strcmp(state,"stale"))&&operation&&activation&&text(ticket,"operation_id")&&text(ticket,"activation_id")&&!strcmp(operation,text(ticket,"operation_id"))&&!strcmp(activation,text(ticket,"activation_id"))&&num(ack,"expected_generation",&generation)&&num(ticket,"expected_generation",&expected)&&generation==expected;cJSON_Delete(envelope);if(!ok)return 0;
|
|
83
|
+
char completed[128];snprintf(completed,sizeof(completed),"%s",logout_text(ticket,"operation_id"));cJSON_DeleteItemFromArray(pending_logouts(c),0);if(!acknowledge_logout(c,completed)||!save_logout_state(c))return 0;
|
|
84
|
+
}
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
int ln_retry_pending_logouts(ln_client*c){if(!c)return 0;lock(c);int ok=retry_logouts_unlocked(c);unlock(c);return ok;}
|
|
88
|
+
int ln_logout_pending(ln_client*c){if(!c)return 0;lock(c);int pending=has_pending_logouts(c);unlock(c);return pending;}
|
|
89
|
+
int ln_logout(ln_client*c){
|
|
90
|
+
if(!c)return 0;
|
|
91
|
+
set_denied(c);lock(c);int ok=deny_runtime(c);retry_logouts_unlocked(c);int pending=has_pending_logouts(c);strcpy(c->error,ok?"DEVICE_RELEASED":"LOCAL_CREDENTIAL_CLEANUP_FAILED");publish(c,0);unlock(c);notify(c);if(pending)ln_start_heartbeat(c);return ok;
|
|
92
|
+
}
|
data/vendor/c/licentivo.c
CHANGED
|
@@ -15,17 +15,51 @@
|
|
|
15
15
|
#include <math.h>
|
|
16
16
|
#include <errno.h>
|
|
17
17
|
#include <sys/stat.h>
|
|
18
|
+
#include <wchar.h>
|
|
18
19
|
#ifdef _WIN32
|
|
19
20
|
#include <windows.h>
|
|
20
21
|
#include <bcrypt.h>
|
|
21
22
|
#include <direct.h>
|
|
22
|
-
#
|
|
23
|
+
#include <io.h>
|
|
24
|
+
/* State journals can exceed MAX_PATH under a caller's cache directory. Use
|
|
25
|
+
* absolute UTF-16 paths with the extended prefix for every state-file operation. */
|
|
26
|
+
static wchar_t*state_windows_path(const char*path){
|
|
27
|
+
int size=MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,path,-1,NULL,0);if(!size){errno=EINVAL;return NULL;}
|
|
28
|
+
wchar_t*input=malloc((size_t)size*sizeof(wchar_t));if(!input){errno=ENOMEM;return NULL;}
|
|
29
|
+
if(!MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,path,-1,input,size)){free(input);errno=EINVAL;return NULL;}
|
|
30
|
+
for(wchar_t*p=input;*p;p++)if(*p==L'/')*p=L'\\';
|
|
31
|
+
if(!wcsncmp(input,L"\\\\?\\",4))return input;
|
|
32
|
+
DWORD count=GetFullPathNameW(input,0,NULL,NULL);if(!count||count>32760){free(input);errno=EINVAL;return NULL;}
|
|
33
|
+
wchar_t*absolute=malloc((size_t)count*sizeof(wchar_t));if(!absolute){free(input);errno=ENOMEM;return NULL;}
|
|
34
|
+
DWORD written=GetFullPathNameW(input,count,absolute,NULL);free(input);if(!written||written>=count){free(absolute);errno=EINVAL;return NULL;}
|
|
35
|
+
int unc=absolute[0]==L'\\'&&absolute[1]==L'\\';
|
|
36
|
+
wchar_t*result=malloc(((size_t)written+9)*sizeof(wchar_t));if(!result){free(absolute);errno=ENOMEM;return NULL;}
|
|
37
|
+
wcscpy(result,unc?L"\\\\?\\UNC\\":L"\\\\?\\");wcscat(result,absolute+(unc?2:0));free(absolute);return result;
|
|
38
|
+
}
|
|
39
|
+
static FILE*state_windows_open(const char*path,const char*mode){
|
|
40
|
+
wchar_t*p=state_windows_path(path),wide_mode[8];if(!p)return NULL;size_t length=strlen(mode);if(length>=8){free(p);errno=EINVAL;return NULL;}
|
|
41
|
+
for(size_t i=0;i<=length;i++){wide_mode[i]=(wchar_t)(unsigned char)mode[i];}
|
|
42
|
+
FILE*f=_wfopen(p,wide_mode);free(p);return f;
|
|
43
|
+
}
|
|
44
|
+
static int state_windows_remove(const char*path){wchar_t*p=state_windows_path(path);if(!p)return -1;int result=_wremove(p);free(p);return result;}
|
|
45
|
+
static int state_windows_mkdir(const char*path){wchar_t*p=state_windows_path(path);if(!p)return -1;int result=_wmkdir(p);free(p);return result;}
|
|
46
|
+
static int state_windows_stat(const char*path,struct stat*info){
|
|
47
|
+
wchar_t*p=state_windows_path(path);if(!p)return -1;WIN32_FILE_ATTRIBUTE_DATA data;BOOL ok=GetFileAttributesExW(p,GetFileExInfoStandard,&data);DWORD error=GetLastError();free(p);
|
|
48
|
+
if(!ok){errno=error==ERROR_FILE_NOT_FOUND||error==ERROR_PATH_NOT_FOUND?ENOENT:EACCES;return -1;}
|
|
49
|
+
memset(info,0,sizeof(*info));info->st_size=data.nFileSizeHigh||data.nFileSizeLow>2147483647?2147483647:(long)data.nFileSizeLow;return 0;
|
|
50
|
+
}
|
|
51
|
+
static int state_windows_move(const char*from,const char*to){wchar_t*a=state_windows_path(from),*b=state_windows_path(to);int ok=a&&b&&MoveFileExW(a,b,MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH);free(a);free(b);return ok;}
|
|
52
|
+
#define fopen(path,mode) state_windows_open(path,mode)
|
|
53
|
+
#define remove(path) state_windows_remove(path)
|
|
54
|
+
#define stat(path,info) state_windows_stat(path,info)
|
|
55
|
+
#define mkdir_private(p) state_windows_mkdir(p)
|
|
23
56
|
#else
|
|
24
57
|
#include <openssl/evp.h>
|
|
25
58
|
#include <openssl/rand.h>
|
|
26
59
|
#include <pthread.h>
|
|
27
60
|
#include <stdatomic.h>
|
|
28
61
|
#include <unistd.h>
|
|
62
|
+
#include <dirent.h>
|
|
29
63
|
#define mkdir_private(p) mkdir(p,0700)
|
|
30
64
|
#endif
|
|
31
65
|
#define LIMIT 16384
|
|
@@ -48,14 +82,29 @@ static int ensure_curl(void) { return pthread_once(&curl_once, initialize_curl)
|
|
|
48
82
|
#endif
|
|
49
83
|
|
|
50
84
|
struct ln_client {
|
|
51
|
-
cJSON *cfg,*cached,*auth;int auth_dirty;char session[37];int interval;int offline;int64_t wall;double tick;char error[80];int started;int finished;int refresh_pending;int online;int retryable;long http_status;int retry_after;char request_id[128];int64_t retry_at;
|
|
85
|
+
cJSON *cfg,*cached,*auth,*logout_state,*logout_bindings;int auth_dirty;char session[37];int interval;int offline;int64_t wall;double tick;char error[80];int started;int finished;int refresh_pending;int online;int retryable;long http_status;int retry_after;char request_id[128];int64_t retry_at;
|
|
52
86
|
cJSON *view;int64_t view_wall;double view_tick;int view_online;ln_error_info view_error;ln_status_callback callback;void *callback_data;int task_busy;
|
|
53
87
|
#ifdef _WIN32
|
|
54
|
-
CRITICAL_SECTION mutex,view_mutex;HANDLE thread;volatile LONG stop;
|
|
88
|
+
CRITICAL_SECTION mutex,view_mutex;HANDLE thread;volatile LONG stop,denied;
|
|
55
89
|
#else
|
|
56
|
-
pthread_mutex_t mutex,view_mutex;pthread_t thread;atomic_int stop;
|
|
90
|
+
pthread_mutex_t mutex,view_mutex;pthread_t thread;atomic_int stop,denied;
|
|
57
91
|
#endif
|
|
58
92
|
};
|
|
93
|
+
static int is_denied(ln_client*c){
|
|
94
|
+
#ifdef _WIN32
|
|
95
|
+
return InterlockedCompareExchange(&c->denied,0,0)!=0;
|
|
96
|
+
#else
|
|
97
|
+
return atomic_load(&c->denied);
|
|
98
|
+
#endif
|
|
99
|
+
}
|
|
100
|
+
static void set_denied(ln_client*c){
|
|
101
|
+
#ifdef _WIN32
|
|
102
|
+
InterlockedExchange(&c->denied,1);
|
|
103
|
+
#else
|
|
104
|
+
atomic_store(&c->denied,1);
|
|
105
|
+
#endif
|
|
106
|
+
}
|
|
107
|
+
static void load_logout_state(ln_client*c);static int deny_runtime(ln_client*c);static int revocation_error(const char*code);static int retry_logouts_unlocked(ln_client*c);static int has_pending_logouts(ln_client*c);
|
|
59
108
|
static const cJSON *get(const cJSON *o,const char*k){return cJSON_GetObjectItemCaseSensitive(o,k);}
|
|
60
109
|
static const char *text(const cJSON *o,const char*k){const cJSON*v=get(o,k);return cJSON_IsString(v)?v->valuestring:NULL;}
|
|
61
110
|
static int num(const cJSON*o,const char*k,int64_t*out){const cJSON*v=get(o,k);if(!cJSON_IsNumber(v)||!isfinite(v->valuedouble)||v->valuedouble!=floor(v->valuedouble)||fabs(v->valuedouble)>9007199254740991.0)return 0;*out=(int64_t)v->valuedouble;return 1;}
|
|
@@ -65,13 +114,17 @@ static int fields(const cJSON*v,const char*const*names,int n){if(!cJSON_IsObject
|
|
|
65
114
|
static int uuid(const char*s){if(!s||strlen(s)!=36)return 0;for(int i=0;i<36;i++){if(i==8||i==13||i==18||i==23){if(s[i]!='-')return 0;}else if(!isxdigit((unsigned char)s[i]))return 0;}return 1;}
|
|
66
115
|
static int device(const char*s){if(!s||strlen(s)!=64)return 0;for(int i=0;i<64;i++)if(!((s[i]>='a'&&s[i]<='f')||(s[i]>='0'&&s[i]<='9')))return 0;return 1;}
|
|
67
116
|
static int credential_secret(const char*s,const char*prefix){if(!s||strlen(s)!=50||strncmp(s,prefix,7))return 0;for(int i=7;i<50;i++)if(!isalnum((unsigned char)s[i])&&s[i]!='_'&&s[i]!='-')return 0;return 1;}
|
|
68
|
-
static
|
|
117
|
+
static const char*source_input(ln_client*c){const char*identity=text(c->cfg,"identity_token");return identity&&identity[0]?identity:text(c->cfg,"license_key");}
|
|
118
|
+
static int source_hash(ln_client*c,char out[65]);
|
|
119
|
+
static int temporary(ln_client*c){return c->auth!=NULL||source_input(c)[0]!=0;}
|
|
69
120
|
static const char*runtime_key(ln_client*c){const char*k=text(c->auth,"device_key");return k&&k[0]?k:text(c->cfg,"license_key");}
|
|
70
121
|
static void publish(ln_client*c,int online);
|
|
71
122
|
static void notify(ln_client*c);
|
|
72
123
|
static void load_credential(ln_client*c);
|
|
73
124
|
static int prepare_credential(ln_client*c);
|
|
74
125
|
static int accept_credential(ln_client*c,const cJSON*result);
|
|
126
|
+
static int save_logout_state(ln_client*c);
|
|
127
|
+
static int logout_binding_blocked(ln_client*c,const cJSON*result);
|
|
75
128
|
static const char b64chars[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
76
129
|
static int b64(const char*s,unsigned char*out,size_t capacity){if(!s||!s[0])return -1;unsigned int acc=0,bits=0;size_t used=0;for(size_t i=0;s[i];i++){const char*p=strchr(b64chars,s[i]);if(!p||s[i]=='=')return -1;acc=(acc<<6)|(unsigned int)(p-b64chars);bits+=6;if(bits>=8){bits-=8;if(used>=capacity)return -1;out[used++]=(unsigned char)(acc>>bits);}}if(bits==6||(bits&&(acc&((1u<<bits)-1u))))return -1;return (int)used;}
|
|
77
130
|
static int64_t policy_span(const cJSON*c,int64_t issued){
|
|
@@ -95,11 +148,12 @@ static int app_version(const char*s){if(!s)return 0;const char*p=s;for(int i=0;i
|
|
|
95
148
|
static int advanced_claims(const cJSON*c){
|
|
96
149
|
const cJSON*request=get(c,"offline_request_id");if(request&&!uuid(text(c,"offline_request_id")))return 0;const cJSON*x=get(c,"capabilities");if(!x)return 1;if(!cJSON_IsObject(x))return 0;
|
|
97
150
|
const char*model=text(x,"license_model");if(!model||(strcmp(model,"node_locked")&&strcmp(model,"floating")))return 0;
|
|
98
|
-
const char*const allowed[]={"license_model","session_id","session_expires_at","app_version","version_released_at","maintenance_until"};for(const cJSON*v=x->child;v;v=v->next){int ok=0;for(int i=0;i<
|
|
151
|
+
const char*const allowed[]={"license_model","session_id","session_expires_at","app_version","version_released_at","maintenance_until","launch_connection_id","environment","binding_version"};for(const cJSON*v=x->child;v;v=v->next){int ok=0;for(int i=0;i<9;i++)if(!strcmp(v->string,allowed[i]))ok=1;if(!ok)return 0;}
|
|
99
152
|
int64_t session=0,released=0,maintenance=0,issued=0,expires=0;const char*const dates[]={"session_expires_at","version_released_at","maintenance_until"};int64_t*values[]={&session,&released,&maintenance};for(int i=0;i<3;i++)if(get(x,dates[i])&&(!num(x,dates[i],values[i])||*values[i]<0||*values[i]>253402300799LL))return 0;
|
|
100
153
|
if(get(x,"app_version")&&!app_version(text(x,"app_version")))return 0;
|
|
101
154
|
if(!num(c,"issued_at",&issued)||!num(c,"expires_at",&expires))return 0;
|
|
102
155
|
const cJSON*policy=get(c,"runtime_policy");int64_t heartbeat=0;if(!strcmp(model,"floating")&&(!uuid(text(x,"session_id"))||session<=issued||expires>session||!num(policy,"heartbeat_seconds",&heartbeat)||heartbeat==0||!strcmp(text(policy,"offline_mode")?text(policy,"offline_mode"):"","permanent")))return 0;
|
|
156
|
+
if(get(x,"launch_connection_id")||get(x,"environment")||get(x,"binding_version")){int64_t binding=0,h=0,o=0;const char*env=text(x,"environment"),*mode=text(policy,"offline_mode");if(!uuid(text(x,"launch_connection_id"))||!env||(strcmp(env,"live")&&strcmp(env,"sandbox"))||!num(x,"binding_version",&binding)||binding<1||strcmp(model,"node_locked")||!uuid(text(x,"session_id"))||session<=issued||expires>session||!num(policy,"heartbeat_seconds",&h)||h<600||!mode||!strcmp(mode,"permanent")||!num(policy,"offline_seconds",&o)||o>86400)return 0;}
|
|
103
157
|
if(released>issued||(maintenance>0&&(!text(x,"app_version")||released==0||released>maintenance)))return 0;
|
|
104
158
|
return 1;
|
|
105
159
|
}
|
|
@@ -196,9 +250,10 @@ static int origin(const cJSON*cfg){CURLU*u=curl_url();if(!u)return 0;int valid=0
|
|
|
196
250
|
valid=!strcmp(scheme,"https")||(!strcmp(scheme,"http")&&cJSON_IsTrue(get(cfg,"allow_http"))&&(!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")||!strcmp(host,"[::1]")));
|
|
197
251
|
done:curl_free(scheme);curl_free(host);curl_free(path);curl_url_cleanup(u);return valid;}
|
|
198
252
|
static cJSON*claims_from(const cJSON*envelope){unsigned char raw[LIMIT+1];int n=b64(text(envelope,"payload"),raw,LIMIT);if(n<0)return NULL;raw[n]=0;return ln_json((char*)raw);}
|
|
199
|
-
static int client_context(ln_client*c,const cJSON*envelope){cJSON*claims=claims_from(envelope);const cJSON*x=get(claims,"capabilities");int ok=1;if(x){const char*version=text(x,"app_version"),*expected=text(c->cfg,"app_version"),*model=text(x,"license_model");if(strcmp(version?version:"",expected?expected:""))ok=0;if(model&&!strcmp(model,"floating")&&(!text(x,"session_id")||strcmp(text(x,"session_id"),c->session)))ok=0;}cJSON_Delete(claims);return ok;}
|
|
253
|
+
static int client_context(ln_client*c,const cJSON*envelope){cJSON*claims=claims_from(envelope);const cJSON*x=get(claims,"capabilities");int ok=1;const char*connection=text(c->cfg,"launch_connection_id"),*environment=text(c->cfg,"environment");if(!x&&connection&&connection[0])ok=0;if(x){const char*signed_connection=text(x,"launch_connection_id"),*signed_environment=text(x,"environment");if(strcmp(signed_connection?signed_connection:"",connection?connection:"")||strcmp(signed_environment?signed_environment:"",environment?environment:""))ok=0;const char*version=text(x,"app_version"),*expected=text(c->cfg,"app_version"),*model=text(x,"license_model");if(strcmp(version?version:"",expected?expected:""))ok=0;if(model&&!strcmp(model,"floating")&&(!text(x,"session_id")||strcmp(text(x,"session_id"),c->session)))ok=0;}cJSON_Delete(claims);return ok;}
|
|
200
254
|
static int floating(ln_client*c){cJSON*claims=claims_from(c->cached);const char*model=text(get(claims,"capabilities"),"license_model");int result=model&&!strcmp(model,"floating");cJSON_Delete(claims);return result;}
|
|
201
|
-
static int
|
|
255
|
+
static int source_hash(ln_client*c,char out[65]){const char*source=source_input(c);if(source&&source[0])return hash(source,out);const char*h=text(c->auth,"source_hash");if(!device(h))return 0;strcpy(out,h);return 1;}
|
|
256
|
+
static int cache_valid(ln_client*c){return !is_denied(c)&&c->cached&&client_context(c,c->cached)&&(!c->wall||time(NULL)>=c->wall+(monotonic()-c->tick)-300)&&ln_verify(c->cached,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));}
|
|
202
257
|
static void view_lock(ln_client*c){
|
|
203
258
|
#ifdef _WIN32
|
|
204
259
|
EnterCriticalSection(&c->view_mutex);
|
|
@@ -214,7 +269,7 @@ static void view_unlock(ln_client*c){
|
|
|
214
269
|
#endif
|
|
215
270
|
}
|
|
216
271
|
static void publish(ln_client*c,int online){view_lock(c);cJSON_Delete(c->view);c->view=cJSON_Duplicate(c->cached,1);c->view_wall=c->wall;c->view_tick=c->tick;c->view_online=online;snprintf(c->view_error.code,sizeof(c->view_error.code),"%s",c->error);snprintf(c->view_error.request_id,sizeof(c->view_error.request_id),"%s",c->request_id);c->view_error.http_status=(int)c->http_status;c->view_error.retryable=c->retryable;c->view_error.retry_after_seconds=c->retry_after;view_unlock(c);}
|
|
217
|
-
static cJSON*local_claims(ln_client*c){view_lock(c);cJSON*envelope=cJSON_Duplicate(c->view,1);int64_t wall=c->view_wall;double tick=c->view_tick;view_unlock(c);int valid=envelope&&client_context(c,envelope)&&(!wall||time(NULL)>=wall+monotonic()-tick-300)&&ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));cJSON*claims=valid?claims_from(envelope):NULL;cJSON_Delete(envelope);return claims;}
|
|
272
|
+
static cJSON*local_claims(ln_client*c){if(is_denied(c))return NULL;view_lock(c);cJSON*envelope=cJSON_Duplicate(c->view,1);int64_t wall=c->view_wall;double tick=c->view_tick;view_unlock(c);int valid=envelope&&client_context(c,envelope)&&(!wall||time(NULL)>=wall+monotonic()-tick-300)&&ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));cJSON*claims=valid?claims_from(envelope):NULL;cJSON_Delete(envelope);return claims;}
|
|
218
273
|
int ln_get_error_info(ln_client*c,ln_error_info*out){if(!c||!out)return 0;view_lock(c);*out=c->view_error;view_unlock(c);return 1;}
|
|
219
274
|
char*ln_status(ln_client*c){if(!c)return NULL;cJSON*claims=local_claims(c),*status=cJSON_CreateObject();view_lock(c);ln_error_info e=c->view_error;int online=c->view_online;view_unlock(c);const char*state=claims?(online?"active":"offline_valid"):"verification_required";if(!strcmp(e.code,"LICENSE_EXPIRED"))state="expired";if(!strcmp(e.code,"LICENSE_REVOKED")||!strcmp(e.code,"LICENSE_SUSPENDED"))state="revoked";if(!strcmp(e.code,"DEVICE_RELEASED")||!strcmp(e.code,"DEVICE_CREDENTIAL_REVOKED"))state="released";if(!strcmp(e.code,"FEATURE_QUOTA_EXCEEDED"))state="quota_exhausted";int64_t expires=0;num(claims,"expires_at",&expires);cJSON_AddStringToObject(status,"state",state);cJSON_AddBoolToObject(status,"allowed",claims!=NULL);cJSON_AddStringToObject(status,"code",e.code);cJSON_AddStringToObject(status,"request_id",e.request_id);cJSON_AddBoolToObject(status,"retryable",e.retryable);cJSON_AddNumberToObject(status,"lease_valid_until",(double)expires);char*out=cJSON_PrintUnformatted(status);cJSON_Delete(status);cJSON_Delete(claims);return out;}
|
|
220
275
|
char*ln_feature_status(ln_client*c,const char*feature){
|
|
@@ -242,9 +297,9 @@ ln_client*ln_init(const char*config){if(!ensure_curl())return NULL;cJSON*cfg=ln_
|
|
|
242
297
|
#ifdef _WIN32
|
|
243
298
|
InitializeCriticalSection(&c->mutex);InitializeCriticalSection(&c->view_mutex);
|
|
244
299
|
#else
|
|
245
|
-
pthread_mutex_init(&c->mutex,NULL);pthread_mutex_init(&c->view_mutex,NULL);atomic_init(&c->stop,0);
|
|
300
|
+
pthread_mutex_init(&c->mutex,NULL);pthread_mutex_init(&c->view_mutex,NULL);atomic_init(&c->stop,0);atomic_init(&c->denied,0);
|
|
246
301
|
#endif
|
|
247
|
-
load_credential(c);if(!
|
|
302
|
+
load_credential(c);load_logout_state(c);if(!source_input(c)[0]&&!c->auth&&!is_denied(c)){ln_destroy(c);return NULL;}const char*path=text(cfg,"cache_path");char*b=!is_denied(c)&&path&&path[0]?readfile(path,LIMIT):NULL;if(b){cJSON*s=ln_json(b);int64_t version,observed;char h[65];int hash_ok=source_hash(c,h);if(hash_ok&&s&&num(s,"version",&version)&&version==1&&text(s,"license_hash")&&!strcmp(text(s,"license_hash"),h)&&num(s,"observed_at",&observed)&&time(NULL)+300>=observed&&ln_verify(get(s,"lease"),keys,text(cfg,"product_id"),text(cfg,"device_id"),time(NULL))){int interval=3600,mode=envelope_policy(get(s,"lease"),&interval);if(mode!=1&&client_context(c,get(s,"lease"))){c->cached=cJSON_Duplicate(get(s,"lease"),1);c->wall=observed;c->interval=interval;c->offline=mode;}}cJSON_Delete(s);free(b);}publish(c,0);if(has_pending_logouts(c))ln_start_heartbeat(c);return c;}
|
|
248
303
|
ln_client*ln_init_file(const char*p){char*b=readfile(p,LIMIT);if(!b)return NULL;ln_client*c=ln_init(b);free(b);return c;}
|
|
249
304
|
ln_client*ln_init_product(const char*product_id,const char*trusted_keys_json,const char*license_key){
|
|
250
305
|
if(!product_id||!trusted_keys_json||!license_key)return NULL;
|
|
@@ -272,38 +327,48 @@ ln_client*ln_init_file_with_license(const char*path,const char*license_key){
|
|
|
272
327
|
ln_client*c=ln_init(b);free(b);return c;
|
|
273
328
|
}
|
|
274
329
|
const char*ln_last_error(const ln_client*c){return c?c->error:"INVALID_CONFIGURATION";}
|
|
275
|
-
static void invalidate(ln_client*c){cJSON_Delete(c->cached);c->cached=NULL;c->offline=0;c->interval=3600;publish(c,0);const char*p=text(c->cfg,"cache_path");if(p&&p[0])remove(p);}
|
|
330
|
+
static void invalidate(ln_client*c){cJSON_Delete(c->cached);c->cached=NULL;c->offline=0;c->interval=3600;publish(c,0);const char*p=text(c->cfg,"cache_path");if(p&&p[0]&&!is_denied(c))remove(p);}
|
|
276
331
|
static void mkdirs(const char*p){char*copy=malloc(strlen(p)+1);if(!copy)return;strcpy(copy,p);for(char*s=copy+1;*s;s++)if(*s=='/'||*s=='\\'){char old=*s;*s=0;mkdir_private(copy);*s=old;}free(copy);}
|
|
277
332
|
static int write_state(const char*p,const cJSON*s){
|
|
278
|
-
char*b=cJSON_PrintUnformatted(s);char*tmp=malloc(strlen(p)+40);if(!tmp||!b){free(tmp);free(b);return 0;}unsigned char random[8];if(!random_bytes(random,8)){free(tmp);free(b);return 0;}
|
|
333
|
+
char*b=cJSON_PrintUnformatted(s);char*tmp=malloc(strlen(p)+40);if(!tmp||!b){free(tmp);free(b);return 0;}unsigned char random[8];if(!random_bytes(random,8)){free(tmp);free(b);return 0;}
|
|
334
|
+
/* Keep temporary filenames short even when a task journal has a long name. */
|
|
335
|
+
const char*slash=strrchr(p,'/'),*backslash=strrchr(p,'\\');if(backslash&&(!slash||backslash>slash))slash=backslash;size_t prefix=slash?(size_t)(slash-p)+1:0;memcpy(tmp,p,prefix);sprintf(tmp+prefix,".state-%02x%02x%02x%02x%02x%02x%02x%02x.tmp",random[0],random[1],random[2],random[3],random[4],random[5],random[6],random[7]);mkdirs(p);FILE*f=fopen(tmp,"wb");int ok=0;if(f){
|
|
279
336
|
#ifndef _WIN32
|
|
280
337
|
fchmod(fileno(f),0600);
|
|
281
338
|
#endif
|
|
282
|
-
ok=fwrite(b,1,strlen(b),f)==strlen(b);if(
|
|
339
|
+
ok=fwrite(b,1,strlen(b),f)==strlen(b);if(fflush(f))ok=0;
|
|
340
|
+
#ifdef _WIN32
|
|
341
|
+
if(ok&&_commit(_fileno(f)))ok=0;
|
|
342
|
+
#else
|
|
343
|
+
if(ok&&fsync(fileno(f)))ok=0;
|
|
344
|
+
#endif
|
|
345
|
+
if(fclose(f))ok=0;
|
|
283
346
|
#ifdef _WIN32
|
|
284
|
-
if(ok)ok=
|
|
347
|
+
if(ok)ok=state_windows_move(tmp,p);
|
|
285
348
|
#else
|
|
286
349
|
if(ok)ok=rename(tmp,p)==0;
|
|
287
350
|
#endif
|
|
288
351
|
}remove(tmp);free(tmp);free(b);return ok;
|
|
289
352
|
}
|
|
290
|
-
static int save(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return 1;char h[65];if(!
|
|
353
|
+
static int save(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return 1;char h[65];if(!source_hash(c,h))return 0;cJSON*s=cJSON_CreateObject();cJSON_AddNumberToObject(s,"version",1);cJSON_AddStringToObject(s,"license_hash",h);cJSON_AddItemToObject(s,"lease",cJSON_Duplicate(c->cached,1));cJSON_AddNumberToObject(s,"observed_at",(double)c->wall);int ok=write_state(p,s);cJSON_Delete(s);return ok;}
|
|
291
354
|
static char*credential_path(ln_client*c){const char*p=text(c->cfg,"cache_path");if(!p||!p[0])return NULL;char*out=malloc(strlen(p)+18);if(out)sprintf(out,"%s.credential.json",p);return out;}
|
|
292
355
|
static void load_credential(ln_client*c){
|
|
293
356
|
|
|
294
|
-
char*p=credential_path(c);if(!p)return;char*b=readfile(p,LIMIT);free(p);if(!b)return;cJSON*s=ln_json(b);free(b);char h[65];int64_t version;int ok=hash(
|
|
357
|
+
char*p=credential_path(c);if(!p)return;char*b=readfile(p,LIMIT);free(p);if(!b)return;cJSON*s=ln_json(b);free(b);char h[65];int64_t version;int ok=hash(source_input(c),h)&&s&&num(s,"version",&version)&&version==1&&text(s,"origin")&&!strcmp(text(s,"origin"),text(c->cfg,"base_url"))&&device(text(s,"source_hash"))&&(!source_input(c)[0]||!strcmp(text(s,"source_hash"),h))&&uuid(text(s,"request_id"))&&uuid(text(s,"session_id"));
|
|
295
358
|
const char*ctx[]={"product_id","device_id"};for(int i=0;i<2;i++){const char*a=text(s,ctx[i]),*b=text(c->cfg,ctx[i]);if(!a||!b||strcmp(a,b))ok=0;}const char*k=text(s,"device_key");if(!k||(k[0]&&!credential_secret(k,"lv_dev_"))||(!text(c->cfg,"license_key")[0]&&!k[0]))ok=0;
|
|
296
359
|
if(ok){c->auth=s;if(!k[0])strcpy(c->session,text(s,"session_id"));}else cJSON_Delete(s);
|
|
297
360
|
}
|
|
298
361
|
static int save_credential(ln_client*c){char*p=credential_path(c);if(!p){strcpy(c->error,"CREDENTIAL_PATH_REQUIRED");return 0;}int ok=write_state(p,c->auth);free(p);if(ok)c->auth_dirty=0;else strcpy(c->error,"CREDENTIAL_WRITE_FAILED");return ok;}
|
|
299
362
|
static int prepare_credential(ln_client*c){
|
|
300
|
-
if(!temporary(c))return 1;
|
|
301
|
-
if(!c->auth){char h[65],id[37];if(!
|
|
363
|
+
if(is_denied(c)){strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");return 0;}if(!temporary(c))return 1;
|
|
364
|
+
if(!c->auth){char h[65],id[37];if(!source_hash(c,h)||!request_uuid(id))return 0;c->auth=cJSON_CreateObject();cJSON_AddNumberToObject(c->auth,"version",1);cJSON_AddStringToObject(c->auth,"origin",text(c->cfg,"base_url"));cJSON_AddStringToObject(c->auth,"source_hash",h);cJSON_AddStringToObject(c->auth,"product_id",text(c->cfg,"product_id"));cJSON_AddStringToObject(c->auth,"device_id",text(c->cfg,"device_id"));cJSON_AddStringToObject(c->auth,"device_key","");cJSON_AddStringToObject(c->auth,"request_id",id);cJSON_AddStringToObject(c->auth,"session_id",c->session);c->auth_dirty=1;}
|
|
302
365
|
return !c->auth_dirty||save_credential(c);
|
|
303
366
|
}
|
|
304
367
|
static int accept_credential(ln_client*c,const cJSON*result){
|
|
305
|
-
if(
|
|
306
|
-
|
|
368
|
+
if(logout_binding_blocked(c,result))return 0;
|
|
369
|
+
if(cJSON_IsTrue(get(c->logout_state,"signed_out"))){cJSON_ReplaceItemInObjectCaseSensitive(c->logout_state,"signed_out",cJSON_CreateBool(0));if(!save_logout_state(c))return 0;}
|
|
370
|
+
if(get(result,"logout_ticket")){if(!prepare_credential(c))return 0;cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(get(result,"logout_ticket"),1));c->auth_dirty=1;if(!save_credential(c))return 0;}if(!temporary(c)||(text(c->auth,"device_key")&&text(c->auth,"device_key")[0]))return 1;
|
|
371
|
+
const char*k=text(result,"device_key");if(!credential_secret(k,"lv_dev_")){if(strncmp(text(c->cfg,"license_key"),"lv_tmp_",7)&&!(text(c->cfg,"identity_token")&&text(c->cfg,"identity_token")[0]))return 1;strcpy(c->error,"DEVICE_CREDENTIAL_MISSING");return 0;}if(!prepare_credential(c))return 0;cJSON_ReplaceItemInObjectCaseSensitive(c->auth,"device_key",cJSON_CreateString(k));c->auth_dirty=1;return save_credential(c);
|
|
307
372
|
}
|
|
308
373
|
struct buffer{char data[LIMIT+1];size_t size;int oversized;};
|
|
309
374
|
static size_t receive(void*data,size_t size,size_t n,void*arg){struct buffer*b=arg;size_t amount=size*n;if(amount>LIMIT-b->size){b->oversized=1;return 0;}memcpy(b->data+b->size,data,amount);b->size+=amount;b->data[b->size]=0;return amount;}
|
|
@@ -315,19 +380,20 @@ static void sleepms(int ms){
|
|
|
315
380
|
#endif
|
|
316
381
|
}
|
|
317
382
|
static size_t receive_header(char*data,size_t size,size_t n,void*arg){size_t length=size*n;ln_client*c=arg;if(length>12&&!strncmp(data,"Retry-After:",12)){char value[32];size_t count=length-12;if(count>31)count=31;memcpy(value,data+12,count);value[count]=0;long seconds=strtol(value,NULL,10);if(seconds>0)c->retry_after=seconds>86400?86400:(int)seconds;}return length;}
|
|
318
|
-
static cJSON*request_extra(ln_client*c,const char*op,const char*activation,int*transient,const char*operation_id,const cJSON*extra){*transient=0;c->error[0]=0;c->request_id[0]=0;c->retryable=0;c->http_status=0;c->retry_after=0;if(!prepare_credential(c))return NULL;if(!strcmp(op,"activate")&&temporary(c)&&!text(c->auth,"device_key")[0]&&!c->cached)operation_id=text(c->auth,"request_id");unsigned char random[16];if(!random_bytes(random,16)){strcpy(c->error,"RANDOM_SOURCE_FAILED");return NULL;}char idem[120]="Idempotency-Key: ";size_t used=strlen(idem);for(int i=0;i<16;i++)sprintf(idem+used+i*2,"%02x",random[i]);if(operation_id){if(strlen(operation_id)>80||strpbrk(operation_id," \t\r\n")){strcpy(c->error,"INVALID_OPERATION_ID");return NULL;}snprintf(idem,sizeof(idem),"Idempotency-Key: %s",operation_id);}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*s=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],s?s:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation);cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));if(extra)for(const cJSON*x=extra->child;x;x=x->next)cJSON_AddItemToObject(body,x->string,cJSON_Duplicate(x,1));char*json=cJSON_PrintUnformatted(body);cJSON_Delete(body);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+strlen(op)+20);if(!url||!json){free(url);free(json);return NULL;}strcpy(url,base);size_t len=strlen(url);if(len&&url[len-1]=='/')url[len-1]=0;strcat(url,"/api/v1/");strcat(url,op);struct curl_slist*headers=NULL;headers=curl_slist_append(headers,"Content-Type: application/json");headers=curl_slist_append(headers,idem);cJSON*result=NULL;
|
|
383
|
+
static cJSON*request_extra(ln_client*c,const char*op,const char*activation,int*transient,const char*operation_id,const cJSON*extra){*transient=0;c->error[0]=0;c->request_id[0]=0;c->retryable=0;c->http_status=0;c->retry_after=0;if(!prepare_credential(c))return NULL;if(!strcmp(op,"activate")&&temporary(c)&&!text(c->auth,"device_key")[0]&&!c->cached)operation_id=text(c->auth,"request_id");unsigned char random[16];if(!random_bytes(random,16)){strcpy(c->error,"RANDOM_SOURCE_FAILED");return NULL;}char idem[120]="Idempotency-Key: ";size_t used=strlen(idem);for(int i=0;i<16;i++)sprintf(idem+used+i*2,"%02x",random[i]);if(operation_id){if(strlen(operation_id)>80||strpbrk(operation_id," \t\r\n")){strcpy(c->error,"INVALID_OPERATION_ID");return NULL;}snprintf(idem,sizeof(idem),"Idempotency-Key: %s",operation_id);}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*s=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],s?s:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation);cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));if(extra)for(const cJSON*x=extra->child;x;x=x->next)cJSON_AddItemToObject(body,x->string,cJSON_Duplicate(x,1));if(!get(c->auth,"logout_ticket"))cJSON_AddBoolToObject(body,"request_logout_ticket",1);if(text(c->cfg,"launch_connection_id")){cJSON_AddStringToObject(body,"launch_connection_id",text(c->cfg,"launch_connection_id"));cJSON_AddStringToObject(body,"environment",text(c->cfg,"environment")?text(c->cfg,"environment"):"");}if(!runtime_key(c)[0]&&text(c->cfg,"identity_token")){const char*identity_names[]={"identity_token","launch_connection_id","environment"};for(int i=0;i<3;i++){const char*v=text(c->cfg,identity_names[i]);cJSON_DeleteItemFromObjectCaseSensitive(body,identity_names[i]);cJSON_AddStringToObject(body,identity_names[i],v?v:"");}op="launch/exchange";}char*json=cJSON_PrintUnformatted(body);cJSON_Delete(body);const char*base=text(c->cfg,"base_url");char*url=malloc(strlen(base)+strlen(op)+20);if(!url||!json){free(url);free(json);return NULL;}strcpy(url,base);size_t len=strlen(url);if(len&&url[len-1]=='/')url[len-1]=0;strcat(url,"/api/v1/");strcat(url,op);struct curl_slist*headers=NULL;headers=curl_slist_append(headers,"Content-Type: application/json");headers=curl_slist_append(headers,idem);cJSON*result=NULL;
|
|
319
384
|
for(int attempt=0;attempt<2;attempt++){if(attempt)sleepms(200);struct buffer b={{0},0,0};CURL*curl=curl_easy_init();if(!curl)break;curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,json);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&b);int64_t timeout=10;num(c->cfg,"timeout_seconds",&timeout);curl_easy_setopt(curl,CURLOPT_TIMEOUT,(long)timeout);const char*proxy=text(c->cfg,"proxy_url");if(proxy&&proxy[0])curl_easy_setopt(curl,CURLOPT_PROXY,proxy);curl_easy_setopt(curl,CURLOPT_HEADERFUNCTION,receive_header);curl_easy_setopt(curl,CURLOPT_HEADERDATA,c);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L);curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1L);curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);const char*ca=getenv("LICENTIVO_CA_BUNDLE");if(ca&&ca[0])curl_easy_setopt(curl,CURLOPT_CAINFO,ca);CURLcode err=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);if(b.oversized){strcpy(c->error,"RESPONSE_TOO_LARGE");*transient=0;break;}c->http_status=status;if(err!=CURLE_OK){*transient=1;c->retryable=1;strcpy(c->error,"SERVER_UNAVAILABLE");continue;}*transient=0;if(memchr(b.data,0,b.size)){strcpy(c->error,"INVALID_JSON");break;}cJSON*env=ln_json(b.data);if(!env){strcpy(c->error,"UNEXPECTED_RESPONSE");c->retryable=status==429||status>=500;*transient=c->retryable;if(c->retry_after>0)c->retry_at=(int64_t)time(NULL)+c->retry_after;if(status>=500)continue;break;}const char*trace=text(env,"request_id");snprintf(c->request_id,sizeof(c->request_id),"%s",trace?trace:"");if(status!=200||get(env,"error")){const char*code=text(get(env,"error"),"code");snprintf(c->error,sizeof(c->error),"%s",code?code:"UNEXPECTED_RESPONSE");c->retryable=status==429||status>=500;*transient=c->retryable;if(c->retry_after>0)c->retry_at=(int64_t)time(NULL)+c->retry_after;cJSON_Delete(env);if(status>=500)continue;break;}result=cJSON_Duplicate(get(env,"data"),1);cJSON_Delete(env);break;}
|
|
320
|
-
curl_slist_free_all(headers);free(url);free(json);return result;
|
|
385
|
+
curl_slist_free_all(headers);free(url);free(json);if(!result&&revocation_error(c->error))deny_runtime(c);return result;
|
|
321
386
|
}
|
|
322
387
|
static cJSON*request(ln_client*c,const char*op,const char*activation,int*transient){return request_extra(c,op,activation,transient,NULL,NULL);}
|
|
388
|
+
#include "device_logout.h"
|
|
323
389
|
static char*activation_id(ln_client*c){if(!c->cached)return NULL;unsigned char raw[LIMIT+1];int n=b64(text(c->cached,"payload"),raw,LIMIT);if(n<0)return NULL;raw[n]=0;cJSON*claims=ln_json((char*)raw);const char*s=text(claims,"activation_id");char*out=s?malloc(strlen(s)+1):NULL;if(out)strcpy(out,s);cJSON_Delete(claims);return out;}
|
|
324
|
-
static int call(ln_client*c,const char*op,int force_online){if(!c)return 0;lock(c);if(!force_online&&!strcmp(op,"activate")&&c->offline==2&&c->interval==0&&cache_valid(c)){unlock(c);return 1;}char*id=NULL;if(strcmp(op,"activate")){id=activation_id(c);if(!id){strcpy(c->error,"ACTIVATE_FIRST");unlock(c);return 0;}}int temporary=0;cJSON*data=request(c,op,id?id:"",&temporary);free(id);if(!data){int ok=!force_online&&temporary&&c->offline!=1&&cache_valid(c);if(!temporary||c->offline==1)invalidate(c);publish(c,0);unlock(c);notify(c);return ok;}
|
|
325
|
-
const cJSON*e=get(data,"lease");int ok=client_context(c,e)&&ln_verify(e,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(e,1);char*activation=activation_id(c);const char*expected=text(data,"activation_id");ok=activation&&expected&&!strcmp(activation,expected);free(activation);}if(ok){c->wall=time(NULL);c->tick=monotonic();int64_t interval;if(num(data,"heartbeat_seconds",&interval)&&interval>=10&&interval<=86400)c->interval=(int)interval;c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,data)&&save(c);if(ok)c->error[0]=0;else if(c->error[0]==0)strcpy(c->error,"CACHE_WRITE_FAILED");}else{strcpy(c->error,"INVALID_LEASE");invalidate(c);}cJSON_Delete(data);publish(c,ok);unlock(c);notify(c);return ok;}
|
|
390
|
+
static int call(ln_client*c,const char*op,int force_online){if(!c)return 0;lock(c);if(is_denied(c)){strcpy(c->error,"LAUNCH_LOGIN_REQUIRED");unlock(c);return 0;}if(!force_online&&!strcmp(op,"activate")&&c->offline==2&&c->interval==0&&cache_valid(c)){unlock(c);return 1;}char*id=NULL;if(strcmp(op,"activate")){id=activation_id(c);if(!id){strcpy(c->error,"ACTIVATE_FIRST");unlock(c);return 0;}}int temporary=0;cJSON*data=request(c,op,id?id:"",&temporary);free(id);if(!data){int ok=!force_online&&temporary&&c->offline!=1&&cache_valid(c);if(!temporary||c->offline==1)invalidate(c);publish(c,0);unlock(c);notify(c);return ok;}
|
|
391
|
+
if(is_denied(c)){if(c->auth&&get(data,"logout_ticket")){cJSON_DeleteItemFromObjectCaseSensitive(c->auth,"logout_ticket");cJSON_AddItemToObject(c->auth,"logout_ticket",cJSON_Duplicate(get(data,"logout_ticket"),1));}cJSON_Delete(data);unlock(c);return 0;}const cJSON*e=get(data,"lease");int ok=client_context(c,e)&&ln_verify(e,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL));if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(e,1);char*activation=activation_id(c);const char*expected=text(data,"activation_id");ok=activation&&expected&&!strcmp(activation,expected);free(activation);}if(ok){c->wall=time(NULL);c->tick=monotonic();int64_t interval;if(num(data,"heartbeat_seconds",&interval)&&interval>=10&&interval<=86400)c->interval=(int)interval;c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,data)&&save(c);if(ok)c->error[0]=0;else if(c->error[0]==0)strcpy(c->error,"CACHE_WRITE_FAILED");}else{strcpy(c->error,"INVALID_LEASE");invalidate(c);}cJSON_Delete(data);publish(c,ok);unlock(c);notify(c);return ok;}
|
|
326
392
|
int ln_activate(ln_client*c){return call(c,"activate",0);}int ln_validate(ln_client*c){return call(c,"validate",0);}int ln_heartbeat(ln_client*c){return call(c,"heartbeat",0);}
|
|
327
393
|
int ln_check_feature(ln_client*c,const char*feature){if(!c||!feature)return 0;cJSON*claims=local_claims(c);int ok=0;const cJSON*fs=get(claims,"features");for(const cJSON*f=fs?fs->child:NULL;f;f=f->next)if(cJSON_IsString(f)&&!strcmp(f->valuestring,feature))ok=1;cJSON_Delete(claims);return ok;}
|
|
328
394
|
int ln_run_feature(ln_client*c,const char*feature,ln_feature_action action,void*context){if(!action||!ln_check_feature(c,feature))return 0;return action(context)!=0;}
|
|
329
395
|
int ln_refresh_online(ln_client*c){return call(c,"activate",1);}
|
|
330
|
-
int ln_deactivate(ln_client*c){
|
|
396
|
+
int ln_deactivate(ln_client*c){return ln_logout(c);}
|
|
331
397
|
static int stopped(ln_client*c){
|
|
332
398
|
#ifdef _WIN32
|
|
333
399
|
return InterlockedCompareExchange(&c->stop,0,0)!=0;
|
|
@@ -340,7 +406,7 @@ static DWORD WINAPI heartbeat_run(LPVOID arg){
|
|
|
340
406
|
#else
|
|
341
407
|
static void*heartbeat_run(void*arg){
|
|
342
408
|
#endif
|
|
343
|
-
ln_client*c=arg;lock(c);int refresh=c->refresh_pending;c->refresh_pending=0;unlock(c);if(refresh&&!stopped(c))ln_refresh_online(c);while(!stopped(c)){unsigned char r=0;random_bytes(&r,1);lock(c);double wait=(c->interval<600?600:c->interval)*(1+r/255.0*.1);double retry=(double)c->retry_at-time(NULL);if(retry>wait)wait=retry;int disabled=c->interval==0;unlock(c);if(disabled)break;double end=monotonic()+wait;while(!stopped(c)&&monotonic()<end)sleepms(100);if(!stopped(c))ln_heartbeat(c);}lock(c);c->finished=1;unlock(c);return 0;}
|
|
409
|
+
ln_client*c=arg;lock(c);int refresh=c->refresh_pending;c->refresh_pending=0;unlock(c);if(refresh&&!stopped(c))ln_refresh_online(c);while(!stopped(c)){unsigned char r=0;random_bytes(&r,1);lock(c);double wait=has_pending_logouts(c)?2:(c->interval<600?600:c->interval)*(1+r/255.0*.1);double retry=(double)c->retry_at-time(NULL);if(retry>wait)wait=retry;int disabled=(c->interval==0||is_denied(c))&&!has_pending_logouts(c);unlock(c);if(disabled)break;double end=monotonic()+wait;while(!stopped(c)&&monotonic()<end)sleepms(100);if(!stopped(c)){if(is_denied(c)||ln_logout_pending(c))ln_retry_pending_logouts(c);if(!is_denied(c))ln_heartbeat(c);}}lock(c);c->finished=1;unlock(c);return 0;}
|
|
344
410
|
int ln_start_heartbeat(ln_client*c){if(!c)return 0;if(c->started){lock(c);int finished=c->finished;unlock(c);if(!finished)return 1;ln_stop_heartbeat(c);}lock(c);int disabled=c->interval==0;c->finished=0;unlock(c);if(disabled&&!c->refresh_pending)return 1;
|
|
345
411
|
#ifdef _WIN32
|
|
346
412
|
InterlockedExchange(&c->stop,0);c->thread=CreateThread(NULL,0,heartbeat_run,c,0,NULL);if(!c->thread)return 0;
|
|
@@ -355,7 +421,7 @@ void ln_stop_heartbeat(ln_client*c){if(!c||!c->started)return;
|
|
|
355
421
|
atomic_store(&c->stop,1);pthread_join(c->thread,NULL);
|
|
356
422
|
#endif
|
|
357
423
|
c->started=0;}
|
|
358
|
-
void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c))
|
|
424
|
+
void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c)&&!is_denied(c)){char*id=activation_id(c);if(id){int transient;cJSON*result=request(c,"deactivate",id,&transient);cJSON_Delete(result);free(id);}}cJSON_Delete(c->logout_bindings);cJSON_Delete(c->logout_state);cJSON_Delete(c->cfg);cJSON_Delete(c->cached);cJSON_Delete(c->auth);cJSON_Delete(c->view);
|
|
359
425
|
#ifdef _WIN32
|
|
360
426
|
DeleteCriticalSection(&c->mutex);DeleteCriticalSection(&c->view_mutex);
|
|
361
427
|
#else
|
|
@@ -363,13 +429,13 @@ void ln_destroy(ln_client*c){if(!c)return;ln_stop_heartbeat(c);if(floating(c))ln
|
|
|
363
429
|
#endif
|
|
364
430
|
free(c);}
|
|
365
431
|
|
|
366
|
-
int ln_start(ln_client*c){if(!c)return 0;lock(c);int fast=cache_valid(c)&&c->offline!=1&&!floating(c);if(fast)c->refresh_pending=1;unlock(c);return (fast||ln_activate(c))&&ln_start_heartbeat(c);}
|
|
432
|
+
int ln_start(ln_client*c){if(!c)return 0;if(is_denied(c)&&ln_logout_pending(c))ln_retry_pending_logouts(c);lock(c);int fast=cache_valid(c)&&c->offline!=1&&!floating(c);if(fast)c->refresh_pending=1;unlock(c);return (fast||ln_activate(c))&&ln_start_heartbeat(c);}
|
|
367
433
|
ln_client*ln_open(const char*path){ln_client*c=ln_init_file(path);if(c&&!ln_start(c)){ln_destroy(c);return NULL;}return c;}
|
|
368
434
|
ln_client*ln_open_with_license(const char*path,const char*license_key){ln_client*c=ln_init_file_with_license(path,license_key);if(c&&!ln_start(c)){ln_destroy(c);return NULL;}return c;}
|
|
369
435
|
void ln_free_string(char*s){free(s);}
|
|
370
436
|
char*ln_consume(ln_client*c,const char*feature,int64_t quantity,const char*operation_id){if(!c||!feature||!operation_id||!operation_id[0])return NULL;lock(c);if(!cache_valid(c)){strcpy(c->error,"LICENSE_CHECK_REQUIRED");unlock(c);return NULL;}char*activation=activation_id(c);cJSON*extra=cJSON_CreateObject();cJSON_AddStringToObject(extra,"feature",feature);cJSON_AddNumberToObject(extra,"quantity",(double)quantity);int transient=0;cJSON*result=request_extra(c,"consume",activation,&transient,operation_id,extra);char*out=result?cJSON_PrintUnformatted(get(result,"consumption")):NULL;cJSON_Delete(extra);free(activation);if(!result&&(!strcmp(c->error,"LICENSE_REVOKED")||!strcmp(c->error,"LICENSE_SUSPENDED")||!strcmp(c->error,"DEVICE_RELEASED")||!strcmp(c->error,"DEVICE_CREDENTIAL_REVOKED")||!strcmp(c->error,"LICENSE_EXPIRED")))invalidate(c);int success=result!=NULL;cJSON_Delete(result);publish(c,success);unlock(c);notify(c);return out;}
|
|
371
|
-
char*ln_offline_request(ln_client*c,const char*operation){if(!c||!operation||(strcmp(operation,"activate")&&strcmp(operation,"deactivate")))return NULL;lock(c);if(!prepare_credential(c)){unlock(c);return NULL;}char*activation=NULL;if(!strcmp(operation,"deactivate")){if(!cache_valid(c)){unlock(c);return NULL;}activation=activation_id(c);invalidate(c);}char id[37];if(!request_uuid(id)){free(activation);unlock(c);return NULL;}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*v=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],v?v:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation?activation:"");cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));cJSON_AddNumberToObject(body,"version",1);cJSON_AddStringToObject(body,"request_id",id);cJSON_AddNumberToObject(body,"created_at",(double)time(NULL));cJSON_AddStringToObject(body,"operation",operation);char*out=cJSON_PrintUnformatted(body);cJSON_Delete(body);free(activation);unlock(c);return out;}
|
|
372
|
-
int ln_import_offline(ln_client*c,const char*request_json,const char*response_json){if(!c||!request_json||!response_json||strlen(request_json)>LIMIT||strlen(response_json)>LIMIT)return 0;lock(c);cJSON*req=ln_json(request_json),*res=ln_json(response_json);int64_t version=0;int ok=req&&res&&num(res,"version",&version)&&version==1;const char*fields[]={"request_id","operation"};for(int i=0;i<2;i++){const char*a=text(req,fields[i]),*b=text(res,fields[i]);if(!a||!b||strcmp(a,b))ok=0;}if(!text(req,"operation")||strcmp(text(req,"operation"),"activate"))ok=0;const char*context[]={"product_id","device_id","license_key"};for(int i=0;i<3;i++){const char*a=text(req,context[i]),*b=!strcmp(context[i],"license_key")?runtime_key(c):text(c->cfg,context[i]);if(!a||!b||strcmp(a,b))ok=0;}
|
|
437
|
+
char*ln_offline_request(ln_client*c,const char*operation){if(!c||is_denied(c)||!operation||(strcmp(operation,"activate")&&strcmp(operation,"deactivate")))return NULL;lock(c);if(!prepare_credential(c)){unlock(c);return NULL;}char*activation=NULL;if(!strcmp(operation,"deactivate")){if(!cache_valid(c)){unlock(c);return NULL;}activation=activation_id(c);invalidate(c);}char id[37];if(!request_uuid(id)){free(activation);unlock(c);return NULL;}cJSON*body=cJSON_CreateObject();const char*names[]={"product_id","license_key","device_id","device_name","app_version"};for(int i=0;i<5;i++){const char*v=text(c->cfg,names[i]);cJSON_AddStringToObject(body,names[i],v?v:"");}cJSON_ReplaceItemInObjectCaseSensitive(body,"license_key",cJSON_CreateString(runtime_key(c)));cJSON_AddStringToObject(body,"activation_id",activation?activation:"");cJSON_AddStringToObject(body,"session_id",c->session);cJSON_AddItemToObject(body,"device_profile",cJSON_Duplicate(get(c->cfg,"device_profile"),1));cJSON_AddBoolToObject(body,"request_logout_ticket",1);cJSON_AddNumberToObject(body,"version",1);cJSON_AddStringToObject(body,"request_id",id);cJSON_AddNumberToObject(body,"created_at",(double)time(NULL));cJSON_AddStringToObject(body,"operation",operation);char*out=cJSON_PrintUnformatted(body);cJSON_Delete(body);free(activation);unlock(c);return out;}
|
|
438
|
+
int ln_import_offline(ln_client*c,const char*request_json,const char*response_json){if(!c||is_denied(c)||!request_json||!response_json||strlen(request_json)>LIMIT||strlen(response_json)>LIMIT)return 0;lock(c);cJSON*req=ln_json(request_json),*res=ln_json(response_json);int64_t version=0;int ok=req&&res&&num(res,"version",&version)&&version==1;const char*fields[]={"request_id","operation"};for(int i=0;i<2;i++){const char*a=text(req,fields[i]),*b=text(res,fields[i]);if(!a||!b||strcmp(a,b))ok=0;}if(!text(req,"operation")||strcmp(text(req,"operation"),"activate"))ok=0;const char*context[]={"product_id","device_id","license_key"};for(int i=0;i<3;i++){const char*a=text(req,context[i]),*b=!strcmp(context[i],"license_key")?runtime_key(c):text(c->cfg,context[i]);if(!a||!b||strcmp(a,b))ok=0;}
|
|
373
439
|
const cJSON*result=get(res,"result"),*envelope=get(result,"lease");if(!client_context(c,envelope)||!ln_verify(envelope,get(c->cfg,"trusted_keys"),text(c->cfg,"product_id"),text(c->cfg,"device_id"),time(NULL)))ok=0;cJSON*claims=claims_from(envelope);const char*id=text(claims,"offline_request_id"),*expected=text(req,"request_id"),*activation=text(claims,"activation_id"),*response_id=text(result,"activation_id");if(!id||!expected||strcmp(id,expected)||!activation||!response_id||strcmp(activation,response_id)||envelope_policy(envelope,NULL)==1)ok=0;
|
|
374
440
|
if(ok){cJSON_Delete(c->cached);c->cached=cJSON_Duplicate(envelope,1);c->wall=time(NULL);c->tick=monotonic();c->offline=envelope_policy(c->cached,&c->interval);ok=accept_credential(c,result)&&save(c);}if(!ok)strcpy(c->error,"INVALID_OFFLINE_RESPONSE");cJSON_Delete(claims);cJSON_Delete(req);cJSON_Delete(res);publish(c,0);unlock(c);notify(c);return ok;
|
|
375
441
|
}
|
data/vendor/c/licentivo.h
CHANGED
|
@@ -53,6 +53,10 @@ int ln_heartbeat(ln_client *client);
|
|
|
53
53
|
int ln_refresh_online(ln_client *client);
|
|
54
54
|
int ln_check_feature(ln_client *client,const char *feature);
|
|
55
55
|
int ln_deactivate(ln_client *client);
|
|
56
|
+
/* Logout denies local use immediately. Network failure leaves an unlink-only retry. */
|
|
57
|
+
int ln_logout(ln_client *client);
|
|
58
|
+
int ln_retry_pending_logouts(ln_client *client);
|
|
59
|
+
int ln_logout_pending(ln_client *client);
|
|
56
60
|
int ln_start_heartbeat(ln_client *client);
|
|
57
61
|
void ln_stop_heartbeat(ln_client *client);
|
|
58
62
|
void ln_destroy(ln_client *client);
|
|
Binary file
|
|
@@ -1135,7 +1135,7 @@ typedef CURLSTScode (*curl_hstswrite_callback)(CURL *easy,
|
|
|
1135
1135
|
|
|
1136
1136
|
/* CURLOPT aliases that make no runtime difference */
|
|
1137
1137
|
|
|
1138
|
-
/* 'char *' argument to a string with a
|
|
1138
|
+
/* 'char *' argument to a string with a trailing zero */
|
|
1139
1139
|
#define CURLOPTTYPE_STRINGPOINT CURLOPTTYPE_OBJECTPOINT
|
|
1140
1140
|
|
|
1141
1141
|
/* 'struct curl_slist *' argument */
|
|
@@ -3042,9 +3042,8 @@ typedef enum {
|
|
|
3042
3042
|
} curl_closepolicy;
|
|
3043
3043
|
|
|
3044
3044
|
#define CURL_GLOBAL_SSL (1 << 0) /* no purpose since 7.57.0 */
|
|
3045
|
-
#define
|
|
3046
|
-
#define
|
|
3047
|
-
#define CURL_GLOBAL_ALL (CURL_GLOBAL_SSL | CURL_GLOBAL_WINSOCK)
|
|
3045
|
+
#define CURL_GLOBAL_WIN32 (1 << 1)
|
|
3046
|
+
#define CURL_GLOBAL_ALL (CURL_GLOBAL_SSL | CURL_GLOBAL_WIN32)
|
|
3048
3047
|
#define CURL_GLOBAL_NOTHING 0
|
|
3049
3048
|
#define CURL_GLOBAL_DEFAULT CURL_GLOBAL_ALL
|
|
3050
3049
|
#define CURL_GLOBAL_ACK_EINTR (1 << 2)
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
|
|
33
33
|
/* This is the version number of the libcurl package from which this header
|
|
34
34
|
file origins: */
|
|
35
|
-
#define LIBCURL_VERSION "8.
|
|
35
|
+
#define LIBCURL_VERSION "8.22.0"
|
|
36
36
|
|
|
37
37
|
/* The numeric version number is also available "in parts" by using these
|
|
38
38
|
defines: */
|
|
39
39
|
#define LIBCURL_VERSION_MAJOR 8
|
|
40
|
-
#define LIBCURL_VERSION_MINOR
|
|
40
|
+
#define LIBCURL_VERSION_MINOR 22
|
|
41
41
|
#define LIBCURL_VERSION_PATCH 0
|
|
42
42
|
/* This is the numeric version of the libcurl version number, meant for easier
|
|
43
43
|
parsing and comparisons by programs. The LIBCURL_VERSION_NUM define always
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
Note: This define is the full hex number and _does not_ use the
|
|
58
58
|
CURL_VERSION_BITS() macro since curl's own configure script greps for it
|
|
59
59
|
and needs it to contain the full number. */
|
|
60
|
-
#define LIBCURL_VERSION_NUM
|
|
60
|
+
#define LIBCURL_VERSION_NUM 0x081600
|
|
61
61
|
|
|
62
62
|
/*
|
|
63
63
|
* This is the date and time when the full source package was created. The
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
*
|
|
69
69
|
* "2007-11-23"
|
|
70
70
|
*/
|
|
71
|
-
#define LIBCURL_TIMESTAMP "2026-
|
|
71
|
+
#define LIBCURL_TIMESTAMP "2026-09-02"
|
|
72
72
|
|
|
73
73
|
#define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
|
|
74
74
|
#define CURL_AT_LEAST_VERSION(x, y, z) \
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: licentivo-launch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Licentivo Launch contributors
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- vendor/VERSION
|
|
47
47
|
- vendor/c/app.c
|
|
48
48
|
- vendor/c/demo.c
|
|
49
|
+
- vendor/c/device_logout.h
|
|
49
50
|
- vendor/c/feature_tasks.h
|
|
50
51
|
- vendor/c/licentivo.c
|
|
51
52
|
- vendor/c/licentivo.h
|