printerxpl-forge 6.2.0__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.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. src/version.py +36 -0
src/utils/helper.py ADDED
@@ -0,0 +1,732 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ # Author : Andre Henrique (@mrhenrike)
6
+ # GitHub : https://github.com/mrhenrike
7
+ # LinkedIn : https://linkedin.com/in/mrhenrike
8
+ # X/Twitter : https://x.com/mrhenrike
9
+
10
+ from __future__ import print_function
11
+
12
+ # python standard library
13
+ from socket import socket
14
+ import socket as socket_module
15
+ import sys
16
+ import os
17
+ import re
18
+ import stat
19
+ import time
20
+ import datetime
21
+ import traceback
22
+
23
+ # third party modules
24
+ try: # unicode monkeypatch for windoze (only needed on Python 2 / legacy Windows)
25
+ import win_unicode_console
26
+ win_unicode_console.enable()
27
+ except Exception:
28
+ pass # Optional module – not required on Python 3
29
+
30
+ try: # os independent color support (preferred)
31
+ from colorama import init, Fore, Back, Style
32
+ init(autoreset=False) # required for ANSI on Windows via colorama
33
+ except ImportError:
34
+ # Fallback ANSI – works on Linux, macOS, BSD, Android/Termux,
35
+ # and on Windows 10 1511+ (ConEmu, Windows Terminal, VS Code terminal).
36
+ # On legacy Windows consoles (cmd.exe < Win10) ANSI isn't supported,
37
+ # but colorama should always be installed via requirements.txt.
38
+ _ANSI = sys.stdout.isatty() # only emit colors when output is a tty
39
+
40
+ class Back(): # type: ignore[no-redef]
41
+ BLUE = '\x1b[44m' if _ANSI else ''
42
+ CYAN = '\x1b[46m' if _ANSI else ''
43
+ GREEN = '\x1b[42m' if _ANSI else ''
44
+ MAGENTA = '\x1b[45m' if _ANSI else ''
45
+ RED = '\x1b[41m' if _ANSI else ''
46
+ YELLOW = '\x1b[43m' if _ANSI else ''
47
+
48
+ class Fore(): # type: ignore[no-redef]
49
+ BLUE = '\x1b[34m' if _ANSI else ''
50
+ CYAN = '\x1b[36m' if _ANSI else ''
51
+ MAGENTA = '\x1b[35m' if _ANSI else ''
52
+ YELLOW = '\x1b[33m' if _ANSI else ''
53
+ RED = '\x1b[31m' if _ANSI else ''
54
+
55
+ class Style(): # type: ignore[no-redef]
56
+ DIM = '\x1b[2m' if _ANSI else ''
57
+ BRIGHT = '\x1b[1m' if _ANSI else ''
58
+ RESET_ALL = '\x1b[0m' if _ANSI else ''
59
+ NORMAL = '\x1b[22m' if _ANSI else ''
60
+
61
+ _msg = "Install 'colorama' for best color support: pip install colorama"
62
+ print(Back.RED + _msg + Style.RESET_ALL)
63
+
64
+ # ----------------------------------------------------------------------
65
+
66
+ # return first item of list or alternative
67
+
68
+
69
+ def item(mylist, alternative=""):
70
+ return next(iter(mylist), alternative)
71
+
72
+ # split list into chunks of equal size
73
+
74
+
75
+ def chunks(l, n):
76
+ for i in range(0, len(l), n):
77
+ yield l[i:i+n]
78
+
79
+ # ----------------------------------------------------------------------
80
+
81
+
82
+ class log():
83
+ # open logfile
84
+ def open(self, filename):
85
+ try:
86
+ return open(filename, mode='wb')
87
+ except IOError as e:
88
+ output().errmsg("Cannot open logfile", e)
89
+ return None
90
+
91
+ # write raw data to logfile
92
+ def write(self, logfile, data):
93
+ # logfile open and data non-empty
94
+ if logfile and data:
95
+ try:
96
+ logfile.write(data)
97
+ except IOError as e:
98
+ output().errmsg("Cannot log", e)
99
+
100
+ # write comment to logfile
101
+ def comment(self, logfile, line):
102
+ comment = "%" + ("[ " + line + " ]").center(72, '-')
103
+ self.write(logfile, os.linesep + comment + os.linesep)
104
+
105
+ # close logfile
106
+ def close(self, logfile):
107
+ try:
108
+ logfile.close()
109
+ except IOError as e:
110
+ output().errmsg("Cannot close logfile", e)
111
+
112
+ # ----------------------------------------------------------------------
113
+
114
+
115
+ class output():
116
+ # show message in cyan color
117
+ def message(self, msg):
118
+ print(f"\033[1;36m{msg}\033[0m")
119
+
120
+ # show red message
121
+ def red(self, msg):
122
+ print(f"\033[1;31m{msg}\033[0m")
123
+
124
+ # show green message
125
+ def green(self, msg):
126
+ print(f"\033[1;32m{msg}\033[0m")
127
+
128
+ # show blue message
129
+ def blue(self, msg):
130
+ print(f"\033[1;34m{msg}\033[0m")
131
+
132
+ # show yellow message
133
+ def yellow(self, msg):
134
+ print(f"\033[1;33m{msg}\033[0m")
135
+
136
+ # show banner
137
+ def intro(self, quiet):
138
+ if not quiet:
139
+ print(Style.BRIGHT + Fore.CYAN + "PrinterXPL-Forge - Advanced Printer Penetration Testing" + Style.RESET_ALL)
140
+ print(Style.DIM + "Printer Reaper - a tool to discover, fuzz and exploit printers" + Style.RESET_ALL)
141
+
142
+ def header(self, msg, eol=None):
143
+ if msg:
144
+ print(Back.BLUE + msg + Style.RESET_ALL, end=eol)
145
+ sys.stdout.flush()
146
+
147
+ # show send commands (debug mode)
148
+ def send(self, str, mode):
149
+ if str:
150
+ print(Back.CYAN + str + Style.RESET_ALL)
151
+ if str and mode == 'hex':
152
+ print(Fore.CYAN + conv().hex(str, ':') + Style.RESET_ALL)
153
+
154
+ # show recv commands (debug mode)
155
+ def recv(self, str, mode):
156
+ if str:
157
+ print(Back.MAGENTA + str + Style.RESET_ALL)
158
+ if str and mode == 'hex':
159
+ print(Fore.MAGENTA + conv().hex(str, ':') + Style.RESET_ALL)
160
+
161
+ # show information
162
+ def info(self, msg, eol=None):
163
+ if msg:
164
+ print(Back.BLUE + msg + Style.RESET_ALL, end=eol)
165
+ sys.stdout.flush()
166
+
167
+ # show raw data
168
+ def raw(self, msg, eol=None):
169
+ if msg:
170
+ print(Fore.YELLOW + msg + Style.RESET_ALL, end=eol)
171
+ sys.stdout.flush()
172
+
173
+ # show chit-chat
174
+ def chitchat(self, msg, eol=None):
175
+ if msg:
176
+ print(Style.DIM + msg + Style.RESET_ALL, end=eol)
177
+ sys.stdout.flush()
178
+
179
+ # show warning message
180
+ def warning(self, msg):
181
+ if msg:
182
+ print(Back.RED + msg + Style.RESET_ALL)
183
+
184
+ # show green message
185
+ def green(self, msg):
186
+ if msg:
187
+ print(Back.GREEN + msg + Style.RESET_ALL)
188
+
189
+ # show error message
190
+ def errmsg(self, msg, info=""):
191
+ info = str(info).strip()
192
+ if info: # monkeypatch to make python error message less ugly
193
+ info = item(re.findall(r'Errno -?\d+\] (.*)', info),
194
+ '') or info.splitlines()[-1]
195
+ info = Style.RESET_ALL + Style.DIM + \
196
+ " (" + info.strip('<>') + ")" + Style.RESET_ALL
197
+ if msg:
198
+ print(Fore.RED + Style.BRIGHT + msg + info + Style.RESET_ALL)
199
+
200
+ # show printer and status
201
+ def discover(self, entry):
202
+ """Display a discovered printer entry.
203
+
204
+ Accepts either:
205
+ - a 2-tuple (ipaddr, fields_tuple) where fields_tuple has at least
206
+ (device, uptime, status, prstat) as first four elements, or
207
+ - a 2-tuple (label, tuple_of_labels) used as table header.
208
+ """
209
+ ipaddr, fields = entry
210
+ # Normalize: accept tuple with any length, use first 4 fields
211
+ if isinstance(fields, (tuple, list)):
212
+ fields = list(fields)
213
+ else:
214
+ fields = [str(fields)]
215
+ # Pad to at least 4 elements
216
+ while len(fields) < 4:
217
+ fields.append('?')
218
+ device, uptime, status = str(fields[0]), str(fields[1]), str(fields[2])
219
+ prstat = str(fields[3]) if len(fields) > 3 else '?'
220
+
221
+ ipaddr = output().strfit(str(ipaddr), 15)
222
+ device = output().strfit(device, 27)
223
+ uptime = output().strfit(uptime, 8)
224
+ status = output().strfit(status, 23)
225
+ if device.strip() != 'device':
226
+ device = Style.BRIGHT + device + Style.NORMAL
227
+ if prstat == '1':
228
+ status = Back.GREEN + status + Back.BLUE # unknown
229
+ if prstat == '2':
230
+ status = Back.GREEN + status + Back.BLUE # running
231
+ if prstat == '3':
232
+ status = Back.YELLOW + status + Back.BLUE # warning
233
+ if prstat == '4':
234
+ status = Back.GREEN + status + Back.BLUE # testing
235
+ if prstat == '5':
236
+ status = Back.RED + status + Back.BLUE # down
237
+ line = (ipaddr, device, uptime, status)
238
+ output().info('%-15s %-27s %-8s %-23s' % line)
239
+
240
+ # recursively list files
241
+ def psfind(self, name):
242
+ vol = Style.DIM + Fore.YELLOW + \
243
+ item(re.findall("^(%.*%)", name)) + Style.RESET_ALL
244
+ name = Fore.YELLOW + const.SEP + \
245
+ re.sub("^(%.*%)", '', name) + Style.RESET_ALL
246
+ print("%s %s" % (vol, name))
247
+
248
+ # show directory listing
249
+ def psdir(self, isdir, size, mtime, name, otime):
250
+ otime = Style.DIM + "(created " + otime + ")" + Style.RESET_ALL
251
+ vol = Style.DIM + Fore.YELLOW + \
252
+ item(re.findall("^(%.*%)", name)) + Style.RESET_ALL
253
+ # remove volume information from filename
254
+ name = re.sub("^(%.*%)", '', name)
255
+ name = Style.BRIGHT + Fore.BLUE + name + Style.RESET_ALL if isdir else name
256
+ if isdir:
257
+ print("d %8s %s %s %s %s" % (size, mtime, otime, vol, name))
258
+ else:
259
+ print("- %8s %s %s %s %s" % (size, mtime, otime, vol, name))
260
+
261
+ # show directory listing
262
+ def pjldir(self, name, size):
263
+ name = name if size else Style.BRIGHT + Fore.BLUE + name + Style.RESET_ALL
264
+ if size:
265
+ print("- %8s %s" % (size, name))
266
+ else:
267
+ print("d %8s %s" % ("-", name))
268
+
269
+ # show directory listing
270
+ def pcldir(self, size, mtime, id, name):
271
+ id = Style.DIM + "(macro id: " + id + ")" + Style.RESET_ALL
272
+ print("- %8s %s %s %s" % (size, mtime, id, name))
273
+
274
+ # show output from df
275
+ def df(self, args):
276
+ self.info("%-16s %-11s %-11s %-9s %-10s %-8s %-9s %-10s %-10s" % args)
277
+
278
+ # show fuzzing results
279
+ def fuzzed(self, path, cmd, opt):
280
+ opt1, opt2, opt3 = opt
281
+ if isinstance(opt1, bool):
282
+ opt1 = (Back.GREEN + str(opt1) + Back.BLUE + " ")\
283
+ if opt1 else (Back.RED + str(opt1) + Back.BLUE + " ")
284
+ if isinstance(opt2, bool):
285
+ opt2 = (Back.GREEN + str(opt2) + Back.BLUE + " ")\
286
+ if opt2 else (Back.RED + str(opt2) + Back.BLUE + " ")
287
+ if isinstance(opt3, bool):
288
+ opt3 = (Back.GREEN + str(opt3) + Back.BLUE + " ")\
289
+ if opt3 else (Back.RED + str(opt3) + Back.BLUE + " ")
290
+ opt = opt1, opt2, opt3
291
+ self.info("%-35s %-12s %-7s %-7s %-7s" % ((path, cmd) + opt))
292
+
293
+ # show captured jobs
294
+ def joblist(self, xxx_todo_changeme1):
295
+ (date, size, user, name, soft) = xxx_todo_changeme1
296
+ user = output().strfit(user, 13)
297
+ name = output().strfit(name, 22)
298
+ soft = output().strfit(soft, 20)
299
+ line = (date, size, user, name, soft)
300
+ output().info('%-12s %5s %-13s %-22s %-20s' % line)
301
+
302
+ # show ascii only
303
+ def ascii(self, data):
304
+ data = re.sub(r"(\x00){10}", "\x00", data) # shorten nullbyte streams
305
+ data = re.sub(r"([^ -~])", ".", data) # replace non-printable chars
306
+ self.raw(data, "")
307
+
308
+ # show binary dump
309
+ def dump(self, data):
310
+ # experimental regex to match sensitive strings like passwords
311
+ data = re.sub(
312
+ r"[\x00-\x06,\x1e]([!-~]{6,}?(?!\\0A))\x00{16}", "START" + r"\1" + "STOP", data)
313
+ data = re.sub(r"\00+", "\x00", data) # ignore nullbyte streams
314
+ data = re.sub(r"(\x00){10}", "\x00", data) # ignore nullbyte streams
315
+ data = re.sub(r"([\x00-\x1f,\x7f-\xff])", ".", data)
316
+ data = re.sub(r"START([!-~]{6,}?)STOP", Style.RESET_ALL +
317
+ Back.BLUE + r"\1" + Style.RESET_ALL + Fore.YELLOW, data)
318
+ self.raw(data, "")
319
+
320
+ # dump ps dictionary
321
+ def psdict(self, data, indent=''):
322
+ # Python 3 handles UTF-8 natively; no reload needed
323
+ # convert list to dictionary with indices as keys
324
+ if isinstance(data, list):
325
+ data = dict(enumerate(data))
326
+ # data now is expected to be a dictionary
327
+ if len(list(data.keys())) > 0:
328
+ last = sorted(data.keys())[-1]
329
+ for key, val in sorted(data.items()):
330
+ type = val['type'].replace('type', '')
331
+ value = val['value']
332
+ perms = val['perms']
333
+ recursion = False
334
+ # current entry is a dictionary
335
+ if isinstance(value, dict):
336
+ value, recursion = '', True
337
+ # current entry is a ps array
338
+ if isinstance(value, list):
339
+ try: # array contains only atomic values
340
+ value = ' '.join(x['value'] for x in value)
341
+ except: # array contains further list or dict
342
+ # value = sum(val['value'], [])
343
+ value, recursion = '', True
344
+ # value = value.encode('ascii', errors='ignore')
345
+ node = '┬' if recursion else '─'
346
+ edge = indent + ('└' if key == last else '├')
347
+ # output current node in dictionary
348
+ print("%s%s %-3s %-11s %-30s %s" %
349
+ (edge, node, perms, type, key, value))
350
+ if recursion: # ...
351
+ self.psdict(val['value'], indent +
352
+ (' ' if key == last else '│'))
353
+
354
+ # show some information
355
+ def psonly(self):
356
+ self.chitchat(
357
+ "Info: This only affects jobs printed by a PostScript driver")
358
+
359
+ # countdown from sec to zero
360
+ def countdown(self, msg, sec, cmd):
361
+ try:
362
+ sys.stdout.write(msg)
363
+ for x in reversed(list(range(1, sec+1))):
364
+ sys.stdout.write(" " + str(x))
365
+ sys.stdout.flush()
366
+ time.sleep(1)
367
+ print(" KABOOM!")
368
+ return True
369
+ except KeyboardInterrupt:
370
+ print("")
371
+
372
+ # show horizontal line
373
+ def hline(self, len=72):
374
+ self.info("─" * len)
375
+
376
+ # crop/pad string to fixed length
377
+ def strfit(self, str, max):
378
+ str = str.strip() or "-"
379
+ if str.startswith('(') and str.endswith(')'):
380
+ str = str[1:-1]
381
+ # crop long strings
382
+ if len(str) > max:
383
+ str = str[0:max-1] + "…"
384
+ # pad short strings
385
+ return str.ljust(max)
386
+
387
+ # ----------------------------------------------------------------------
388
+
389
+
390
+ class conv():
391
+ # return current time
392
+ def now(self):
393
+ return int(time.time())
394
+
395
+ # return time elapsed since unix epoch
396
+ def elapsed(self, date, div=1, short=False):
397
+ date = str(datetime.timedelta(seconds=int(date)/div))
398
+ return date.split(",")[0] if short else date
399
+
400
+ # return date dependent on current year
401
+ def lsdate(self, date):
402
+ year1 = datetime.datetime.now().year
403
+ year2 = datetime.datetime.fromtimestamp(date).year
404
+ # %e (space-padded day) is POSIX-only; %d (zero-padded) is portable.
405
+ # On Windows, %e is not supported – use %d unconditionally for safety.
406
+ pdate = '%b %d '
407
+ fmt = pdate + "%H:%M" if year1 == year2 else pdate + " %Y"
408
+ return time.strftime(fmt, time.localtime(date))
409
+
410
+ # return date plus/minus given seconds
411
+ def timediff(self, seconds):
412
+ return self.lsdate(self.now() + self.int(seconds) / 1000)
413
+
414
+ # convert size to human readable value
415
+ def filesize(self, num):
416
+ num = self.int(num)
417
+ for unit in ['B', 'K', 'M']:
418
+ if abs(num) < 1024.0:
419
+ return (("%4.1f%s" if unit == 'M' else "%4.0f%s") % (num, unit))
420
+ num /= 1024.0
421
+
422
+ # remove carriage return from line breaks
423
+ def nstrip(self, data):
424
+ return re.sub(r'\r\n', '\n', data)
425
+
426
+ # convert string to hexadecimal
427
+ def hex(self, data, sep=''):
428
+ return sep.join("{:02x}".format(ord(c)) for c in data)
429
+
430
+ # convert to ascii character
431
+ def chr(self, num):
432
+ return chr(self.int(num))
433
+
434
+ # convert to integer or zero
435
+ def int(self, num):
436
+ try:
437
+ n = int(num)
438
+ except ValueError:
439
+ n = 0
440
+ return n
441
+
442
+ # ----------------------------------------------------------------------
443
+
444
+
445
+ class file():
446
+ # read from local file
447
+ def read(self, path):
448
+ try:
449
+ with open(path, mode='rb') as f:
450
+ data = f.read()
451
+ f.close()
452
+ return data
453
+ except IOError as e:
454
+ output().errmsg("Cannot read from file", e)
455
+
456
+ # write to local file
457
+ def write(self, path, data, m='wb'):
458
+ try:
459
+ with open(path, mode=m) as f:
460
+ f.write(data)
461
+ f.close()
462
+ except IOError as e:
463
+ output().errmsg("Cannot write to file", e)
464
+
465
+ # append to local file
466
+ def append(self, path, data):
467
+ # Ensure data is in bytes format
468
+ if isinstance(data, str):
469
+ data = data.encode('utf-8')
470
+ self.write(path, data, 'ab+')
471
+
472
+ # ----------------------------------------------------------------------
473
+
474
+
475
+ class conn(object):
476
+ # create debug connection object
477
+ def __init__(self, mode, debug, quiet):
478
+ self.mode = mode
479
+ self.debug = debug
480
+ self.quiet = quiet
481
+ self._file = None
482
+ self._sock = socket()
483
+
484
+ # open connection
485
+ def open(self, target, port=9100):
486
+ try:
487
+ # target is a character device
488
+ if os.path.exists(target) \
489
+ and stat.S_ISCHR(os.stat(target).st_mode):
490
+ self._file = os.open(target, os.O_RDWR)
491
+ return self
492
+ # treat target as ipv4 socket
493
+ else:
494
+ m = re.search('^(.+?):([0-9]+)$', target)
495
+ if m:
496
+ [target, port] = m.groups()
497
+ port = int(port)
498
+ # If no port specified in target, use default port parameter
499
+
500
+ self._sock.connect((target, port))
501
+ return self
502
+ except Exception as e:
503
+ output().errmsg(f"Failed to connect to {target}: {str(e)}")
504
+ return None
505
+
506
+ # close connection
507
+ def close(self, *arg):
508
+ # close file descriptor
509
+ if self._file:
510
+ os.close(self._file)
511
+ # close inet socket
512
+ else:
513
+ self._sock.close()
514
+
515
+ # set timeout
516
+ def timeout(self, *arg):
517
+ self._sock.settimeout(*arg)
518
+
519
+ # send data
520
+ def send(self, data):
521
+ if self.debug:
522
+ output().send(self.beautify(data), self.debug)
523
+ # send data to device
524
+ if self._file:
525
+ return os.write(self._file, data)
526
+ # send data to socket
527
+ elif self._sock:
528
+ if not isinstance(data, bytes):
529
+ data = data.encode()
530
+ try:
531
+ return self._sock.sendall(data)
532
+ except (ConnectionResetError, BrokenPipeError, OSError) as e:
533
+ output().errmsg("Connection lost while sending data")
534
+ return None
535
+ except Exception as e:
536
+ output().errmsg(f"Send error: {str(e)}")
537
+ return None
538
+
539
+ # receive data
540
+ def recv(self, bytes):
541
+ # receive data from device
542
+ if self._file:
543
+ data = os.read(self._file, bytes).decode()
544
+ # receive data from socket
545
+ else:
546
+ try:
547
+ data = self._sock.recv(bytes).decode()
548
+ except (ConnectionResetError, BrokenPipeError, OSError) as e:
549
+ output().errmsg("Connection lost while receiving data")
550
+ return ""
551
+ except Exception as e:
552
+ output().errmsg(f"Receive error: {str(e)}")
553
+ return ""
554
+ # output recv data when in debug mode
555
+ if self.debug:
556
+ output().recv(self.beautify(data), self.debug)
557
+ return data
558
+
559
+ # so-many-seconds-passed bool condition
560
+ def past(self, seconds, watchdog):
561
+ return int(watchdog * 100) % (seconds * 100) == 0
562
+
563
+ # connection-feels-slow bool condition
564
+ def slow(self, limit, watchdog):
565
+ return not (self.quiet or self.debug) and watchdog > limit
566
+
567
+ # receive data until a delimiter is reached
568
+ def recv_until(self, delimiter, fb=True, crop=True, binary=False):
569
+ data = ""
570
+ sleep = 0.01 # pause in recv loop
571
+ limit = 3.0 # max watchdog overrun
572
+ wd = 0.0 # watchdog timeout counter
573
+ max_timeout = 30.0 # maximum timeout in seconds
574
+ r = re.compile(delimiter, re.DOTALL)
575
+ s = re.compile("^\x04?\x0d?\x0a?" + delimiter, re.DOTALL)
576
+ wd_old = 0
577
+ bytes_received = 0
578
+
579
+ # Set socket timeout to prevent infinite blocking (30 seconds as requested)
580
+ if hasattr(self, '_sock') and self._sock:
581
+ self._sock.settimeout(30.0) # 30 second timeout
582
+
583
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
584
+ while not r.search(data):
585
+ # Check for maximum timeout
586
+ if wd > max_timeout:
587
+ output().errmsg("Maximum timeout reached", "timeout exceeded")
588
+ break
589
+
590
+ try:
591
+ new_data = self.recv(4096) # receive actual data
592
+ if new_data is None or new_data == "":
593
+ # No data received, check if we should continue waiting
594
+ if wd > limit and bytes_received == 0:
595
+ output().errmsg("No data received within timeout period")
596
+ break
597
+ time.sleep(sleep)
598
+ wd += sleep
599
+ continue
600
+
601
+ data += new_data
602
+ bytes_received = len(data)
603
+
604
+ except socket_module.timeout:
605
+ output().errmsg("Socket timeout while receiving data")
606
+ break
607
+ except Exception as e:
608
+ output().errmsg(f"Error receiving data: {str(e)}")
609
+ break
610
+
611
+ if self.past(limit, wd):
612
+ wd_old, bytes_received = wd, len(data)
613
+ wd += sleep # workaround for endless loop w/o socket timeout
614
+ time.sleep(sleep) # happens on some devices - python socket error?
615
+ # timeout plus it seems we are not receiving data anymore
616
+ if hasattr(self, '_sock') and self._sock and wd > self._sock.gettimeout() and wd >= wd_old + limit:
617
+ if len(data) == bytes_received:
618
+ output().errmsg("Receiving data failed", "watchdog timeout")
619
+ break
620
+ # visual feedback on large/slow data transfers
621
+ if self.slow(limit, wd) and self.past(0.1, wd) and len(data) > 0:
622
+ output().chitchat(str(len(data)) + " bytes received\r", '')
623
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
624
+ # clear current line from 'so-many bytes received' chit-chat
625
+ if self.slow(limit, wd):
626
+ output().chitchat(' ' * 24 + "\r", '')
627
+ # warn if feedback expected but response empty (= delimiter only)
628
+ # this also happens for data received out of order (e.g. brother)
629
+ if fb and s.search(data):
630
+ output().chitchat("No data received.")
631
+ # remove delimiter itself from data
632
+ if crop:
633
+ data = r.sub('', data)
634
+ # crop uel sequence at the beginning
635
+ data = re.sub(r'(^' + const.UEL + ')', '', data)
636
+ '''
637
+ ┌─────────────────────────────────────────────────────────────────────────┐
638
+ │ delimiters -- note that carriage return (0d) is optional in ps/pjl │
639
+ ├─────────────────────────┬─────────────────────────┬─────────────────────┤
640
+ │ │ PJL │ PostScript │
641
+ ├─────────────────────────┼─────────┬───────────────┼────────┬────────────┤
642
+ │ │ send │ recv │ send │ recv │
643
+ ├─────────────────────────┼─────────┼───────────────┼────────┼────────────┤
644
+ │ normal commands (ascii) │ 0d? 0a │ 0d+ 0a 0c 04? │ 0d? 0a │ 0d? 0a 04? │
645
+ ├─────────────────────────┼─────────┼───────────────┼────────┼────────────┤
646
+ │ file transfers (binary) │ 0d? 0a │ 0c │ 0d? 0a │ - │
647
+ └─────────────────────────┴─────────┴───────────────┴────────┴────────────┘
648
+ '''
649
+ # crop end-of-transmission chars
650
+ if self.mode == 'ps':
651
+ data = re.sub(r'^\x04', '', data)
652
+ if not binary:
653
+ data = re.sub(r'\x0d?\x0a\x04?$', '', data)
654
+ else: # pjl and pcl mode
655
+ if binary:
656
+ data = re.sub(r'\x0c$', '', data)
657
+ else:
658
+ data = re.sub(r'\x0d+\x0a\x0c\x04?$', '', data)
659
+ # crop whitespaces/newline as feedback
660
+ if not binary:
661
+ data = data.strip()
662
+ return data
663
+
664
+ # beautify debug output
665
+ def beautify(self, data):
666
+ # remove sent/recv uel sequences
667
+ """if isinstance(data, bytes):
668
+ try:
669
+ data = data.decode()
670
+ except UnicodeDecodeError:
671
+ data = "[PDF bytes]"""
672
+ data = re.sub(r'' + const.UEL, '', str(data))
673
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
674
+ if self.mode == 'ps':
675
+ # remove sent postscript header
676
+ data = re.sub(r'' + re.escape(const.PS_HEADER), '', data)
677
+ # remove sent postscript hack
678
+ data = re.sub(r'' + re.escape(const.PS_IOHACK), '', data)
679
+ # remove sent delimiter token
680
+ data = re.sub(r'\(DELIMITER\d+\\n\) print flush\n', '', data)
681
+ # remove recv delimiter token
682
+ data = re.sub(r'DELIMITER\d+', '', data)
683
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
684
+ elif self.mode == 'pjl':
685
+ # remove sent/recv delimiter token
686
+ data = re.sub(r'@PJL ECHO\s+DELIMITER\d+', '', data)
687
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
688
+ elif self.mode == 'pcl':
689
+ # remove sent delimiter token
690
+ data = re.sub(r'\x1b\*s-\d+X', '', data)
691
+ # remove recv delimiter token
692
+ data = re.sub(r'PCL\x0d?\x0a?\x0c?ECHO -\d+', '', data)
693
+ # replace sent escape sequences
694
+ data = re.sub(r'(' + const.ESC + ')', '<Esc>', data)
695
+ pass
696
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
697
+ # replace lineseps in between
698
+ data = re.sub(r'\x0d?\x0a?\x0c', os.linesep, data)
699
+ # remove eot/eof sequences
700
+ data = data.strip(const.EOF)
701
+ return data
702
+
703
+ # ----------------------------------------------------------------------
704
+
705
+
706
+ class const(): # define constants
707
+ SEP = '/' # use posixoid path separator
708
+ EOL = '\r\n' # line feed || carriage return
709
+ ESC = '\x1b' # used to start escape sequences
710
+ UEL = ESC + '%-12345X' # universal exit language
711
+ EOF = EOL + '\x0c\x04' # potential end of file chars
712
+ DELIMITER = "DELIMITER" # delimiter marking end of response
713
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
714
+ PS_CATCH = r'%%\[ (.*)\]%%'
715
+ PS_ERROR = r'%%\[ Error: (.*)\]%%'
716
+ PS_FLUSH = r'%%\[ Flushing: (.*)\]%%'
717
+ PS_PROMPT = '>' # TBD: could be derived from PS command 'prompt'
718
+ PS_HEADER = '@PJL ENTER LANGUAGE = POSTSCRIPT\n%!\n'
719
+ PS_GLOBAL = 'true 0 startjob pop\n' # 'serverdict begin 0 exitserver'
720
+ PS_SUPER = '\n1183615869 internaldict /superexec get exec'
721
+ PS_NOHOOK = '/nohook true def\n'
722
+ PS_IOHACK = '/print {(%stdout) (w) file dup 3 2 roll writestring flushfile} def\n'\
723
+ '/== {128 string cvs print (\\n) print} def\n'
724
+ PCL_HEADER = '@PJL ENTER LANGUAGE = PCL' + EOL + ESC
725
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
726
+ SUPERBLOCK = '31337' # define super macro id to contain pclfs table
727
+ BLOCKRANGE = list(range(10000, 20000)) # use those macros for file content
728
+ FILE_EXISTS = -1 # file size to be returned if file/dir size unknown
729
+ NONEXISTENT = -2 # file size to be returned if a file does not exist
730
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
731
+ PS_VOL = '' # no default volume in ps (read: any, write: first)
732
+ PJL_VOL = '0:' + SEP # default pjl volume name || path separator