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.
- nse/README.md +204 -0
- nse/__init__.py +6 -0
- nse/install_nse.py +412 -0
- nse/lib/printerxpl.lua +238 -0
- nse/scripts/cups-info.nse +74 -0
- nse/scripts/cups-queue-info.nse +43 -0
- nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
- nse/scripts/http-device-mac.nse +107 -0
- nse/scripts/http-hp-ilo-info.nse +121 -0
- nse/scripts/http-info-xerox-enum.nse +101 -0
- nse/scripts/http-vuln-cve2022-1026.nse +158 -0
- nse/scripts/lexmark-config.nse +89 -0
- nse/scripts/pjl-ready-message.nse +106 -0
- nse/scripts/printer-banner.nse +217 -0
- nse/scripts/printer-cups-rce.nse +189 -0
- nse/scripts/printer-cve-detect.nse +279 -0
- nse/scripts/printer-discover.nse +205 -0
- nse/scripts/printer-firmware-exposed.nse +219 -0
- nse/scripts/printer-hp-pjl.nse +192 -0
- nse/scripts/printer-http-ews.nse +293 -0
- nse/scripts/printer-ipp-info.nse +235 -0
- nse/scripts/printer-lexmark-ipp.nse +203 -0
- nse/scripts/printer-passback.nse +204 -0
- nse/scripts/printer-pjl-info.nse +146 -0
- nse/scripts/printer-printnightmare.nse +211 -0
- nse/scripts/printer-snmp-info.nse +176 -0
- nse/scripts/printer-vuln-check.nse +256 -0
- nse/scripts/snmp-device-mac.nse +93 -0
- nse/scripts/snmp-info.nse +146 -0
- nse/scripts/snmp-sysdescr.nse +70 -0
- printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
- printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
- printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
- printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
- printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
- printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
- src/assets/fonts/gunplay.pfa +1671 -0
- src/assets/fonts/kshandwrt.pfa +315 -0
- src/assets/fonts/laksoner.pfa +2402 -0
- src/assets/fonts/paintcans.pfa +9699 -0
- src/assets/fonts/stencilod.pfa +4076 -0
- src/assets/fonts/takecover.pfa +26138 -0
- src/assets/fonts/topsecret.pfa +6652 -0
- src/assets/fonts/whoa.pfa +773 -0
- src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
- src/assets/mibs/Printer-MIB +4389 -0
- src/assets/mibs/README.md +9 -0
- src/assets/mibs/SNMPv2-MIB +854 -0
- src/assets/overlays/hacker.eps +596 -0
- src/assets/overlays/smiley.eps +214 -0
- src/assets/overlays/smiley2.eps +240 -0
- src/core/attack_orchestrator.py +1025 -0
- src/core/capabilities.py +323 -0
- src/core/destructive_audit.py +430 -0
- src/core/discovery.py +488 -0
- src/core/osdetect.py +74 -0
- src/core/poly_runner.py +579 -0
- src/core/printer.py +1426 -0
- src/main.py +2134 -0
- src/modules/install_printer.py +318 -0
- src/modules/login_bruteforce.py +852 -0
- src/modules/pcl.py +506 -0
- src/modules/pjl.py +3575 -0
- src/modules/print_job.py +1290 -0
- src/modules/ps.py +1102 -0
- src/payloads/__init__.py +98 -0
- src/payloads/assets/overlays/notice.eps +9 -0
- src/protocols/__init__.py +19 -0
- src/protocols/firmware.py +738 -0
- src/protocols/ipp.py +216 -0
- src/protocols/ipp_attacks.py +609 -0
- src/protocols/lpd.py +141 -0
- src/protocols/network_map.py +1004 -0
- src/protocols/raw.py +173 -0
- src/protocols/smb.py +359 -0
- src/protocols/ssrf_pivot.py +427 -0
- src/protocols/storage.py +587 -0
- src/ui/__init__.py +6 -0
- src/ui/interactive.py +742 -0
- src/ui/spinner.py +112 -0
- src/ui/tables.py +132 -0
- src/utils/banner_grabber.py +852 -0
- src/utils/codebook.py +456 -0
- src/utils/config.py +522 -0
- src/utils/cve_loader.py +158 -0
- src/utils/default_creds.py +134 -0
- src/utils/discovery_online.py +1327 -0
- src/utils/exploit_manager.py +805 -0
- src/utils/fuzzer.py +220 -0
- src/utils/helper.py +732 -0
- src/utils/local_printers.py +307 -0
- src/utils/ml_engine.py +491 -0
- src/utils/operators.py +474 -0
- src/utils/ports.py +234 -0
- src/utils/vuln_scanner.py +823 -0
- src/utils/wordlist_loader.py +412 -0
- 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
|