pyarmor.cli 9.2.6__4-py3-none-any.whl

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.
@@ -0,0 +1,1093 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ #############################################################
5
+ # #
6
+ # Copyright @ 2023 - Dashingsoft corp. #
7
+ # All rights reserved. #
8
+ # #
9
+ # Pyarmor #
10
+ # #
11
+ # Version: 8.0.1 - #
12
+ # #
13
+ #############################################################
14
+ #
15
+ #
16
+ # @File: cli/register.py
17
+ #
18
+ # @Author: Jondy Zhao (pyarmor@163.com)
19
+ #
20
+ # @Create Date: Mon Jan 2 15:39:08 CST 2023
21
+ #
22
+ import os
23
+
24
+ from base64 import b64decode, b64encode, urlsafe_b64encode
25
+ from json import loads as json_loads
26
+ from string import Template
27
+
28
+ from . import logger, CliError
29
+
30
+
31
+ # All supported machine flags for group license: [11, 26)
32
+ MACHFLAGS = 22, 21, 18, 20, 16, 11
33
+
34
+ # Upgrade notes for Pyarmor 9
35
+ URL_UPGRADE_V9 = 'https://github.com/dashingsoft/pyarmor/issues/1958'
36
+
37
+ # Template for license info
38
+ LICENSE_INFO_TEMPLATE = '''$advanced
39
+
40
+ $notes
41
+ '''
42
+
43
+
44
+ def parse_token(data):
45
+ from struct import unpack
46
+
47
+ if data and data.find(b' ') > 0:
48
+ try:
49
+ buf = b64decode(data.split()[0])
50
+
51
+ token, value = unpack('II', buf[:8])
52
+ rev, features = value & 0xff, value >> 8
53
+ licno = buf[16:34].decode('utf-8')
54
+
55
+ pstr = []
56
+ i = 64
57
+ for k in range(4):
58
+ n = buf[i]
59
+ i += 1
60
+ pstr.append(buf[i:i+n].decode('utf-8') if n
61
+ else '')
62
+ i += n
63
+
64
+ product = ('non-profits(TBD)' if pstr[2] in ('', 'TBD')
65
+ else pstr[2])
66
+ return {
67
+ 'token': token,
68
+ 'rev': rev,
69
+ 'features': features,
70
+ 'licno': licno,
71
+ 'machine': pstr[0],
72
+ 'regname': pstr[1],
73
+ 'product': product,
74
+ 'note': pstr[3],
75
+ }
76
+ except Exception as e:
77
+ logger.warning('bad token: %s', str(e))
78
+
79
+ return {
80
+ 'token': 0,
81
+ 'rev': 0,
82
+ 'features': 0,
83
+ 'licno': 'pyarmor-vax-000000',
84
+ 'regname': '',
85
+ 'product': 'non-profits',
86
+ 'note': 'This is trial license'
87
+ }
88
+
89
+
90
+ def show_help_page(prompt, url):
91
+ choice = input('\n'.join(prompt)).lower()[:1]
92
+ if choice in ('h', 'y'):
93
+ import webbrowser
94
+ webbrowser.open(url)
95
+ return choice
96
+
97
+
98
+ def check_license_version(ctx, silent=False):
99
+ licinfo = ctx.license_info
100
+ rev = licinfo.get('rev', 0)
101
+ token = licinfo.get('token', 0)
102
+ features = licinfo.get('features', 0)
103
+ if rev == 1 and features > 0 and token > 0:
104
+ logger.warning('this license is not ready for Pyarmor 9')
105
+ if silent:
106
+ return False
107
+
108
+ # Group License
109
+ if features == 15:
110
+ prompt = (
111
+ '',
112
+ 'Pyarmor 9 has some changes on license policy',
113
+ 'This group license is still available',
114
+ 'But it need request new device regfile as before',
115
+ 'Press "h" to check Pyarmor 9 Upgrade Notes',
116
+ '',
117
+ 'Help (h), Quit (q): '
118
+ )
119
+ show_help_page(prompt, URL_UPGRADE_V9)
120
+ raise SystemExit('Quit')
121
+
122
+ prompt = (
123
+ '',
124
+ 'Pyarmor 9 has big change on CI/CD pipeline',
125
+ 'If not using Pyarmor License in CI/CD pipeline',
126
+ 'Press "c" to continue',
127
+ 'Otherwise press "h" to check Pyarmor 9.0 Upgrade Notes',
128
+ '',
129
+ 'Continue (c), Help (h), Quit (q): '
130
+ )
131
+ if not show_help_page(prompt, URL_UPGRADE_V9) == 'c':
132
+ raise SystemExit('Quit')
133
+
134
+
135
+ class Register(object):
136
+
137
+ def __init__(self, ctx):
138
+ self.ctx = ctx
139
+ self.notes = []
140
+
141
+ def check_args(self, args):
142
+ if args.upgrade and args.keyfile.endswith('.zip'):
143
+ raise CliError('use .txt file to upgrade, not .zip file')
144
+
145
+ def _get_old_rcode(self):
146
+ old_license = self.ctx.read_license()
147
+ if not old_license:
148
+ logger.debug('no license file found')
149
+ return
150
+ if len(old_license) == 256:
151
+ logger.debug('no old purchased license')
152
+ return
153
+
154
+ data = b64decode(old_license)
155
+ i = data.find(b'pyarmor-vax-')
156
+ if i == -1:
157
+ raise CliError('no valid old license')
158
+ return data[i:i+18].decode()
159
+
160
+ def regurl(self, ucode, product=None, rcode=None, prepare=False):
161
+ url = self.ctx.cfg['pyarmor']['regurl'] % ucode
162
+ if product:
163
+ if len(product) > 60:
164
+ raise CliError('too long product name (length > 60)')
165
+ url += '&product=' + \
166
+ urlsafe_b64encode(product.encode('utf-8')).decode()
167
+ if rcode:
168
+ url += '&rcode=' + rcode
169
+ if prepare:
170
+ url += '&prepare=1'
171
+ return url
172
+
173
+ def update_token(self):
174
+ from pyarmor.cli.core import Pytransform3
175
+ assert Pytransform3._pytransform3 is None
176
+ with open(self.ctx.license_token, 'wb') as f:
177
+ f.close()
178
+ Pytransform3.init(self.ctx)
179
+
180
+ @property
181
+ def license_info(self):
182
+ return parse_token(self.ctx.read_token())
183
+
184
+ def _license_type(self, info):
185
+ return 'basic' if info['features'] in (1, 17) else \
186
+ 'pro' if info['features'] == 7 else \
187
+ 'group' if info['features'] == 15 else \
188
+ 'ci' if info['features'] == 23 else \
189
+ 'trial' if info['token'] == 0 else 'unknown'
190
+
191
+ def _license_to(self, info):
192
+ name = info['regname']
193
+ product = info['product']
194
+ return '%s (%s)' % (product, name) if name and product else \
195
+ 'non-profits' if not name else 'non-profits (%s)' % name
196
+
197
+ def parse_keyfile(self, filename):
198
+ with open(filename, 'r', encoding='utf-8') as f:
199
+ for line in f:
200
+ line = line.strip()
201
+ marker = 'Dear '
202
+ if line.startswith(marker):
203
+ regname = line[len(marker):].strip(' ,')
204
+ break
205
+
206
+ for line in f:
207
+ line = line.strip()
208
+ if len(line) == 192 and line.find(' ') == -1:
209
+ return regname, line
210
+
211
+ logger.error('please make sure the activation file is '
212
+ 'pure text file, the content starts with `Dear `, '
213
+ 'and includes the activation code'
214
+ '(its length is 192 and no line break)')
215
+ raise CliError('invalid activation file "%s"' % filename)
216
+
217
+ def _register_offline_license(self, fzip, namelist):
218
+ logger.info('machine id in group license: %s', ', '.join(
219
+ [x[7:] for x in namelist if x.startswith('tokens')]))
220
+ for idver in MACHFLAGS:
221
+ machid = self._get_machine_id(idver).decode('utf-8')
222
+ logger.debug('got machine id: %s', machid)
223
+ name = '/'.join(['tokens', machid])
224
+ if name in namelist:
225
+ logger.info('this machine id matchs group license')
226
+ break
227
+ else:
228
+ logger.info('no machine id matchs this group license')
229
+ logger.info('take this machine as docker container, and '
230
+ 'connect to docker host for authentication...')
231
+ mlist = self._get_docker_hostname()
232
+ if not mlist:
233
+ logger.info(
234
+ 'could not get docker host machine id\n%s',
235
+ '\n'.join([
236
+ '',
237
+ 'if this machine is docker container, please '
238
+ 'run command `pyarmor-auth` in docker host, '
239
+ 'and try it again', '',
240
+ 'otherwise please generate new group '
241
+ 'device license for this machine', '',
242
+ 'more information please check section '
243
+ '"using group license" in documentation '
244
+ '"how-to register" guide', ''
245
+ ]))
246
+ raise CliError('this group device license is not for '
247
+ 'this machine')
248
+ for machid in mlist:
249
+ hostname = '/'.join(['tokens', machid])
250
+ if hostname in namelist:
251
+ name = hostname
252
+ break
253
+ else:
254
+ logger.debug('docker host machine ids: %s', mlist)
255
+ raise CliError('this group device license is not for '
256
+ 'this docker host')
257
+
258
+ logger.debug('extracting %s', name)
259
+ self.ctx.save_group_token(fzip.read(name))
260
+
261
+ def _init_token(self, reginfo):
262
+ from struct import pack
263
+ rev, licno, lictp, regname, product = (
264
+ reginfo['rev'], reginfo['rcode'], reginfo['type'],
265
+ reginfo['name'], reginfo['product']
266
+ )
267
+
268
+ token = 0
269
+ old_license = self.license_info
270
+ if old_license['licno'] == licno:
271
+ token = old_license['token']
272
+
273
+ features = (1 if lictp == 'J' else 7 if lictp == 'Z' else
274
+ 15 if lictp == 'G' else 23 if lictp == 'C' else
275
+ 0)
276
+ notes = (
277
+ '* Do not use this file in CI/CD pipeline directly\n'
278
+ '* Only use it to request CI regfile '
279
+ '"pyarmor-ci-%s.zip"' % licno[:6].lstrip('0')
280
+ if lictp == 'C' else ''
281
+ )
282
+
283
+ regname = regname.encode('utf-8')
284
+ product = product.encode('utf-8')
285
+ notes = notes.encode('utf-8')
286
+ sizes = len(regname), len(product), len(notes)
287
+ data = pack('<II8x20s28xBB{0}sB{1}sB{2}s'.format(*sizes),
288
+ token,
289
+ rev | features << 8,
290
+ licno.encode('utf-8'),
291
+ 0,
292
+ sizes[0], regname,
293
+ sizes[1], product,
294
+ sizes[2], notes)
295
+ return b64encode(data) + b' *=='
296
+
297
+ def register_regfile(self, regfile, clean=True):
298
+ from zipfile import ZipFile
299
+
300
+ if os.path.exists(self.ctx.license_group_token):
301
+ os.remove(self.ctx.license_group_token)
302
+
303
+ path = self.ctx.reg_path
304
+ with ZipFile(regfile, 'r') as f:
305
+ for item in ('license.lic', '.pyarmor_capsule.zip'):
306
+ logger.debug('extracting %s', item)
307
+ f.extract(item, path=path)
308
+ namelist = f.namelist()
309
+ if 'group.tokens' in namelist:
310
+ self._register_offline_license(f, namelist)
311
+ elif 'ci.token' in namelist:
312
+ name = 'ci.token'
313
+ logger.debug('extracting %s', name)
314
+ self.ctx.save_token(f.read(name))
315
+ elif 'group.info' in namelist:
316
+ logger.info('this file is only used to '
317
+ 'request device regfile:\n\n'
318
+ '\tpyarmor reg -g 1 %s\n', regfile)
319
+ raise CliError('wrong usage for Group License')
320
+ elif 'reg.info' in namelist:
321
+ data = f.read('reg.info')
322
+ info = json_loads(data)
323
+ if info.get('type', '') == 'C':
324
+ logger.info('this file is only used to '
325
+ 'request ci regfile:\n\n'
326
+ '\tpyarmor reg -C %s\n', regfile)
327
+ raise CliError('wrong usage for CI License')
328
+ self.ctx.save_token(self._init_token(info))
329
+ else:
330
+ logger.error('this license is not ready for Pyarmor 9')
331
+ prompt = (
332
+ '',
333
+ 'Pyarmor 9 has big change on CI/CD pipeline',
334
+ 'Press "h" to check Pyarmor 9.0 Upgrade Notes',
335
+ '',
336
+ 'Help (h), Quit (q): '
337
+ )
338
+ show_help_page(prompt, URL_UPGRADE_V9)
339
+ raise SystemExit()
340
+
341
+ def _get_docker_hostname(self):
342
+ try:
343
+ from socket import socket, AF_INET, SOCK_STREAM
344
+ host = os.getenv('PYARMOR_DOCKER_HOST', 'host.docker.internal')
345
+ port = 29092
346
+ rlist = []
347
+ with socket(AF_INET, SOCK_STREAM) as s:
348
+ s.connect((host, port))
349
+ logger.info('socket addr: %s', str(s.getsockname()))
350
+ logger.info('remote addr: %s', str(s.getpeername()))
351
+ s.sendall(b'PADH' + b'x' * 60)
352
+ while True:
353
+ flag = s.recv(1)
354
+ if ord(flag) - 87 in MACHFLAGS:
355
+ data = s.recv(32)
356
+ machid = (flag + data).decode('utf-8')
357
+ logger.info('got docker host machine id: %s', machid)
358
+ rlist.append(machid)
359
+ if s.recv(1) == b'\x00':
360
+ break
361
+ return rlist
362
+ except Exception as e:
363
+ logger.debug('%s:%d:%s', host, port, str(e))
364
+
365
+ def _get_machine_id(self, devflag=11):
366
+ from pyarmor.cli.core import Pytransform3
367
+ try:
368
+ return Pytransform3.get_hd_info(devflag)
369
+ except SystemError:
370
+ from time import time
371
+ return b'u-machine-id:%020.6f' % time()
372
+
373
+ def generate_group_device(self, devid):
374
+ from datetime import datetime
375
+ from platform import uname
376
+ path = self.ctx.group_device_file(devid)
377
+ logger.info('generating device file "%s"', path)
378
+ os.makedirs(os.path.dirname(path), exist_ok=True)
379
+ uinfo = uname()
380
+ devflag = self.ctx.cfg['builder'].getint('group_device_flag', 21)
381
+ machid = self._get_machine_id(devflag).decode('utf-8')
382
+ logger.info('current machine id is "%s"', machid)
383
+ tpl = Template('\n'.join([
384
+ '# Generated by Pyarmor $rev, $timestamp',
385
+ 'host: $node',
386
+ 'system: $host ($version)',
387
+ 'machine: $machine'
388
+ ])).substitute(
389
+ rev='.'.join(self.ctx.version),
390
+ node=uinfo.node,
391
+ host=uinfo.system,
392
+ version=uinfo.version,
393
+ timestamp=datetime.now().isoformat(),
394
+ machine=machid,
395
+ )
396
+ with open(path, "wb") as f:
397
+ f.write(tpl.encode('utf-8'))
398
+ return path
399
+
400
+ def check_group_license(self, silent=False):
401
+ licinfo = self.license_info
402
+ if licinfo['features'] & 8:
403
+ licmach = licinfo.get('machine', '')
404
+ if not licmach:
405
+ raise RuntimeError('no token machine')
406
+
407
+ idver = ord(licmach[0]) - 87
408
+ # This can't be called in "cmd_gen", otherwise crash
409
+ machid = self._get_machine_id(idver).decode('utf-8')
410
+ if machid == licmach:
411
+ return
412
+
413
+ mlist = self._get_docker_hostname()
414
+ if mlist and licmach in mlist:
415
+ return
416
+
417
+ logger.info('this license is for machine: %s', licmach)
418
+
419
+ if mlist:
420
+ logger.info('but docker host machine ids: %s',
421
+ ', '.join(mlist))
422
+ raise RuntimeError(
423
+ 'this group license is not for this docker host')
424
+ else:
425
+ logger.info('but this machine id: %s', machid)
426
+ raise RuntimeError(
427
+ 'this group license is not for this machine')
428
+
429
+ def __str__(self):
430
+ info = self.license_info
431
+ lictype = self._license_type(info)
432
+
433
+ fmt = '%-16s: %s'
434
+ lines = [
435
+ fmt % ('License Type', 'pyarmor-' + lictype),
436
+ fmt % ('License No.', info['licno']),
437
+ fmt % ('License To', info['regname']),
438
+ fmt % ('License Product', info['product']),
439
+ '',
440
+ ]
441
+
442
+ bccmode = info['features'] & 2
443
+ rftmode = info['features'] & 4
444
+ cidmode = info.get('machine', '').startswith('CITK')
445
+ advanced = [
446
+ fmt % ('BCC Mode', 'Yes' if bccmode else 'No'),
447
+ fmt % ('RFT Mode', 'Yes' if rftmode else 'No'),
448
+ fmt % ('CI/CD Mode', 'Yes' if cidmode else 'No'),
449
+ ]
450
+ if lictype == 'trial':
451
+ self.notes.append('* Can\'t obfuscate big script and mix str')
452
+ elif lictype in ('basic', 'pro'):
453
+ self.notes.append('* Need verify license online '
454
+ 'when obfuscating scripts')
455
+ elif lictype == 'group':
456
+ self.notes.append('* Offline obfuscation')
457
+
458
+ if info['note']:
459
+ self.notes.extend(info['note'].splitlines())
460
+
461
+ if self.notes:
462
+ self.notes.insert(0, 'Notes')
463
+
464
+ lines.append(Template(LICENSE_INFO_TEMPLATE).substitute(
465
+ advanced='\n'.join(advanced),
466
+ notes='\n'.join(self.notes),
467
+ ))
468
+
469
+ return '\n'.join(lines)
470
+
471
+
472
+ upgrade_to_basic_info = Template('''
473
+ You are about to upgrade old Pyarmor license to Pyarmor Basic License
474
+
475
+ The upgraded license information will be''')
476
+
477
+ upgrade_to_pro_info = Template('''
478
+ You are about to upgrade old Pyarmor license to Pyarmor Pro License
479
+
480
+ The original license no: $rcode
481
+
482
+ The upgraded license information will be''')
483
+
484
+
485
+ class WebRegister(Register):
486
+
487
+ # Before v9.0: 1
488
+ # Pyarmor v9.0: 2
489
+ # Pyarmor v9.2: 3
490
+ LICENSE_REVSION = 3
491
+
492
+ # Before v9.0: no CI License
493
+ # Pyarmor v9.0: 1
494
+ # Pyarmor v9.2: 2
495
+ CI_LICENSE_REVSION = 2
496
+
497
+ def _request(self, url):
498
+ from http.client import HTTPSConnection
499
+ n = len('https://')
500
+ k = url.find('/', n)
501
+ conn = HTTPSConnection(url[n:k])
502
+ conn.request("GET", url[k:])
503
+ return conn.getresponse()
504
+
505
+ def check_request_interval(self, delta=30.0, activation=False):
506
+ """Make sure no more than 2 requests in 1 minute"""
507
+ tspath = os.path.join(self.ctx.reg_path, 'last_register')
508
+ try:
509
+ st = os.stat(tspath)
510
+ except FileNotFoundError:
511
+ os.makedirs(tspath)
512
+ else:
513
+ from time import sleep, time
514
+ d = delta - (time() - st.st_mtime)
515
+ if d > 0:
516
+ if activation:
517
+ logger.warning(
518
+ 'caution: this activation file "%s" can only '
519
+ 'be used no more than 10 times', activation)
520
+ logger.info('waiting for %d seconds', d)
521
+ while time() - st.st_mtime < delta:
522
+ logger.info('waiting ...')
523
+ sleep(3.0)
524
+ os.utime(tspath)
525
+
526
+ def _send_request(self, url, timeout=6.0):
527
+ try:
528
+ return self._request(url)
529
+ except Exception as e:
530
+ logger.debug('direct request failed "%s"', str(e))
531
+
532
+ from urllib.request import urlopen
533
+ from ssl import _create_unverified_context
534
+ context = _create_unverified_context()
535
+ return urlopen(url, None, timeout, context=context)
536
+
537
+ def _remove_token(self):
538
+ if os.path.exists(self.ctx.license_token):
539
+ logger.debug('remove old token')
540
+ os.remove(self.ctx.license_token)
541
+
542
+ def _check_product_name(self, name):
543
+ name = name.lower()
544
+ if name.count(name[0]) == len(name) or name.find('pyarmor') >= 0:
545
+ raise RuntimeError('invalid product name "%s"' % name)
546
+
547
+ def prepare(self, keyfile, product, upgrade=False):
548
+ reginfo = self.parse_keyfile(keyfile)
549
+ logger.info('prepare "%s"', keyfile)
550
+
551
+ if product:
552
+ self._check_product_name(product)
553
+
554
+ rcode = self._get_old_rcode() if upgrade else None
555
+ if upgrade and not rcode and keyfile.endswith('regcode-to-pro.txt'):
556
+ logger.error('please use `pyarmor-7 -v` to check old license')
557
+ logger.error('this code is used to upgrade old license')
558
+ raise CliError('no found old license in this machine')
559
+ url = self.regurl(reginfo[1], rcode=rcode, prepare=True)
560
+ logger.debug('url: %s', url)
561
+
562
+ logger.info('query key file from server')
563
+ with self._send_request(url) as res:
564
+ if not res:
565
+ logger.error('please try it later')
566
+ raise CliError('no response from license server')
567
+ if res.code != 200:
568
+ logger.error('HTTP Error %s', res.code)
569
+ raise CliError(res.read().decode('utf-8'))
570
+ info = json_loads(res.read())
571
+
572
+ pname = info['product']
573
+ if pname in ('', 'TBD'):
574
+ info['product'] = product
575
+ elif pname != product:
576
+ logger.warning('this license has bound to product "%s"', pname)
577
+ logger.warning('it can not be changed to "%s"', product)
578
+
579
+ lines = []
580
+ if upgrade:
581
+ if rcode and not rcode.startswith('pyarmor-vax-'):
582
+ raise CliError('old license "%s" can not be upgraded' % rcode)
583
+ if info['upgrade']:
584
+ lines.append(upgrade_to_pro_info.substitute(rcode=rcode))
585
+ else:
586
+ lines.append(upgrade_to_basic_info.substitute())
587
+ else:
588
+ if info['lictype'] == 'OLD':
589
+ raise CliError('old license only works for Pyarmor <= 7.7.4')
590
+ if info['lictype'] not in ('BASIC', 'PRO', 'GROUP', 'CI'):
591
+ raise CliError('unknown license type %s' % info['lictype'])
592
+ lines.append('This license registration information will be')
593
+
594
+ if info['product'] in ('', 'TBD') and info['lictype'] == 'GROUP':
595
+ raise CliError('"TBD" is invalid product name for group license')
596
+
597
+ fmt = '%-16s: %s'
598
+ lines.extend([
599
+ '',
600
+ fmt % ('License Type', 'pyarmor-' + info['lictype'].lower()),
601
+ fmt % ('License To', info['regname']),
602
+ fmt % ('License Product', info['product']),
603
+ '',
604
+ ])
605
+ if info['product'] == 'non-profits':
606
+ lines.append('This license is about to be used for non-profits')
607
+ if info['product'] in ('', 'TBD'):
608
+ lines.append('This license is bind to non-profits(TBD) '
609
+ 'for the time being')
610
+ lines.append('If not change "TBD" to product name in 6 months, '
611
+ 'it will be set to "non-profits" automatically')
612
+ else:
613
+ lines.append('This license is about to be used for product "%s"'
614
+ % info['product'])
615
+ if info['product'] not in ('', 'TBD'):
616
+ lines.append('')
617
+ lines.append('IMPORTANT: PRODUCT NAME CAN NOT BE CHANGED '
618
+ 'AFTER INITIAL REGISTRATION')
619
+
620
+ lines.extend(['', ''])
621
+ return info, '\n'.join(lines)
622
+
623
+ def upgrade_to_pro(self, keyfile, product):
624
+ raise CliError('this feature is not available now')
625
+
626
+ logger.info('process upgrading file "%s"', keyfile)
627
+ reginfo = self.parse_keyfile(keyfile)
628
+
629
+ rcode = self._get_old_rcode()
630
+ logger.info('old license no: %s', rcode)
631
+
632
+ url = self.regurl(reginfo[1], product=product, rcode=rcode)
633
+ logger.debug('url: %s', url)
634
+
635
+ logger.info('send upgrade request to server')
636
+ res = self._send_request(url)
637
+ regfile, lictype = self._handle_response(res)
638
+
639
+ logger.info('update license token')
640
+ self.update_token()
641
+ logger.info('This license has been upgraded successfully')
642
+
643
+ notes = '* Please backup regfile "%s" carefully, and ' \
644
+ 'use this file for subsequent registration' % regfile,
645
+ logger.info('Import Notes:\n\n%s\n', notes)
646
+
647
+ def register(self, keyfile, product, upgrade=False, group=False):
648
+ if keyfile.endswith('.zip'):
649
+ logger.info('register "%s"', keyfile)
650
+ self.register_regfile(keyfile)
651
+ return
652
+
653
+ logger.info('process activation file "%s"', keyfile)
654
+ reginfo = self.parse_keyfile(keyfile)
655
+
656
+ url = self.regurl(reginfo[1], product=product)
657
+ url += '&rev=' + str(self.LICENSE_REVSION)
658
+ if upgrade:
659
+ url += '&upgrade_to_basic=1'
660
+ logger.debug('url: %s', url)
661
+
662
+ logger.info('send request to server')
663
+ res = self._send_request(url)
664
+ regfile, lictype = self._handle_response(res)
665
+
666
+ logger.info('')
667
+ logger.info('the registration file "%s" has been generated', regfile)
668
+ logger.info('this license has been activated sucessfully')
669
+
670
+ notes = [
671
+ '* Please backup "%s", but do not use it to '
672
+ 'register Pyarmor' % os.path.basename(keyfile),
673
+ '* Please backup regfile "%s", and '
674
+ 'use this file for next any registration' % regfile,
675
+ '* Do not use this file in docker and CI/CD pipeline',
676
+ '',
677
+ ]
678
+ logger.info('\n\nImport Notes:\n%s\n', '\n'.join(notes))
679
+
680
+ input('Type Enter to continue ...')
681
+ LicenseHelper(self).run(lictype, regfile)
682
+
683
+ def _handle_response(self, res):
684
+ if res and res.code == 200:
685
+ lictype = None
686
+ dis = res.headers.get('Content-Disposition')
687
+ filename = dis.split('"')[1] if dis else 'pyarmor-regfile.zip'
688
+ logger.info('write registration file "%s"', filename)
689
+ data = res.read()
690
+ if data.startswith(b'{"group":'):
691
+ lictype = 'G'
692
+ n = data.find(b'}') + 1
693
+ with open(filename, 'wb') as f:
694
+ f.write(data[n:])
695
+ self._write_group_info(filename, data[:n])
696
+ elif data.startswith(b'REGINFO:'):
697
+ i = len(b'REGINFO:')
698
+ n = data[i] + (data[i+1] << 8)
699
+ i += 2
700
+ n += i
701
+ with open(filename, 'wb') as f:
702
+ f.write(data[n:])
703
+ reginfo = data[i:n]
704
+ lictype = json_loads(reginfo).get('type', None)
705
+ self._write_reg_info(filename, reginfo)
706
+ else:
707
+ # Only for request group token
708
+ logger.debug('no REGINFO found')
709
+ with open(filename, 'wb') as f:
710
+ f.write(data)
711
+ return filename, lictype
712
+
713
+ elif res:
714
+ data = res.read()
715
+ logger.debug('server return(%d): %s', res.code, data)
716
+ try:
717
+ msg = data.decode('utf-8')
718
+ except Exception as e:
719
+ logger.debug('decode server data error "%s"', e)
720
+ msg = data
721
+ raise CliError(msg)
722
+
723
+ raise CliError('no response from license server')
724
+
725
+ def _write_reg_info(self, filename, data):
726
+ from zipfile import ZipFile
727
+ logger.info('write reg information')
728
+ with ZipFile(filename, 'a') as f:
729
+ f.writestr('reg.info', data)
730
+
731
+ def _write_group_info(self, filename, data):
732
+ from zipfile import ZipFile
733
+ logger.info('write group information')
734
+ with ZipFile(filename, 'a') as f:
735
+ f.writestr('group.info', data)
736
+
737
+ def request_device_regfile(self, regfile, devid):
738
+ from zipfile import ZipFile
739
+ devfile = self.ctx.group_device_file(devid)
740
+ rev = self.LICENSE_REVSION
741
+ logger.info('request device regfile "%s" (v%d)', devfile, rev)
742
+ logger.info('use group license "%s"', regfile)
743
+ if not os.path.exists(devfile):
744
+ logger.error('please generate device file in offline device by')
745
+ logger.error(' pyarmor reg -g %s', devid)
746
+ logger.error('and copy generated device file to this machine')
747
+ raise CliError('no group device file "%s"' % devfile)
748
+
749
+ with open(devfile) as f:
750
+ prefix = 'machine:'
751
+ for line in f:
752
+ if line.startswith(prefix):
753
+ machid = line[len(prefix):].strip()
754
+ break
755
+ else:
756
+ logger.error('no machid information in device file')
757
+ raise CliError('invalid device file "%s"' % devfile)
758
+
759
+ with ZipFile(regfile, 'r') as f:
760
+ if 'group.info' not in f.namelist():
761
+ logger.error('no group information in group license file')
762
+ raise CliError('invalid group license file "%s"' % regfile)
763
+ group = json_loads(f.read('group.info'))
764
+ licdata = f.read('license.lic')
765
+ capsule = f.read('.pyarmor_capsule.zip')
766
+
767
+ # Ignore token cache
768
+ tokencache = os.path.join(os.path.dirname(devfile), 'tokens', machid)
769
+ if False and os.path.exists(tokencache):
770
+ logger.info('read cached "%s"', tokencache)
771
+ with open(tokencache, 'rb') as f:
772
+ data = f.read()
773
+ filename = regfile.replace('pyarmor-', 'pyarmor-device-').replace(
774
+ '.zip', '.%s.zip' % devid)
775
+ logger.info('write registeration file "%s"', filename)
776
+ else:
777
+ logger.info('send request to server')
778
+ url = self.regurl('/'.join(['group', group['ucode']]))
779
+ paras = ('rev', str(rev)), ('group', str(group['group'])), \
780
+ ('source', machid), ('devid', str(devid))
781
+ url += '&'.join(['='.join(x) for x in paras])
782
+ logger.debug('url: %s', url)
783
+
784
+ res = self._send_request(url)
785
+ filename, lictype = self._handle_response(res)
786
+ with open(filename, 'rb') as f:
787
+ data = f.read()
788
+ os.makedirs(os.path.dirname(tokencache), exist_ok=True)
789
+ with open(tokencache, 'wb') as f:
790
+ f.write(data)
791
+
792
+ with ZipFile(filename, 'w') as f:
793
+ f.writestr('license.lic', licdata)
794
+ f.writestr('.pyarmor_capsule.zip', capsule)
795
+ f.writestr('group.tokens', b'')
796
+ f.writestr('dev.info', ('%s,%s' % (devid, machid)).encode('utf-8'))
797
+ f.writestr('tokens/' + machid, data)
798
+
799
+ logger.info('please copy deivce regfile to offline device and run')
800
+ logger.info(' pyarmor reg %s', filename)
801
+
802
+ register_group_device = request_device_regfile
803
+
804
+ def _write_ci_info(self, filename, data):
805
+ from zipfile import ZipFile
806
+ logger.info('write ci information')
807
+ with ZipFile(filename, 'a') as f:
808
+ f.writestr('ci.token', data)
809
+
810
+ def request_ci_regfile(self, regfile):
811
+ rev = self.LICENSE_REVSION
812
+ cirev = self.CI_LICENSE_REVSION
813
+ logger.info('request ci regfile (v%d) by "%s"', cirev, regfile)
814
+ from zipfile import ZipFile
815
+
816
+ with ZipFile(regfile, 'r') as f:
817
+ if 'reg.info' not in f.namelist():
818
+ logger.error('missing reg.info in regfile, '
819
+ 'this license may be out of date, '
820
+ 'please check Pyarmor upgrade notes')
821
+ raise CliError('can not request CI license')
822
+ reginfo = json_loads(f.read('reg.info'))
823
+
824
+ ucode = reginfo['ucode']
825
+ rcode = reginfo['rcode']
826
+ if len(ucode) != 192:
827
+ raise CliError('invalid ci registration file "%s"', regfile)
828
+
829
+ url = self.regurl('ci/%s' % ucode)
830
+ paras = ('rev', str(rev)), ('cirev', str(cirev))
831
+ url += '&'.join(['='.join(x) for x in paras])
832
+ logger.debug('url: %s', url)
833
+
834
+ logger.info('send request to server')
835
+ res = self._send_request(url)
836
+
837
+ logger.info('handle response')
838
+ if res is None:
839
+ raise CliError('no response from license server')
840
+
841
+ elif res.code != 200:
842
+ raise CliError(res.read().decode('utf-8'))
843
+
844
+ data = res.read()
845
+ if not data.startswith(b'CITOKEN:'):
846
+ raise CliError('wrong server data: "%s"' % data)
847
+
848
+ i = len(b'CITOKEN:') + 2
849
+ n = data[i-2] + (data[i-1] << 8)
850
+ token = data[i:i+n]
851
+
852
+ rn = rcode[-6:].lstrip('0')
853
+ cifile = 'pyarmor-ci-%s.zip' % rn
854
+
855
+ with ZipFile(regfile, 'r') as src:
856
+ with ZipFile(cifile, 'w') as dst:
857
+ for x in src.namelist():
858
+ dst.writestr(x, src.read(x))
859
+ dst.writestr('ci.token', token)
860
+
861
+ logger.info('generate CI regfile "%s" successfully', cifile)
862
+
863
+ ver = '.'.join([str(x) for x in self.ctx.version])
864
+ logger.info('\n\nCheck CI license in local machine by:\n'
865
+ '\n\tpyarmor reg %s\n\n'
866
+ 'Register Pyarmor in CI/CD pipeline by:\n'
867
+ '\n\tpip install pyarmor==%s\n'
868
+ '\tpyarmor reg %s\n',
869
+ cifile, ver, cifile)
870
+
871
+
872
+ BASIC_LICENSE_HELP_INFO = Template('''
873
+ Using Basic license in CI/CD pipeline or docker container need extra steps, please check this page
874
+
875
+ $docurl/how-to/ci.html
876
+
877
+ More usage about Basic License, check this page
878
+
879
+ $docurl/how-to/register.html
880
+
881
+ If need register Pyarmor in build device, run this command:
882
+
883
+ pyarmor reg $regfile
884
+
885
+ ''')
886
+
887
+ PRO_LICENSE_HELP_INFO = Template('''
888
+ Pro license can't be used in CI/CD pipeline or docker container direclty.
889
+
890
+ A few times for debug purpose, about 60 runs per month, may work.
891
+
892
+ But there is one workaroud for Pro licnese in CI/CD pipeline, please check this page
893
+
894
+ $docurl/how-to/ci.html
895
+
896
+ More usage about Pro License, check this page
897
+
898
+ $docurl/how-to/register.html
899
+
900
+ If need register Pyarmor in build device, run this command:
901
+
902
+ pyarmor reg $regfile
903
+
904
+ ''')
905
+
906
+ GROUP_LICENSE_HELP_INFO = Template('''
907
+ Group License file `$regfile` is only used to request device regfile
908
+
909
+ In order to register Pyarmor in offline device:
910
+
911
+ 1. In the build device (may be offline), generate device info by this command
912
+
913
+ pyarmor reg -g 1
914
+
915
+ 2. In any online device, copy device file generated by first step, then request device regfile for this device
916
+
917
+ cp pyarmor-group-device.1 .pyarmor/group/
918
+ pyarmor reg -g 1 $regfile
919
+
920
+ 3. In the build device, register Pyarmor by device regfile
921
+
922
+ pyarmor reg pyarmor-device-regfile-xxxx.1.zip
923
+
924
+ More usage about CI License, check section `Using group licnese`
925
+
926
+ $docurl/how-to/register.html
927
+ ''')
928
+
929
+ CI_LICENSE_HELP_INFO = Template('''
930
+ CI license file `$regfile` is only used to request CI regfile
931
+
932
+ CI regfile is used to register Pyarmor in the CI/CD pipeline
933
+
934
+ If need request CI regfile, run this command:
935
+
936
+ pyarmor reg -C $regfile
937
+
938
+ Note that CI regfile can NOT be used in physical machine, and there is rate limits to register CI regfile in CI/CD pipeline and docker container.
939
+
940
+ It may need request new CI regfile after Pyarmor is upgraded, please check section `When need to request new CI regfile` in this page
941
+
942
+ $docurl/how-to/ci.html
943
+
944
+ More usage about CI License, check these pages
945
+
946
+ $docurl/how-to/register.html
947
+ $docurl/how-to/ci.html
948
+ ''')
949
+
950
+
951
+ class LicenseHelper(object):
952
+ """Only used for first activate license.
953
+
954
+ Help the beginner to understand how to use different licenses.
955
+
956
+ """
957
+
958
+ def __init__(self, parent):
959
+ self.parent = parent
960
+ self.docurl = parent.ctx.cfg.get(
961
+ 'pyarmor', 'docurl').rstrip('/').replace('{lang}', 'en')
962
+ self.print = print
963
+
964
+ def run(self, lictype, regfile):
965
+ if lictype == 'G':
966
+ self._group_license_helper(regfile)
967
+
968
+ elif lictype == 'C':
969
+ self._ci_license_helper(regfile)
970
+
971
+ elif lictype == 'Z':
972
+ self._pro_license_helper(regfile)
973
+
974
+ elif lictype in ('J', 'B', 'P'):
975
+ self._basic_license_helper(regfile)
976
+
977
+ else:
978
+ raise CliError('unknown license type "%s"' % lictype)
979
+
980
+ def _basic_license_helper(self, regfile):
981
+ self.print(BASIC_LICENSE_HELP_INFO.substitute(
982
+ docurl=self.docurl, regfile=regfile))
983
+
984
+ prompt = 'Yes (y), No (n), Quit (q): '
985
+ self.print('Show basic license usage in webbrowser? (n)')
986
+ choice = show_help_page([prompt], self.docurl + '/how-to/register.html')
987
+ if choice == 'q':
988
+ return
989
+
990
+ self.print('Show basic license for CI/CD pipeline or docker? (n)')
991
+ choice = show_help_page([prompt], self.docurl + '/how-to/ci.html')
992
+ if choice == 'q':
993
+ return
994
+
995
+ self.print('Do you want register Pyarmor in this machine? (y)')
996
+ choice = input(prompt).lower()[:1]
997
+ if choice == 'q':
998
+ return
999
+
1000
+ if choice in ('y', ''):
1001
+ self.print('register "%s"' % regfile)
1002
+ self.parent.register_regfile(regfile)
1003
+ self.print('This license registration information:\n\n'
1004
+ '%s' % self.parent)
1005
+
1006
+ def _pro_license_helper(self, regfile):
1007
+ self.print(PRO_LICENSE_HELP_INFO.substitute(
1008
+ docurl=self.docurl, regfile=regfile))
1009
+
1010
+ prompt = 'Yes (y), No (n), Quit (q): '
1011
+ self.print('Show pro license usage in webbrowser? (n)')
1012
+ choice = show_help_page([prompt], self.docurl + '/how-to/register.html')
1013
+ if choice == 'q':
1014
+ return
1015
+
1016
+ self.print('Show pro license for CI/CD pipeline or docker? (n)')
1017
+ choice = show_help_page([prompt], self.docurl + '/how-to/ci.html')
1018
+ if choice == 'q':
1019
+ return
1020
+
1021
+ self.print('Do you want register Pyarmor in this machine? (y)')
1022
+ choice = input(prompt).lower()[:1]
1023
+ if choice == 'q':
1024
+ return
1025
+
1026
+ if choice in ('y', ''):
1027
+ self.print('register "%s"' % regfile)
1028
+ self.parent.register_regfile(regfile)
1029
+ self.print('This license registration information:\n\n'
1030
+ '%s' % self.parent)
1031
+
1032
+ def _group_license_helper(self, regfile):
1033
+ self.print(GROUP_LICENSE_HELP_INFO.substitute(
1034
+ docurl=self.docurl, regfile=regfile))
1035
+
1036
+ prompt = 'Yes (y), No (n), Quit (q): '
1037
+ self.print('Show group license usage in webbrowser? (n)')
1038
+ choice = show_help_page([prompt], self.docurl + '/how-to/register.html')
1039
+ if choice == 'q':
1040
+ return
1041
+
1042
+ self.print('Do you want register Pyarmor in this machine? (n)')
1043
+ choice = input(prompt).lower()[:1]
1044
+ if choice == 'q':
1045
+ return
1046
+
1047
+ if choice != 'y':
1048
+ return
1049
+
1050
+ self.print('Please assign one unused device no. to this device, '
1051
+ 'starts from 1')
1052
+ devid = None
1053
+ while devid is None:
1054
+ a = input('Type device no. (default is 1): ')
1055
+ if a == '':
1056
+ devid = 1
1057
+ elif (not a.isdigit()) or int(a) < 1 or int(a) > 100:
1058
+ self.print('invalid input')
1059
+ else:
1060
+ devid = int(a)
1061
+
1062
+ if devid:
1063
+ sep = '-' * 16
1064
+ devinfo = self.parent.ctx.group_device_file(devid)
1065
+ self.print('%s 1. generate device info' % sep)
1066
+ if os.path.exists(devinfo):
1067
+ logger.warning('old device file has been exists')
1068
+ else:
1069
+ self.parent.generate_group_device(devid)
1070
+ self.print('%s 2. request device regfile' % sep)
1071
+ self.parent.request_device_regfile(regfile, devid)
1072
+ self.print('%s 3. register Pyarmor with device regfile' % sep)
1073
+ self.parent.register(regfile.
1074
+ replace('.zip', '.%d.zip' % devid).
1075
+ replace('-regfile', '-device-regfile'))
1076
+
1077
+ def _ci_license_helper(self, regfile):
1078
+ self.print(CI_LICENSE_HELP_INFO.substitute(
1079
+ docurl=self.docurl, regfile=regfile))
1080
+
1081
+ prompt = 'Yes (y), No (n), Quit (q): '
1082
+ self.print('Show ci license usage in webbrowser? (n)')
1083
+ choice = show_help_page([prompt], self.docurl + '/how-to/ci.html')
1084
+ if choice == 'q':
1085
+ return
1086
+
1087
+ self.print('Do you want request one CI Regfile now? (y)')
1088
+ choice = input(prompt).lower()[:1]
1089
+ if choice == 'q':
1090
+ return
1091
+
1092
+ if choice in ('y', ''):
1093
+ self.parent.request_ci_regfile(regfile)