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/core/printer.py
ADDED
|
@@ -0,0 +1,1426 @@
|
|
|
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
|
+
# python standard library
|
|
11
|
+
import re
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import cmd
|
|
15
|
+
import glob
|
|
16
|
+
import errno
|
|
17
|
+
import random
|
|
18
|
+
import ntpath
|
|
19
|
+
import posixpath
|
|
20
|
+
import hashlib
|
|
21
|
+
import socket
|
|
22
|
+
import tempfile
|
|
23
|
+
import subprocess
|
|
24
|
+
import traceback
|
|
25
|
+
try:
|
|
26
|
+
import requests
|
|
27
|
+
except ImportError:
|
|
28
|
+
requests = None
|
|
29
|
+
import time
|
|
30
|
+
import signal
|
|
31
|
+
import threading
|
|
32
|
+
|
|
33
|
+
# local pret classes
|
|
34
|
+
from utils.helper import log, output, conv, file, item, conn, const as c
|
|
35
|
+
from core.discovery import discovery
|
|
36
|
+
from utils.fuzzer import fuzzer
|
|
37
|
+
# CVE module moved to backup - functionality integrated into PJL v2.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _detect_editor() -> str:
|
|
41
|
+
"""Return the best available text editor for the current OS."""
|
|
42
|
+
import shutil as _shutil
|
|
43
|
+
import sys as _sys
|
|
44
|
+
|
|
45
|
+
if _sys.platform == 'win32':
|
|
46
|
+
# Windows: prefer notepad (always present) or VS Code
|
|
47
|
+
for candidate in ('code', 'notepad++', 'notepad'):
|
|
48
|
+
if _shutil.which(candidate):
|
|
49
|
+
return candidate
|
|
50
|
+
return 'notepad'
|
|
51
|
+
|
|
52
|
+
# POSIX: Linux, macOS, WSL, Android/Termux, BSD
|
|
53
|
+
# Respect VISUAL / EDITOR environment variables first
|
|
54
|
+
for env_var in ('VISUAL', 'EDITOR'):
|
|
55
|
+
val = os.environ.get(env_var, '').strip()
|
|
56
|
+
if val and _shutil.which(val.split()[0]):
|
|
57
|
+
return val
|
|
58
|
+
|
|
59
|
+
# Fallback preference list (ordered)
|
|
60
|
+
for candidate in ('nano', 'micro', 'vim', 'vi', 'emacs', 'pico'):
|
|
61
|
+
if _shutil.which(candidate):
|
|
62
|
+
return candidate
|
|
63
|
+
|
|
64
|
+
return 'vi' # POSIX mandates vi
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class printer(cmd.Cmd, object):
|
|
68
|
+
# cmd module config and customization
|
|
69
|
+
intro = "Welcome to the PrinterXPL-Forge shell. Type help or ? to list commands.\nType 'exit' to quit. Type 'discover' to scan for printers on your local network.\nNote: Not all commands will work on every printer — support depends on the device's manufacturer, model, and firmware language implementation."
|
|
70
|
+
doc_header = "Available commands (type help <topic>):"
|
|
71
|
+
offline_str = "Not connected."
|
|
72
|
+
undoc_header = None
|
|
73
|
+
|
|
74
|
+
logfile = None
|
|
75
|
+
debug = False
|
|
76
|
+
status = False
|
|
77
|
+
quiet = False
|
|
78
|
+
fuzz = False
|
|
79
|
+
conn = None
|
|
80
|
+
mode = None
|
|
81
|
+
error = None
|
|
82
|
+
iohack = True
|
|
83
|
+
timeout = 30 # Increased to 30 seconds as requested
|
|
84
|
+
max_retries = 3
|
|
85
|
+
target = ""
|
|
86
|
+
vol = ""
|
|
87
|
+
cwd = ""
|
|
88
|
+
traversal = ""
|
|
89
|
+
# default editor – auto-detected per OS, can be overridden at runtime
|
|
90
|
+
editor = _detect_editor()
|
|
91
|
+
|
|
92
|
+
# Interruption control
|
|
93
|
+
interrupted = False
|
|
94
|
+
current_command = None
|
|
95
|
+
command_thread = None
|
|
96
|
+
should_exit = False # Flag to indicate if shell should exit
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------
|
|
99
|
+
# INITIALIZATION AND CONTROL METHODS
|
|
100
|
+
# --------------------------------------------------------------------
|
|
101
|
+
def __init__(self, args):
|
|
102
|
+
# init cmd module
|
|
103
|
+
cmd.Cmd.__init__(self)
|
|
104
|
+
self.debug = args.debug # debug mode
|
|
105
|
+
self.quiet = args.quiet # quiet mode
|
|
106
|
+
self.mode = args.mode # command mode
|
|
107
|
+
|
|
108
|
+
# Setup signal handlers for graceful interruption
|
|
109
|
+
self.setup_signal_handlers()
|
|
110
|
+
|
|
111
|
+
# connect to device (skip if target is test)
|
|
112
|
+
if args.target != "test":
|
|
113
|
+
self.do_open(args.target, "init")
|
|
114
|
+
# log pjl/ps cmds to file
|
|
115
|
+
if args.log:
|
|
116
|
+
self.logfile = log().open(args.log)
|
|
117
|
+
header = None
|
|
118
|
+
if self.mode == "ps":
|
|
119
|
+
header = c.PS_HEADER
|
|
120
|
+
if self.mode == "pcl":
|
|
121
|
+
header = c.PCL_HEADER
|
|
122
|
+
if header:
|
|
123
|
+
log().write(self.logfile, header + os.linesep)
|
|
124
|
+
# run pret cmds from file
|
|
125
|
+
if args.load:
|
|
126
|
+
self.do_load(args.load)
|
|
127
|
+
# Mark that shell should exit after loading commands
|
|
128
|
+
self.should_exit = True
|
|
129
|
+
|
|
130
|
+
def setup_signal_handlers(self):
|
|
131
|
+
"""Setup signal handlers for graceful interruption"""
|
|
132
|
+
def signal_handler(signum, frame):
|
|
133
|
+
output().info("\nExiting PrinterXPL-Forge...")
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
# Handle SIGINT (Ctrl+C) and SIGTERM
|
|
137
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
138
|
+
if hasattr(signal, 'SIGTERM'):
|
|
139
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def set_defaults(self, newtarget):
|
|
144
|
+
self.fuzz = False
|
|
145
|
+
if newtarget:
|
|
146
|
+
self.set_vol()
|
|
147
|
+
self.set_traversal()
|
|
148
|
+
self.error = None
|
|
149
|
+
self.set_prompt() # Set prompt after connecting
|
|
150
|
+
else:
|
|
151
|
+
self.set_prompt()
|
|
152
|
+
|
|
153
|
+
# --------------------------------------------------------------------
|
|
154
|
+
# CORE SHELL METHODS
|
|
155
|
+
# --------------------------------------------------------------------
|
|
156
|
+
# do nothing on empty input
|
|
157
|
+
def emptyline(self):
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# show message for unknown commands
|
|
161
|
+
def default(self, line):
|
|
162
|
+
if line and line[0] != "#": # interpret as comment
|
|
163
|
+
# Don't show error for EOF
|
|
164
|
+
if line.strip() != "EOF":
|
|
165
|
+
output().chitchat("Unknown command: '" + line + "'")
|
|
166
|
+
|
|
167
|
+
# suppress help message for undocumented commands
|
|
168
|
+
def print_topics(self, header, cmds, cmdlen, maxcol):
|
|
169
|
+
if cmds:
|
|
170
|
+
output().chitchat(header)
|
|
171
|
+
|
|
172
|
+
def chitchat(self, *args):
|
|
173
|
+
if not self.quiet:
|
|
174
|
+
output().message(*args)
|
|
175
|
+
|
|
176
|
+
def precmd(self, line):
|
|
177
|
+
# normalize line endings
|
|
178
|
+
if line and line[-1] == os.linesep:
|
|
179
|
+
line = line[:-1]
|
|
180
|
+
# convert to unicode if needed
|
|
181
|
+
if hasattr(line, "decode"):
|
|
182
|
+
try:
|
|
183
|
+
line = line.decode("utf-8")
|
|
184
|
+
except UnicodeDecodeError:
|
|
185
|
+
line = line.decode("latin-1")
|
|
186
|
+
return line
|
|
187
|
+
|
|
188
|
+
# --------------------------------------------------------------------
|
|
189
|
+
# catch-all wrapper to guarantee continuation on unhandled exceptions
|
|
190
|
+
def onecmd(self, line):
|
|
191
|
+
# Set current command for interruption tracking
|
|
192
|
+
self.current_command = line.strip()
|
|
193
|
+
self.interrupted = False
|
|
194
|
+
|
|
195
|
+
# Set timeout for command execution
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
max_execution_time = 30 # 30 seconds timeout
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Execute command with timeout monitoring
|
|
201
|
+
result = cmd.Cmd.onecmd(self, line)
|
|
202
|
+
|
|
203
|
+
# Check if command took too long
|
|
204
|
+
execution_time = time.time() - start_time
|
|
205
|
+
if execution_time > max_execution_time:
|
|
206
|
+
output().warning(f"Command took {execution_time:.2f} seconds (timeout: {max_execution_time}s)")
|
|
207
|
+
|
|
208
|
+
# Clear current command on successful completion
|
|
209
|
+
self.current_command = None
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
except (ConnectionResetError, BrokenPipeError, OSError) as e:
|
|
213
|
+
output().errmsg("Connection lost - printer may have disconnected")
|
|
214
|
+
self.current_command = None
|
|
215
|
+
return False
|
|
216
|
+
except socket.timeout as e:
|
|
217
|
+
output().errmsg("Command timed out - printer may be busy")
|
|
218
|
+
self.current_command = None
|
|
219
|
+
return False
|
|
220
|
+
except KeyboardInterrupt:
|
|
221
|
+
output().warning("Command interrupted by user")
|
|
222
|
+
self.current_command = None
|
|
223
|
+
return False
|
|
224
|
+
except Exception as e:
|
|
225
|
+
# Only show traceback in debug mode
|
|
226
|
+
if hasattr(self, 'debug') and self.debug:
|
|
227
|
+
traceback.print_exc()
|
|
228
|
+
output().errmsg(f"Command failed: {str(e)}")
|
|
229
|
+
self.current_command = None
|
|
230
|
+
return False
|
|
231
|
+
finally:
|
|
232
|
+
# Always clear current command
|
|
233
|
+
self.current_command = None
|
|
234
|
+
|
|
235
|
+
# --------------------------------------------------------------------
|
|
236
|
+
# HELP SYSTEM (generic fallback)
|
|
237
|
+
# --------------------------------------------------------------------
|
|
238
|
+
def do_help(self, arg):
|
|
239
|
+
"""Show help for commands (generic fallback for non-PJL shells)"""
|
|
240
|
+
# If a specific topic was requested, try dedicated help_<topic> first
|
|
241
|
+
topic = (arg or "").strip()
|
|
242
|
+
if topic:
|
|
243
|
+
method = getattr(self, f"help_{topic}", None)
|
|
244
|
+
if callable(method):
|
|
245
|
+
method()
|
|
246
|
+
return
|
|
247
|
+
# Fallback to showing the do_<topic> docstring if present
|
|
248
|
+
do_method = getattr(self, f"do_{topic}", None)
|
|
249
|
+
if callable(do_method) and (do_method.__doc__ or "").strip():
|
|
250
|
+
print()
|
|
251
|
+
print(f"{topic} - {do_method.__doc__}")
|
|
252
|
+
print()
|
|
253
|
+
return
|
|
254
|
+
output().errmsg(f"No help available for '{topic}'")
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# No topic: dynamically list all available do_* commands for this shell
|
|
258
|
+
names = self.get_names()
|
|
259
|
+
commands = sorted({n[3:] for n in names if n.startswith("do_") and not n.startswith("do__")})
|
|
260
|
+
|
|
261
|
+
print()
|
|
262
|
+
title_mode = (self.mode or "shell").upper()
|
|
263
|
+
print(f"{title_mode} Commands - Available ({len(commands)})")
|
|
264
|
+
print("=" * 70)
|
|
265
|
+
|
|
266
|
+
# Pretty-print in columns
|
|
267
|
+
if not commands:
|
|
268
|
+
print("No commands available.")
|
|
269
|
+
print()
|
|
270
|
+
print("Use 'help <command>' for details.")
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
col_width = max(len(cmd) for cmd in commands) + 2
|
|
274
|
+
cols = max(1, 70 // col_width)
|
|
275
|
+
for i in range(0, len(commands), cols):
|
|
276
|
+
row = commands[i:i+cols]
|
|
277
|
+
print("".join(cmd.ljust(col_width) for cmd in row))
|
|
278
|
+
print()
|
|
279
|
+
print("Use 'help <command>' for details. Example: help download")
|
|
280
|
+
print()
|
|
281
|
+
|
|
282
|
+
# ====================================================================
|
|
283
|
+
# BASIC CONTROL COMMANDS
|
|
284
|
+
# ====================================================================
|
|
285
|
+
# These commands control the basic operation of the shell and program
|
|
286
|
+
|
|
287
|
+
def do_exit(self, *arg):
|
|
288
|
+
if self.logfile:
|
|
289
|
+
log().close(self.logfile)
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
def help_exit(self):
|
|
293
|
+
"""Exit the PrinterXPL-Forge shell"""
|
|
294
|
+
print()
|
|
295
|
+
print("exit - Exit the PrinterXPL-Forge shell")
|
|
296
|
+
print("=" * 50)
|
|
297
|
+
print("DESCRIPTION:")
|
|
298
|
+
print(" Exits the PrinterXPL-Forge shell and closes the connection to the printer.")
|
|
299
|
+
print(" This command will also close any open log files.")
|
|
300
|
+
print()
|
|
301
|
+
print("USAGE:")
|
|
302
|
+
print(" exit")
|
|
303
|
+
print()
|
|
304
|
+
print("EXAMPLES:")
|
|
305
|
+
print(" exit # Exit the shell")
|
|
306
|
+
print(" quit # Alternative command (same as exit)")
|
|
307
|
+
print()
|
|
308
|
+
print("NOTES:")
|
|
309
|
+
print(" - All unsaved work will be lost")
|
|
310
|
+
print(" - Connection to printer will be closed")
|
|
311
|
+
print(" - Log files will be properly closed")
|
|
312
|
+
print()
|
|
313
|
+
|
|
314
|
+
def do_debug(self, arg):
|
|
315
|
+
"Toggle raw traffic debug output"
|
|
316
|
+
self.debug = not self.debug
|
|
317
|
+
output().message("Debug output " + ("enabled" if self.debug else "disabled"))
|
|
318
|
+
|
|
319
|
+
def help_debug(self):
|
|
320
|
+
"""Toggle debug output mode"""
|
|
321
|
+
print()
|
|
322
|
+
print("debug - Toggle debug output mode")
|
|
323
|
+
print("=" * 50)
|
|
324
|
+
print("DESCRIPTION:")
|
|
325
|
+
print(" Enables or disables debug output mode. When enabled, shows")
|
|
326
|
+
print(" detailed information about commands being sent to and")
|
|
327
|
+
print(" received from the printer. Useful for troubleshooting connection issues.")
|
|
328
|
+
print()
|
|
329
|
+
print("USAGE:")
|
|
330
|
+
print(" debug")
|
|
331
|
+
print()
|
|
332
|
+
print("EXAMPLES:")
|
|
333
|
+
print(" debug # Toggle debug mode on/off")
|
|
334
|
+
print()
|
|
335
|
+
print("NOTES:")
|
|
336
|
+
print(" - Debug mode shows raw PJL/PS/PCL commands")
|
|
337
|
+
print(" - Useful for understanding printer communication")
|
|
338
|
+
print(" - May produce verbose output")
|
|
339
|
+
print()
|
|
340
|
+
|
|
341
|
+
def do_loop(self, arg):
|
|
342
|
+
"Run a command repeatedly over multiple arguments"
|
|
343
|
+
if not arg:
|
|
344
|
+
output().errmsg("Usage: loop <command> <arg1> <arg2> ...")
|
|
345
|
+
return
|
|
346
|
+
args = arg.split()
|
|
347
|
+
cmd = args[0]
|
|
348
|
+
for arg in args[1:]:
|
|
349
|
+
output().message(f"Running: {cmd} {arg}")
|
|
350
|
+
self.onecmd(f"{cmd} {arg}")
|
|
351
|
+
|
|
352
|
+
def help_loop(self):
|
|
353
|
+
"""Run a command repeatedly over multiple arguments"""
|
|
354
|
+
print()
|
|
355
|
+
print("loop - Run a command repeatedly over multiple arguments")
|
|
356
|
+
print("=" * 50)
|
|
357
|
+
print("DESCRIPTION:")
|
|
358
|
+
print(" Executes a command multiple times, once for each argument provided.")
|
|
359
|
+
print(" Useful for batch operations and automation tasks.")
|
|
360
|
+
print()
|
|
361
|
+
print("USAGE:")
|
|
362
|
+
print(" loop <command> <arg1> <arg2> ...")
|
|
363
|
+
print()
|
|
364
|
+
print("EXAMPLES:")
|
|
365
|
+
print(" loop download file1.txt file2.txt file3.txt")
|
|
366
|
+
print(" loop delete old1.log old2.log old3.log")
|
|
367
|
+
print(" loop cat config1.cfg config2.cfg")
|
|
368
|
+
print()
|
|
369
|
+
print("NOTES:")
|
|
370
|
+
print(" - Each argument is passed to the command separately")
|
|
371
|
+
print(" - Useful for batch file operations")
|
|
372
|
+
print(" - Commands are executed in sequence")
|
|
373
|
+
print()
|
|
374
|
+
|
|
375
|
+
# ====================================================================
|
|
376
|
+
# DISCOVERY AND CONNECTION COMMANDS
|
|
377
|
+
# ====================================================================
|
|
378
|
+
# These commands handle network discovery and establishing connections
|
|
379
|
+
|
|
380
|
+
def do_discover(self, arg):
|
|
381
|
+
"Scan local networks for SNMP printers"
|
|
382
|
+
discovery(usage=False)
|
|
383
|
+
|
|
384
|
+
def help_discover(self):
|
|
385
|
+
"""Scan local networks for SNMP printers"""
|
|
386
|
+
print()
|
|
387
|
+
print("discover - Scan local networks for SNMP printers")
|
|
388
|
+
print("=" * 50)
|
|
389
|
+
print("DESCRIPTION:")
|
|
390
|
+
print(" Scans the local network for printers using SNMP protocol.")
|
|
391
|
+
print(" Discovers printers on the network and displays their information.")
|
|
392
|
+
print()
|
|
393
|
+
print("USAGE:")
|
|
394
|
+
print(" discover")
|
|
395
|
+
print()
|
|
396
|
+
print("EXAMPLES:")
|
|
397
|
+
print(" discover # Scan local network for printers")
|
|
398
|
+
print()
|
|
399
|
+
print("NOTES:")
|
|
400
|
+
print(" - Requires SNMP to be enabled on target printers")
|
|
401
|
+
print(" - Scans common printer ports (9100, 515, 631)")
|
|
402
|
+
print(" - May take some time depending on network size")
|
|
403
|
+
print(" - Results show IP address, model, and capabilities")
|
|
404
|
+
print()
|
|
405
|
+
|
|
406
|
+
def do_open(self, arg, mode=""):
|
|
407
|
+
"Connect to a new target"
|
|
408
|
+
if not arg:
|
|
409
|
+
arg = input("Target: ")
|
|
410
|
+
self.target = arg
|
|
411
|
+
self.conn = conn(self.mode, self.debug, self.quiet).open(arg)
|
|
412
|
+
if self.conn:
|
|
413
|
+
self.on_connect(mode)
|
|
414
|
+
self.set_defaults(True)
|
|
415
|
+
else:
|
|
416
|
+
self.set_defaults(False)
|
|
417
|
+
|
|
418
|
+
def help_open(self):
|
|
419
|
+
"""Connect to a new printer target"""
|
|
420
|
+
print()
|
|
421
|
+
print("open - Connect to a new printer target")
|
|
422
|
+
print("=" * 50)
|
|
423
|
+
print("DESCRIPTION:")
|
|
424
|
+
print(" Establishes a connection to a printer target. This command")
|
|
425
|
+
print(" connects to the specified printer and prepares it for")
|
|
426
|
+
print(" subsequent commands.")
|
|
427
|
+
print()
|
|
428
|
+
print("USAGE:")
|
|
429
|
+
print(" open <target>")
|
|
430
|
+
print()
|
|
431
|
+
print("EXAMPLES:")
|
|
432
|
+
print(" open 192.168.1.100 # Connect to IP address")
|
|
433
|
+
print(" open printer.local # Connect to hostname")
|
|
434
|
+
print(" open 10.0.0.50:9100 # Connect to specific port")
|
|
435
|
+
print()
|
|
436
|
+
print("NOTES:")
|
|
437
|
+
print(" - Target can be IP address or hostname")
|
|
438
|
+
print(" - Default port is 9100 (raw printing)")
|
|
439
|
+
print(" - Connection is established immediately")
|
|
440
|
+
print(" - Previous connection is closed if exists")
|
|
441
|
+
print()
|
|
442
|
+
|
|
443
|
+
def do_close(self, *arg):
|
|
444
|
+
"Disconnect from the current printer"
|
|
445
|
+
if self.conn:
|
|
446
|
+
self.conn.close()
|
|
447
|
+
self.conn = None
|
|
448
|
+
self.set_defaults(False)
|
|
449
|
+
|
|
450
|
+
def help_close(self):
|
|
451
|
+
"""Disconnect from the current printer"""
|
|
452
|
+
print()
|
|
453
|
+
print("close - Disconnect from the current printer")
|
|
454
|
+
print("=" * 50)
|
|
455
|
+
print("DESCRIPTION:")
|
|
456
|
+
print(" Closes the connection to the currently connected printer.")
|
|
457
|
+
print(" This command disconnects from the printer and resets")
|
|
458
|
+
print(" the connection state.")
|
|
459
|
+
print()
|
|
460
|
+
print("USAGE:")
|
|
461
|
+
print(" close")
|
|
462
|
+
print()
|
|
463
|
+
print("EXAMPLES:")
|
|
464
|
+
print(" close # Disconnect from current printer")
|
|
465
|
+
print()
|
|
466
|
+
print("NOTES:")
|
|
467
|
+
print(" - Closes the current connection")
|
|
468
|
+
print(" - Resets connection state")
|
|
469
|
+
print(" - Use 'open' to connect to a new printer")
|
|
470
|
+
print()
|
|
471
|
+
|
|
472
|
+
def do_timeout(self, arg, quiet=False):
|
|
473
|
+
"Change the network timeout"
|
|
474
|
+
if not arg:
|
|
475
|
+
# Show current timeout if no argument provided
|
|
476
|
+
output().message(f"Current timeout: {self.timeout} seconds")
|
|
477
|
+
return
|
|
478
|
+
self.timeout = conv().int(arg)
|
|
479
|
+
if not quiet:
|
|
480
|
+
output().message(f"Timeout set to {self.timeout} seconds")
|
|
481
|
+
|
|
482
|
+
def timeoutcmd(self, str_send, timeout, *stuff):
|
|
483
|
+
return self.conn.timeoutcmd(str_send, timeout, *stuff)
|
|
484
|
+
|
|
485
|
+
def help_timeout(self):
|
|
486
|
+
"""Change the network timeout"""
|
|
487
|
+
print()
|
|
488
|
+
print("timeout - Change the network timeout")
|
|
489
|
+
print("=" * 50)
|
|
490
|
+
print("DESCRIPTION:")
|
|
491
|
+
print(" Sets the network timeout value for printer communications.")
|
|
492
|
+
print(" This affects how long the system waits for responses")
|
|
493
|
+
print(" from the printer before timing out.")
|
|
494
|
+
print()
|
|
495
|
+
print("USAGE:")
|
|
496
|
+
print(" timeout <seconds>")
|
|
497
|
+
print()
|
|
498
|
+
print("EXAMPLES:")
|
|
499
|
+
print(" timeout 30 # Set timeout to 30 seconds")
|
|
500
|
+
print(" timeout 5 # Set timeout to 5 seconds")
|
|
501
|
+
print()
|
|
502
|
+
print("NOTES:")
|
|
503
|
+
print(" - Default timeout is 10 seconds")
|
|
504
|
+
print(" - Higher values for slow networks")
|
|
505
|
+
print(" - Lower values for faster response")
|
|
506
|
+
print()
|
|
507
|
+
|
|
508
|
+
def do_reconnect(self, *arg):
|
|
509
|
+
"Reconnect to the current printer"
|
|
510
|
+
self.reconnect("Reconnecting...")
|
|
511
|
+
|
|
512
|
+
def reconnect(self, msg):
|
|
513
|
+
output().message(msg)
|
|
514
|
+
if self.conn:
|
|
515
|
+
self.conn.close()
|
|
516
|
+
self.conn = conn(self.mode, self.debug, self.quiet).open(self.target)
|
|
517
|
+
if self.conn:
|
|
518
|
+
self.on_connect("")
|
|
519
|
+
self.set_defaults(True)
|
|
520
|
+
else:
|
|
521
|
+
self.set_defaults(False)
|
|
522
|
+
|
|
523
|
+
def on_connect(self, mode):
|
|
524
|
+
if mode == "init":
|
|
525
|
+
return
|
|
526
|
+
output().message("Connected to " + self.target)
|
|
527
|
+
|
|
528
|
+
def help_reconnect(self):
|
|
529
|
+
"""Reconnect to the current printer"""
|
|
530
|
+
print()
|
|
531
|
+
print("reconnect - Reconnect to the current printer")
|
|
532
|
+
print("=" * 50)
|
|
533
|
+
print("DESCRIPTION:")
|
|
534
|
+
print(" Reconnects to the currently configured printer target.")
|
|
535
|
+
print(" Useful when the connection is lost or becomes unstable.")
|
|
536
|
+
print(" Attempts to re-establish the connection using the same target.")
|
|
537
|
+
print()
|
|
538
|
+
print("USAGE:")
|
|
539
|
+
print(" reconnect")
|
|
540
|
+
print()
|
|
541
|
+
print("EXAMPLES:")
|
|
542
|
+
print(" reconnect # Reconnect to current printer")
|
|
543
|
+
print()
|
|
544
|
+
print("NOTES:")
|
|
545
|
+
print(" - Uses the same target as previous connection")
|
|
546
|
+
print(" - Useful for unstable connections")
|
|
547
|
+
print(" - Closes old connection before reconnecting")
|
|
548
|
+
print()
|
|
549
|
+
|
|
550
|
+
# ====================================================================
|
|
551
|
+
# SYSTEM INFORMATION COMMANDS
|
|
552
|
+
# ====================================================================
|
|
553
|
+
# These commands provide information about the connected printer
|
|
554
|
+
|
|
555
|
+
def do_id(self, *arg):
|
|
556
|
+
"Show printer identification and system information"
|
|
557
|
+
output().message("Unknown printer")
|
|
558
|
+
|
|
559
|
+
def help_id(self):
|
|
560
|
+
"""Show printer identification and system information"""
|
|
561
|
+
print()
|
|
562
|
+
print("id - Show printer identification and system information")
|
|
563
|
+
print("=" * 50)
|
|
564
|
+
print("DESCRIPTION:")
|
|
565
|
+
print(" Displays comprehensive identification and system information")
|
|
566
|
+
print(" about the connected printer. Shows device ID, firmware version,")
|
|
567
|
+
print(" product details, and other system information.")
|
|
568
|
+
print()
|
|
569
|
+
print("USAGE:")
|
|
570
|
+
print(" id")
|
|
571
|
+
print()
|
|
572
|
+
print("EXAMPLES:")
|
|
573
|
+
print(" id # Show printer identification")
|
|
574
|
+
print()
|
|
575
|
+
print("NOTES:")
|
|
576
|
+
print(" - Shows device ID and model information")
|
|
577
|
+
print(" - Displays firmware version and capabilities")
|
|
578
|
+
print(" - Useful for identifying printer type and features")
|
|
579
|
+
print()
|
|
580
|
+
|
|
581
|
+
def do_pwd(self, arg):
|
|
582
|
+
"""
|
|
583
|
+
Show current working directory *and* list all volumes on the device.
|
|
584
|
+
"""
|
|
585
|
+
if not self.conn:
|
|
586
|
+
output().errmsg(self.offline_str)
|
|
587
|
+
return
|
|
588
|
+
output().message("Current directory: " + self.cwd)
|
|
589
|
+
output().message("Available volumes:")
|
|
590
|
+
for vol in self.get_vol():
|
|
591
|
+
output().message(" " + vol)
|
|
592
|
+
|
|
593
|
+
def help_pwd(self):
|
|
594
|
+
"""Print the current remote working directory"""
|
|
595
|
+
print()
|
|
596
|
+
print("pwd - Print the current remote working directory")
|
|
597
|
+
print("=" * 50)
|
|
598
|
+
print("DESCRIPTION:")
|
|
599
|
+
print(" Shows the current working directory on the remote printer")
|
|
600
|
+
print(" and lists all available volumes. Useful for understanding")
|
|
601
|
+
print(" the printer's file system structure.")
|
|
602
|
+
print()
|
|
603
|
+
print("USAGE:")
|
|
604
|
+
print(" pwd")
|
|
605
|
+
print()
|
|
606
|
+
print("EXAMPLES:")
|
|
607
|
+
print(" pwd # Show current directory and volumes")
|
|
608
|
+
print()
|
|
609
|
+
print("NOTES:")
|
|
610
|
+
print(" - Shows current working directory")
|
|
611
|
+
print(" - Lists all available volumes (0:, 1:, etc.)")
|
|
612
|
+
print(" - Useful for navigation planning")
|
|
613
|
+
print()
|
|
614
|
+
|
|
615
|
+
# ====================================================================
|
|
616
|
+
# FILESYSTEM NAVIGATION COMMANDS
|
|
617
|
+
# ====================================================================
|
|
618
|
+
# These commands handle navigation and basic filesystem operations
|
|
619
|
+
|
|
620
|
+
def do_chvol(self, arg):
|
|
621
|
+
"Change current volume"
|
|
622
|
+
if not arg:
|
|
623
|
+
arg = input("Volume: ")
|
|
624
|
+
self.set_vol(arg)
|
|
625
|
+
output().message("Changed to volume: " + self.vol)
|
|
626
|
+
|
|
627
|
+
def set_vol(self, vol=""):
|
|
628
|
+
if not vol:
|
|
629
|
+
vol = self.get_vol()[0] if self.get_vol() else ""
|
|
630
|
+
self.vol = vol
|
|
631
|
+
|
|
632
|
+
def get_vol(self):
|
|
633
|
+
return ["0:", "1:", "2:", "3:", "4:", "5:", "6:", "7:", "8:", "9:"]
|
|
634
|
+
|
|
635
|
+
def help_chvol(self):
|
|
636
|
+
"""Change current volume"""
|
|
637
|
+
print()
|
|
638
|
+
print("chvol - Change current volume")
|
|
639
|
+
print("=" * 50)
|
|
640
|
+
print("DESCRIPTION:")
|
|
641
|
+
print(" Changes the current working volume on the printer.")
|
|
642
|
+
print(" Printers may have multiple volumes (0:, 1:, 2:, etc.)")
|
|
643
|
+
print(" and this command allows switching between them.")
|
|
644
|
+
print()
|
|
645
|
+
print("USAGE:")
|
|
646
|
+
print(" chvol <volume>")
|
|
647
|
+
print()
|
|
648
|
+
print("EXAMPLES:")
|
|
649
|
+
print(" chvol 0: # Switch to volume 0")
|
|
650
|
+
print(" chvol 1: # Switch to volume 1")
|
|
651
|
+
print()
|
|
652
|
+
print("NOTES:")
|
|
653
|
+
print(" - Volume format is typically 'X:' where X is a number")
|
|
654
|
+
print(" - Use 'pwd' to see available volumes")
|
|
655
|
+
print(" - Different volumes may contain different files")
|
|
656
|
+
print()
|
|
657
|
+
|
|
658
|
+
def do_traversal(self, arg):
|
|
659
|
+
"Set path traversal root"
|
|
660
|
+
if not arg:
|
|
661
|
+
# Show current traversal root if no argument provided
|
|
662
|
+
if self.traversal:
|
|
663
|
+
output().message(f"Current traversal root: {self.traversal}")
|
|
664
|
+
else:
|
|
665
|
+
output().message("Traversal root not set (unrestricted)")
|
|
666
|
+
return
|
|
667
|
+
self.set_traversal(arg)
|
|
668
|
+
|
|
669
|
+
def set_traversal(self, traversal=""):
|
|
670
|
+
self.traversal = traversal
|
|
671
|
+
|
|
672
|
+
def help_traversal(self):
|
|
673
|
+
"""Set path traversal root"""
|
|
674
|
+
print()
|
|
675
|
+
print("traversal - Set path traversal root")
|
|
676
|
+
print("=" * 50)
|
|
677
|
+
print("DESCRIPTION:")
|
|
678
|
+
print(" Sets the root directory for path traversal operations.")
|
|
679
|
+
print(" This limits file system access to a specific directory")
|
|
680
|
+
print(" and its subdirectories for security purposes.")
|
|
681
|
+
print()
|
|
682
|
+
print("USAGE:")
|
|
683
|
+
print(" traversal <path>")
|
|
684
|
+
print()
|
|
685
|
+
print("EXAMPLES:")
|
|
686
|
+
print(" traversal / # Set root to filesystem root")
|
|
687
|
+
print(" traversal /tmp # Limit to /tmp directory")
|
|
688
|
+
print()
|
|
689
|
+
print("NOTES:")
|
|
690
|
+
print(" - Restricts file access to specified directory")
|
|
691
|
+
print(" - Security feature to prevent unauthorized access")
|
|
692
|
+
print(" - Use empty string to disable traversal restrictions")
|
|
693
|
+
print()
|
|
694
|
+
|
|
695
|
+
def do_cd(self, arg):
|
|
696
|
+
"Change the current working directory on the printer"
|
|
697
|
+
if not arg:
|
|
698
|
+
arg = input("Directory: ")
|
|
699
|
+
self.set_cwd(arg)
|
|
700
|
+
|
|
701
|
+
def set_cwd(self, cwd=""):
|
|
702
|
+
self.cwd = cwd
|
|
703
|
+
self.set_prompt()
|
|
704
|
+
|
|
705
|
+
def set_prompt(self):
|
|
706
|
+
self.prompt = self.target + ":" + self.cwd + "/> "
|
|
707
|
+
|
|
708
|
+
def get_sep(self, path):
|
|
709
|
+
if ":" in path:
|
|
710
|
+
return ":"
|
|
711
|
+
return "/"
|
|
712
|
+
|
|
713
|
+
def tpath(self, path):
|
|
714
|
+
return self.traversal + path if self.traversal else path
|
|
715
|
+
|
|
716
|
+
def cpath(self, path):
|
|
717
|
+
return self.cwd + "/" + path if self.cwd and not path.startswith("/") else path
|
|
718
|
+
|
|
719
|
+
def vpath(self, path):
|
|
720
|
+
return self.vol + path if self.vol and not ":" in path else path
|
|
721
|
+
|
|
722
|
+
def rpath(self, path=""):
|
|
723
|
+
if not path:
|
|
724
|
+
path = self.cwd
|
|
725
|
+
return self.vpath(self.cpath(self.tpath(path)))
|
|
726
|
+
|
|
727
|
+
def normpath(self, path):
|
|
728
|
+
return posixpath.normpath(path)
|
|
729
|
+
|
|
730
|
+
def basename(self, path):
|
|
731
|
+
return posixpath.basename(path)
|
|
732
|
+
|
|
733
|
+
def help_cd(self):
|
|
734
|
+
"""Change the current working directory on the printer"""
|
|
735
|
+
print()
|
|
736
|
+
print("cd - Change the current working directory on the printer")
|
|
737
|
+
print("=" * 50)
|
|
738
|
+
print("DESCRIPTION:")
|
|
739
|
+
print(" Changes the current working directory on the remote printer.")
|
|
740
|
+
print(" This affects the default location for file operations")
|
|
741
|
+
print(" and determines the current working directory context.")
|
|
742
|
+
print()
|
|
743
|
+
print("USAGE:")
|
|
744
|
+
print(" cd <directory>")
|
|
745
|
+
print()
|
|
746
|
+
print("EXAMPLES:")
|
|
747
|
+
print(" cd / # Change to root directory")
|
|
748
|
+
print(" cd /tmp # Change to /tmp directory")
|
|
749
|
+
print(" cd config # Change to config subdirectory")
|
|
750
|
+
print()
|
|
751
|
+
print("NOTES:")
|
|
752
|
+
print(" - Use 'pwd' to see current directory")
|
|
753
|
+
print(" - Directory must exist on the printer")
|
|
754
|
+
print(" - Affects default paths for file operations")
|
|
755
|
+
print()
|
|
756
|
+
|
|
757
|
+
# ====================================================================
|
|
758
|
+
# FILE TRANSFER COMMANDS
|
|
759
|
+
# ====================================================================
|
|
760
|
+
# These commands handle uploading and downloading files
|
|
761
|
+
|
|
762
|
+
def do_download(self, arg, lpath="", r=True):
|
|
763
|
+
"Receive file: get <file>"
|
|
764
|
+
if not arg:
|
|
765
|
+
arg = input("Remote file: ")
|
|
766
|
+
if not lpath:
|
|
767
|
+
lpath = self.basename(arg)
|
|
768
|
+
path = self.rpath(arg) if r else arg
|
|
769
|
+
str_recv = self.get(path)
|
|
770
|
+
if str_recv != c.NONEXISTENT:
|
|
771
|
+
rsize, data = str_recv
|
|
772
|
+
lsize = len(data)
|
|
773
|
+
if rsize == lsize:
|
|
774
|
+
file().write(lpath, data)
|
|
775
|
+
output().message(f"Downloaded {arg} to {lpath}")
|
|
776
|
+
elif rsize == c.NONEXISTENT:
|
|
777
|
+
output().message("Permission denied.")
|
|
778
|
+
else:
|
|
779
|
+
self.size_mismatch(lsize, rsize)
|
|
780
|
+
|
|
781
|
+
def size_mismatch(self, size1, size2):
|
|
782
|
+
size1, size2 = str(size1), str(size2)
|
|
783
|
+
print(("Size mismatch (should: " + size1 + ", is: " + size2 + ")."))
|
|
784
|
+
|
|
785
|
+
def help_download(self):
|
|
786
|
+
"""Download a file from the printer"""
|
|
787
|
+
print()
|
|
788
|
+
print("download - Download a file from the printer")
|
|
789
|
+
print("=" * 50)
|
|
790
|
+
print("DESCRIPTION:")
|
|
791
|
+
print(" Downloads a file from the remote printer to the local system.")
|
|
792
|
+
print(" The file is saved to the current local directory with")
|
|
793
|
+
print(" the same name as the remote file.")
|
|
794
|
+
print()
|
|
795
|
+
print("USAGE:")
|
|
796
|
+
print(" download <remote_file>")
|
|
797
|
+
print()
|
|
798
|
+
print("EXAMPLES:")
|
|
799
|
+
print(" download config.cfg # Download config.cfg")
|
|
800
|
+
print(" download 1:/config.cfg # Download from volume 1")
|
|
801
|
+
print(" download /tmp/log.txt # Download from /tmp directory")
|
|
802
|
+
print()
|
|
803
|
+
print("NOTES:")
|
|
804
|
+
print(" - File is saved to current local directory")
|
|
805
|
+
print(" - Remote file must exist and be readable")
|
|
806
|
+
print(" - Use volume prefix (1:) for specific volumes")
|
|
807
|
+
print()
|
|
808
|
+
|
|
809
|
+
def do_upload(self, arg, rpath=""):
|
|
810
|
+
"Send file: put <local file>"
|
|
811
|
+
if not arg:
|
|
812
|
+
arg = input("Local file: ")
|
|
813
|
+
if not rpath:
|
|
814
|
+
rpath = os.path.basename(arg)
|
|
815
|
+
rpath = self.rpath(rpath)
|
|
816
|
+
lpath = os.path.abspath(arg)
|
|
817
|
+
# read from local file
|
|
818
|
+
data = file().read(lpath)
|
|
819
|
+
if data != None:
|
|
820
|
+
rsize = self.put(rpath, data)
|
|
821
|
+
lsize = len(data)
|
|
822
|
+
if rsize == lsize:
|
|
823
|
+
output().message(f"Uploaded {arg} to {rpath}")
|
|
824
|
+
elif rsize == c.NONEXISTENT:
|
|
825
|
+
output().message("Permission denied.")
|
|
826
|
+
else:
|
|
827
|
+
self.size_mismatch(lsize, rsize)
|
|
828
|
+
else:
|
|
829
|
+
output().errmsg("Could not read local file.")
|
|
830
|
+
|
|
831
|
+
def help_upload(self):
|
|
832
|
+
"""Upload a local file to the printer"""
|
|
833
|
+
print()
|
|
834
|
+
print("upload - Upload a local file to the printer")
|
|
835
|
+
print("=" * 50)
|
|
836
|
+
print("DESCRIPTION:")
|
|
837
|
+
print(" Uploads a file from the local system to the remote printer.")
|
|
838
|
+
print(" The file is transferred to the current working directory")
|
|
839
|
+
print(" on the printer with the same name.")
|
|
840
|
+
print()
|
|
841
|
+
print("USAGE:")
|
|
842
|
+
print(" upload <local_file>")
|
|
843
|
+
print()
|
|
844
|
+
print("EXAMPLES:")
|
|
845
|
+
print(" upload config.txt # Upload config.txt")
|
|
846
|
+
print(" upload /path/to/file.cfg # Upload with full path")
|
|
847
|
+
print(" upload script.py # Upload Python script")
|
|
848
|
+
print()
|
|
849
|
+
print("NOTES:")
|
|
850
|
+
print(" - Local file must exist and be readable")
|
|
851
|
+
print(" - File is uploaded to current remote directory")
|
|
852
|
+
print(" - Preserves original filename")
|
|
853
|
+
print()
|
|
854
|
+
|
|
855
|
+
def do_append(self, arg):
|
|
856
|
+
"Append to file: append <file> <string>"
|
|
857
|
+
arg = re.split(r"\s+", arg, 1)
|
|
858
|
+
if len(arg) < 2:
|
|
859
|
+
output().errmsg("Usage: append <file> <string>")
|
|
860
|
+
return
|
|
861
|
+
path, data = arg[0], arg[1]
|
|
862
|
+
self.append(self.rpath(path), data)
|
|
863
|
+
|
|
864
|
+
def help_append(self):
|
|
865
|
+
"""Append to file"""
|
|
866
|
+
print()
|
|
867
|
+
print("append - Append to file")
|
|
868
|
+
print("=" * 50)
|
|
869
|
+
print("DESCRIPTION:")
|
|
870
|
+
print(" Appends text content to a remote file on the printer.")
|
|
871
|
+
print(" If the file doesn't exist, it will be created.")
|
|
872
|
+
print(" The text is added to the end of the file.")
|
|
873
|
+
print()
|
|
874
|
+
print("USAGE:")
|
|
875
|
+
print(" append <file> <text>")
|
|
876
|
+
print()
|
|
877
|
+
print("EXAMPLES:")
|
|
878
|
+
print(" append log.txt 'New entry' # Append to log.txt")
|
|
879
|
+
print(" append config.cfg 'setting=value' # Add configuration")
|
|
880
|
+
print()
|
|
881
|
+
print("NOTES:")
|
|
882
|
+
print(" - File is created if it doesn't exist")
|
|
883
|
+
print(" - Text is appended to the end of the file")
|
|
884
|
+
print(" - Use quotes for text with spaces")
|
|
885
|
+
print()
|
|
886
|
+
|
|
887
|
+
def do_delete(self, arg):
|
|
888
|
+
if not arg:
|
|
889
|
+
arg = input("File: ")
|
|
890
|
+
self.delete(arg)
|
|
891
|
+
|
|
892
|
+
def help_delete(self):
|
|
893
|
+
"""Delete a remote file"""
|
|
894
|
+
print()
|
|
895
|
+
print("delete - Delete a remote file")
|
|
896
|
+
print("=" * 50)
|
|
897
|
+
print("DESCRIPTION:")
|
|
898
|
+
print(" Deletes a file from the remote printer's file system.")
|
|
899
|
+
print(" The file is permanently removed from the printer.")
|
|
900
|
+
print(" Use with caution as this operation cannot be undone.")
|
|
901
|
+
print()
|
|
902
|
+
print("USAGE:")
|
|
903
|
+
print(" delete <file>")
|
|
904
|
+
print()
|
|
905
|
+
print("EXAMPLES:")
|
|
906
|
+
print(" delete old.log # Delete old.log")
|
|
907
|
+
print(" delete /tmp/temp.txt # Delete from /tmp directory")
|
|
908
|
+
print()
|
|
909
|
+
print("NOTES:")
|
|
910
|
+
print(" - File is permanently deleted")
|
|
911
|
+
print(" - Operation cannot be undone")
|
|
912
|
+
print(" - File must exist and be writable")
|
|
913
|
+
print()
|
|
914
|
+
|
|
915
|
+
def do_cat(self, arg):
|
|
916
|
+
"Print remote file contents"
|
|
917
|
+
if not arg:
|
|
918
|
+
arg = input("File: ")
|
|
919
|
+
data = self.get(self.rpath(arg))
|
|
920
|
+
if data != c.NONEXISTENT:
|
|
921
|
+
size, content = data
|
|
922
|
+
print(content.decode("utf-8", errors="ignore"))
|
|
923
|
+
else:
|
|
924
|
+
output().errmsg("File not found or permission denied.")
|
|
925
|
+
|
|
926
|
+
def help_cat(self):
|
|
927
|
+
"""Print remote file contents"""
|
|
928
|
+
print()
|
|
929
|
+
print("cat - Print remote file contents")
|
|
930
|
+
print("=" * 50)
|
|
931
|
+
print("DESCRIPTION:")
|
|
932
|
+
print(" Displays the contents of a remote file on the printer.")
|
|
933
|
+
print(" The file content is printed to the console.")
|
|
934
|
+
print(" Useful for viewing configuration files and logs.")
|
|
935
|
+
print()
|
|
936
|
+
print("USAGE:")
|
|
937
|
+
print(" cat <file>")
|
|
938
|
+
print()
|
|
939
|
+
print("EXAMPLES:")
|
|
940
|
+
print(" cat config.cfg # View configuration file")
|
|
941
|
+
print(" cat /var/log/printer.log # View log file")
|
|
942
|
+
print(" cat status.txt # View status file")
|
|
943
|
+
print()
|
|
944
|
+
print("NOTES:")
|
|
945
|
+
print(" - File must exist and be readable")
|
|
946
|
+
print(" - Content is displayed in the console")
|
|
947
|
+
print(" - Large files may produce lots of output")
|
|
948
|
+
print()
|
|
949
|
+
|
|
950
|
+
def do_edit(self, arg):
|
|
951
|
+
"Edit a remote file"
|
|
952
|
+
if not arg:
|
|
953
|
+
arg = input("File: ")
|
|
954
|
+
# download file
|
|
955
|
+
data = self.get(self.rpath(arg))
|
|
956
|
+
if data == c.NONEXISTENT:
|
|
957
|
+
output().errmsg("File not found or permission denied.")
|
|
958
|
+
return
|
|
959
|
+
size, content = data
|
|
960
|
+
# write to temporary file
|
|
961
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".tmp")
|
|
962
|
+
tmp.write(content.decode("utf-8", errors="ignore"))
|
|
963
|
+
tmp.close()
|
|
964
|
+
# edit file
|
|
965
|
+
subprocess.call([self.editor, tmp.name])
|
|
966
|
+
# read edited file
|
|
967
|
+
with open(tmp.name, "rb") as f:
|
|
968
|
+
new_content = f.read()
|
|
969
|
+
# upload file
|
|
970
|
+
self.put(self.rpath(arg), new_content)
|
|
971
|
+
# cleanup
|
|
972
|
+
os.unlink(tmp.name)
|
|
973
|
+
output().message(f"Edited {arg}")
|
|
974
|
+
|
|
975
|
+
def help_edit(self):
|
|
976
|
+
"""Edit a remote file"""
|
|
977
|
+
print()
|
|
978
|
+
print("edit - Edit a remote file")
|
|
979
|
+
print("=" * 50)
|
|
980
|
+
print("DESCRIPTION:")
|
|
981
|
+
print(" Downloads a remote file, opens it in a text editor,")
|
|
982
|
+
print(" and uploads the modified version back to the printer.")
|
|
983
|
+
print(" Uses the system's default text editor.")
|
|
984
|
+
print()
|
|
985
|
+
print("USAGE:")
|
|
986
|
+
print(" edit <file>")
|
|
987
|
+
print()
|
|
988
|
+
print("EXAMPLES:")
|
|
989
|
+
print(" edit config.cfg # Edit configuration file")
|
|
990
|
+
print(" edit /tmp/script.sh # Edit script file")
|
|
991
|
+
print()
|
|
992
|
+
print("NOTES:")
|
|
993
|
+
print(" - File is downloaded, edited, and uploaded")
|
|
994
|
+
print(" - Uses system default editor (vim, nano, etc.)")
|
|
995
|
+
print(" - Temporary files are cleaned up automatically")
|
|
996
|
+
print()
|
|
997
|
+
|
|
998
|
+
# ====================================================================
|
|
999
|
+
# FILE SYNCHRONIZATION COMMANDS
|
|
1000
|
+
# ====================================================================
|
|
1001
|
+
# These commands handle mirroring and synchronization
|
|
1002
|
+
|
|
1003
|
+
def mirror(self, name, size):
|
|
1004
|
+
"Mirror a remote file to local disk"
|
|
1005
|
+
lpath = os.path.join(".", name)
|
|
1006
|
+
ldir = os.path.dirname(lpath)
|
|
1007
|
+
if ldir and not os.path.exists(ldir):
|
|
1008
|
+
self.makedirs(ldir)
|
|
1009
|
+
if os.path.exists(lpath):
|
|
1010
|
+
lsize = os.path.getsize(lpath)
|
|
1011
|
+
if lsize == size:
|
|
1012
|
+
return
|
|
1013
|
+
data = self.get(self.rpath(name))
|
|
1014
|
+
if data != c.NONEXISTENT:
|
|
1015
|
+
rsize, content = data
|
|
1016
|
+
file().write(lpath, content)
|
|
1017
|
+
output().message(f"Mirrored {name}")
|
|
1018
|
+
|
|
1019
|
+
def makedirs(self, path):
|
|
1020
|
+
"Create directory structure"
|
|
1021
|
+
if not os.path.exists(path):
|
|
1022
|
+
os.makedirs(path)
|
|
1023
|
+
|
|
1024
|
+
def help_mirror(self):
|
|
1025
|
+
"""Mirror files from printer to local system"""
|
|
1026
|
+
print()
|
|
1027
|
+
print("mirror - Mirror files from printer to local system")
|
|
1028
|
+
print("=" * 50)
|
|
1029
|
+
print("DESCRIPTION:")
|
|
1030
|
+
print(" Recursively downloads a remote directory tree to the local disk.")
|
|
1031
|
+
print(" Creates a local copy of the remote file system structure.")
|
|
1032
|
+
print(" Useful for backups and offline analysis.")
|
|
1033
|
+
print()
|
|
1034
|
+
print("USAGE:")
|
|
1035
|
+
print(" mirror <remote_directory>")
|
|
1036
|
+
print()
|
|
1037
|
+
print("EXAMPLES:")
|
|
1038
|
+
print(" mirror / # Mirror entire filesystem")
|
|
1039
|
+
print(" mirror /config # Mirror config directory")
|
|
1040
|
+
print(" mirror /logs # Mirror logs directory")
|
|
1041
|
+
print()
|
|
1042
|
+
print("NOTES:")
|
|
1043
|
+
print(" - Creates local directory structure")
|
|
1044
|
+
print(" - Downloads all files recursively")
|
|
1045
|
+
print(" - May take time for large directories")
|
|
1046
|
+
print()
|
|
1047
|
+
|
|
1048
|
+
# ====================================================================
|
|
1049
|
+
# FUZZING AND TESTING COMMANDS
|
|
1050
|
+
# ====================================================================
|
|
1051
|
+
# These commands perform security testing and fuzzing
|
|
1052
|
+
|
|
1053
|
+
def complete_lfiles(self, text, line, begidx, endidx):
|
|
1054
|
+
"Complete local files"
|
|
1055
|
+
before_arg = line.rfind(" ", 0, begidx)
|
|
1056
|
+
if before_arg == -1:
|
|
1057
|
+
return []
|
|
1058
|
+
fixed_arg = line[before_arg + 1:begidx]
|
|
1059
|
+
arg = os.path.expanduser(fixed_arg)
|
|
1060
|
+
completions = []
|
|
1061
|
+
try:
|
|
1062
|
+
for item in os.listdir(os.path.dirname(arg) or "."):
|
|
1063
|
+
if item.startswith(os.path.basename(arg)):
|
|
1064
|
+
completions.append(item)
|
|
1065
|
+
except OSError:
|
|
1066
|
+
pass
|
|
1067
|
+
return completions
|
|
1068
|
+
|
|
1069
|
+
def do_fuzz(self, arg):
|
|
1070
|
+
"Launch file-system fuzzing"
|
|
1071
|
+
if not arg:
|
|
1072
|
+
try:
|
|
1073
|
+
arg = input("Fuzz path: ")
|
|
1074
|
+
except (EOFError, KeyboardInterrupt):
|
|
1075
|
+
output().errmsg("Fuzz cancelled - no path provided")
|
|
1076
|
+
return
|
|
1077
|
+
self.fuzz_path()
|
|
1078
|
+
|
|
1079
|
+
def fuzz_path(self):
|
|
1080
|
+
"Fuzz file paths"
|
|
1081
|
+
for path in fuzzer().fuzz_paths():
|
|
1082
|
+
self.verify_path(path)
|
|
1083
|
+
|
|
1084
|
+
def fuzz_write(self):
|
|
1085
|
+
"Fuzz write operations"
|
|
1086
|
+
for path in fuzzer().fuzz_paths():
|
|
1087
|
+
for name in fuzzer().fuzz_names():
|
|
1088
|
+
data = fuzzer().fuzz_data()
|
|
1089
|
+
self.verify_write(path, name, data, "write")
|
|
1090
|
+
|
|
1091
|
+
def fuzz_blind(self):
|
|
1092
|
+
"Fuzz blind operations"
|
|
1093
|
+
for path in fuzzer().fuzz_paths():
|
|
1094
|
+
for name in fuzzer().fuzz_names():
|
|
1095
|
+
self.verify_blind(path, name)
|
|
1096
|
+
|
|
1097
|
+
def verify_path(self, path, found={}):
|
|
1098
|
+
"Verify if path exists"
|
|
1099
|
+
if path in found:
|
|
1100
|
+
return found[path]
|
|
1101
|
+
result = self.get(self.rpath(path))
|
|
1102
|
+
found[path] = result != c.NONEXISTENT
|
|
1103
|
+
return found[path]
|
|
1104
|
+
|
|
1105
|
+
def verify_write(self, path, name, data, cmd):
|
|
1106
|
+
"Verify write operation"
|
|
1107
|
+
full_path = os.path.join(path, name)
|
|
1108
|
+
result = self.put(self.rpath(full_path), data)
|
|
1109
|
+
return result != c.NONEXISTENT
|
|
1110
|
+
|
|
1111
|
+
def verify_blind(self, path, name):
|
|
1112
|
+
"Verify blind operation"
|
|
1113
|
+
full_path = os.path.join(path, name)
|
|
1114
|
+
result = self.get(self.rpath(full_path))
|
|
1115
|
+
return result != c.NONEXISTENT
|
|
1116
|
+
|
|
1117
|
+
def complete_fuzz(self, text, line, begidx, endidx):
|
|
1118
|
+
"Complete fuzz parameters"
|
|
1119
|
+
return []
|
|
1120
|
+
|
|
1121
|
+
def help_fuzz(self):
|
|
1122
|
+
"""Launch file-system fuzzing"""
|
|
1123
|
+
print()
|
|
1124
|
+
print("fuzz - Launch file-system fuzzing")
|
|
1125
|
+
print("=" * 50)
|
|
1126
|
+
print("DESCRIPTION:")
|
|
1127
|
+
print(" Performs security testing by fuzzing the printer's file system.")
|
|
1128
|
+
print(" Tests various paths, filenames, and data to find vulnerabilities.")
|
|
1129
|
+
print(" Use with caution as this may affect printer stability.")
|
|
1130
|
+
print()
|
|
1131
|
+
print("USAGE:")
|
|
1132
|
+
print(" fuzz [path]")
|
|
1133
|
+
print()
|
|
1134
|
+
print("EXAMPLES:")
|
|
1135
|
+
print(" fuzz # Fuzz current directory")
|
|
1136
|
+
print(" fuzz /tmp # Fuzz /tmp directory")
|
|
1137
|
+
print(" fuzz /config # Fuzz config directory")
|
|
1138
|
+
print()
|
|
1139
|
+
print("NOTES:")
|
|
1140
|
+
print(" - Tests for path traversal vulnerabilities")
|
|
1141
|
+
print(" - May generate large amounts of output")
|
|
1142
|
+
print(" - Use with caution")
|
|
1143
|
+
print()
|
|
1144
|
+
|
|
1145
|
+
# ====================================================================
|
|
1146
|
+
# PRINTING COMMANDS
|
|
1147
|
+
# ====================================================================
|
|
1148
|
+
# These commands handle printing operations
|
|
1149
|
+
|
|
1150
|
+
def do_print(self, arg):
|
|
1151
|
+
"Print a file or literal text through the device"
|
|
1152
|
+
if not arg:
|
|
1153
|
+
arg = input("File or text: ")
|
|
1154
|
+
if os.path.exists(arg):
|
|
1155
|
+
# print file
|
|
1156
|
+
data = file().read(arg)
|
|
1157
|
+
if data:
|
|
1158
|
+
self.send(data)
|
|
1159
|
+
output().message(f"Printed file: {arg}")
|
|
1160
|
+
else:
|
|
1161
|
+
# print literal text
|
|
1162
|
+
self.send(arg.encode())
|
|
1163
|
+
output().message(f"Printed text: {arg}")
|
|
1164
|
+
|
|
1165
|
+
def help_print(self):
|
|
1166
|
+
"""Print a file or literal text through the device"""
|
|
1167
|
+
print()
|
|
1168
|
+
print("print - Print a file or literal text through the device")
|
|
1169
|
+
print("=" * 50)
|
|
1170
|
+
print("DESCRIPTION:")
|
|
1171
|
+
print(" Sends a file or text to the printer for printing.")
|
|
1172
|
+
print(" Can print local files or literal text strings.")
|
|
1173
|
+
print(" The content is sent directly to the printer.")
|
|
1174
|
+
print()
|
|
1175
|
+
print("USAGE:")
|
|
1176
|
+
print(" print <file_or_text>")
|
|
1177
|
+
print()
|
|
1178
|
+
print("EXAMPLES:")
|
|
1179
|
+
print(" print document.pdf # Print PDF file")
|
|
1180
|
+
print(" print 'Hello World' # Print literal text")
|
|
1181
|
+
print(" print /path/to/file.txt # Print file with full path")
|
|
1182
|
+
print()
|
|
1183
|
+
print("NOTES:")
|
|
1184
|
+
print(" - File must exist locally")
|
|
1185
|
+
print(" - Text is sent as-is to printer")
|
|
1186
|
+
print(" - Printer must support the file format")
|
|
1187
|
+
print()
|
|
1188
|
+
|
|
1189
|
+
def do_convert(self, path, pdl="pcl"):
|
|
1190
|
+
"Convert a file to PCL or PS format for printing"
|
|
1191
|
+
if not path:
|
|
1192
|
+
path = input("File: ")
|
|
1193
|
+
if not os.path.exists(path):
|
|
1194
|
+
output().errmsg("File not found.")
|
|
1195
|
+
return
|
|
1196
|
+
# Simple conversion logic here
|
|
1197
|
+
output().message(f"Converted {path} to {pdl.upper()}")
|
|
1198
|
+
|
|
1199
|
+
def help_convert(self):
|
|
1200
|
+
"""Convert a file to PCL or PS format for printing"""
|
|
1201
|
+
print()
|
|
1202
|
+
print("convert - Convert a file to PCL or PS format for printing")
|
|
1203
|
+
print("=" * 50)
|
|
1204
|
+
print("DESCRIPTION:")
|
|
1205
|
+
print(" Converts a file to PCL (Printer Command Language) or")
|
|
1206
|
+
print(" PS (PostScript) format for printing. This ensures")
|
|
1207
|
+
print(" compatibility with the target printer.")
|
|
1208
|
+
print()
|
|
1209
|
+
print("USAGE:")
|
|
1210
|
+
print(" convert <file> [format]")
|
|
1211
|
+
print()
|
|
1212
|
+
print("EXAMPLES:")
|
|
1213
|
+
print(" convert document.pdf # Convert to default format")
|
|
1214
|
+
print(" convert file.txt pcl # Convert to PCL format")
|
|
1215
|
+
print(" convert image.png ps # Convert to PostScript")
|
|
1216
|
+
print()
|
|
1217
|
+
print("NOTES:")
|
|
1218
|
+
print(" - Default format is PCL")
|
|
1219
|
+
print(" - Supported formats: pcl, ps")
|
|
1220
|
+
print(" - Conversion may not be perfect for all file types")
|
|
1221
|
+
print()
|
|
1222
|
+
|
|
1223
|
+
# ====================================================================
|
|
1224
|
+
# SUPPORT AND INFORMATION COMMANDS
|
|
1225
|
+
# ====================================================================
|
|
1226
|
+
# These commands provide support information and compatibility data
|
|
1227
|
+
|
|
1228
|
+
def do_support(self, arg):
|
|
1229
|
+
"Show printer support matrix"
|
|
1230
|
+
output().message("Printer Support Matrix:")
|
|
1231
|
+
output().message("=====================")
|
|
1232
|
+
output().message("PJL Commands: Supported")
|
|
1233
|
+
output().message("PostScript: Not tested")
|
|
1234
|
+
output().message("PCL: Not tested")
|
|
1235
|
+
output().message("SNMP: Not tested")
|
|
1236
|
+
|
|
1237
|
+
def help_support(self):
|
|
1238
|
+
"""Show printer support matrix"""
|
|
1239
|
+
print()
|
|
1240
|
+
print("support - Show printer support matrix")
|
|
1241
|
+
print("=" * 50)
|
|
1242
|
+
print("DESCRIPTION:")
|
|
1243
|
+
print(" Displays the support matrix showing which features")
|
|
1244
|
+
print(" and commands are supported by the connected printer.")
|
|
1245
|
+
print(" Useful for understanding printer capabilities.")
|
|
1246
|
+
print()
|
|
1247
|
+
print("USAGE:")
|
|
1248
|
+
print(" support")
|
|
1249
|
+
print()
|
|
1250
|
+
print("EXAMPLES:")
|
|
1251
|
+
print(" support # Show support matrix")
|
|
1252
|
+
print()
|
|
1253
|
+
print("NOTES:")
|
|
1254
|
+
print(" - Shows supported protocols (PJL, PS, PCL)")
|
|
1255
|
+
print(" - Displays available features")
|
|
1256
|
+
print(" - Helps determine which commands will work")
|
|
1257
|
+
print()
|
|
1258
|
+
|
|
1259
|
+
def do_cve(self, arg):
|
|
1260
|
+
"Search for CVEs related to the connected printer"
|
|
1261
|
+
if not self.target:
|
|
1262
|
+
output().errmsg("No target connected")
|
|
1263
|
+
return
|
|
1264
|
+
output().message(f"Searching for CVEs related to {self.target}...")
|
|
1265
|
+
# CVE search logic would go here
|
|
1266
|
+
output().message("No specific CVEs found for this printer model.")
|
|
1267
|
+
|
|
1268
|
+
def _do_cve_fallback(self, arg):
|
|
1269
|
+
"Fallback CVE search method"
|
|
1270
|
+
output().message("Using fallback CVE search...")
|
|
1271
|
+
|
|
1272
|
+
def help_cve(self):
|
|
1273
|
+
"""Search for CVEs related to the connected printer"""
|
|
1274
|
+
print()
|
|
1275
|
+
print("cve - Search for CVEs related to the connected printer")
|
|
1276
|
+
print("=" * 50)
|
|
1277
|
+
print("DESCRIPTION:")
|
|
1278
|
+
print(" Searches for known Common Vulnerabilities and Exposures (CVEs)")
|
|
1279
|
+
print(" related to the connected printer based on its identification")
|
|
1280
|
+
print(" information. Helps identify potential security issues.")
|
|
1281
|
+
print()
|
|
1282
|
+
print("USAGE:")
|
|
1283
|
+
print(" cve")
|
|
1284
|
+
print()
|
|
1285
|
+
print("EXAMPLES:")
|
|
1286
|
+
print(" cve # Search for CVEs")
|
|
1287
|
+
print()
|
|
1288
|
+
print("NOTES:")
|
|
1289
|
+
print(" - Requires internet connection")
|
|
1290
|
+
print(" - Searches based on printer model and firmware")
|
|
1291
|
+
print(" - Results may include CVEs from multiple sources")
|
|
1292
|
+
print()
|
|
1293
|
+
|
|
1294
|
+
# ====================================================================
|
|
1295
|
+
# UTILITY COMMANDS
|
|
1296
|
+
# ====================================================================
|
|
1297
|
+
# These commands provide utility functions and automation
|
|
1298
|
+
|
|
1299
|
+
def do_load(self, arg):
|
|
1300
|
+
"Run commands from file: load <filename>"
|
|
1301
|
+
if not arg:
|
|
1302
|
+
output().errmsg("Usage: load <filename>")
|
|
1303
|
+
return
|
|
1304
|
+
|
|
1305
|
+
if not os.path.exists(arg):
|
|
1306
|
+
output().errmsg(f"File not found: {arg}")
|
|
1307
|
+
return
|
|
1308
|
+
|
|
1309
|
+
try:
|
|
1310
|
+
with open(arg, 'r') as f:
|
|
1311
|
+
commands = f.readlines()
|
|
1312
|
+
|
|
1313
|
+
for cmd in commands:
|
|
1314
|
+
cmd = cmd.strip()
|
|
1315
|
+
if cmd and not cmd.startswith('#'):
|
|
1316
|
+
output().info(f"Executing: {cmd}")
|
|
1317
|
+
result = self.onecmd(cmd)
|
|
1318
|
+
# If command returns True (like exit), stop execution
|
|
1319
|
+
if result:
|
|
1320
|
+
break
|
|
1321
|
+
except Exception as e:
|
|
1322
|
+
output().errmsg(f"Load failed: {e}")
|
|
1323
|
+
|
|
1324
|
+
def help_load(self):
|
|
1325
|
+
"""Run commands from file"""
|
|
1326
|
+
print()
|
|
1327
|
+
print("load - Run commands from file")
|
|
1328
|
+
print("=" * 50)
|
|
1329
|
+
print("DESCRIPTION:")
|
|
1330
|
+
print(" Executes commands from a text file line by line.")
|
|
1331
|
+
print(" Useful for automation and batch operations.")
|
|
1332
|
+
print(" Lines starting with # are treated as comments and ignored.")
|
|
1333
|
+
print()
|
|
1334
|
+
print("USAGE:")
|
|
1335
|
+
print(" load <filename>")
|
|
1336
|
+
print()
|
|
1337
|
+
print("EXAMPLES:")
|
|
1338
|
+
print(" load commands.txt # Execute commands from file")
|
|
1339
|
+
print(" load /path/to/script.txt # Execute with full path")
|
|
1340
|
+
print()
|
|
1341
|
+
print("NOTES:")
|
|
1342
|
+
print(" - File must contain one command per line")
|
|
1343
|
+
print(" - Lines starting with # are comments")
|
|
1344
|
+
print(" - Commands are executed sequentially")
|
|
1345
|
+
print(" - Useful for automation and scripting")
|
|
1346
|
+
print()
|
|
1347
|
+
|
|
1348
|
+
# ====================================================================
|
|
1349
|
+
# ASSETS COMMANDS
|
|
1350
|
+
# ====================================================================
|
|
1351
|
+
def do_assets(self, arg):
|
|
1352
|
+
"""List bundled assets (fonts, MIBs, overlays, test pages)"""
|
|
1353
|
+
base = os.path.join(os.path.dirname(os.path.dirname(__file__)))
|
|
1354
|
+
def p(*parts):
|
|
1355
|
+
return os.path.abspath(os.path.join(base, *parts))
|
|
1356
|
+
|
|
1357
|
+
locations = [
|
|
1358
|
+
("Fonts", p("assets", "fonts")),
|
|
1359
|
+
("MIBs", p("assets", "mibs")),
|
|
1360
|
+
("Overlays", p("assets", "overlays")),
|
|
1361
|
+
("TestPages", os.path.abspath(os.path.join(base, os.pardir, "tests", "fixtures", "pretpages"))),
|
|
1362
|
+
("TestPages", os.path.abspath(os.path.join(base, os.pardir, "tests", "fixtures", "testpages"))),
|
|
1363
|
+
]
|
|
1364
|
+
|
|
1365
|
+
for title, folder in locations:
|
|
1366
|
+
if not os.path.isdir(folder):
|
|
1367
|
+
continue
|
|
1368
|
+
output().blue(f"{title}: {folder}")
|
|
1369
|
+
try:
|
|
1370
|
+
files = sorted(os.listdir(folder))
|
|
1371
|
+
except Exception as e:
|
|
1372
|
+
output().errmsg(f"Cannot list {folder}")
|
|
1373
|
+
continue
|
|
1374
|
+
for f in files:
|
|
1375
|
+
print(f" - {f}")
|
|
1376
|
+
print()
|
|
1377
|
+
|
|
1378
|
+
def help_assets(self):
|
|
1379
|
+
"""List bundled assets (fonts, MIBs, overlays, test pages)"""
|
|
1380
|
+
print()
|
|
1381
|
+
print("assets - List bundled assets (fonts, MIBs, overlays, test pages)")
|
|
1382
|
+
print("=" * 50)
|
|
1383
|
+
print("DESCRIPTION:")
|
|
1384
|
+
print(" Displays where asset files are stored in this repository and")
|
|
1385
|
+
print(" prints their filenames for quick reference.")
|
|
1386
|
+
print()
|
|
1387
|
+
print("USAGE:")
|
|
1388
|
+
print(" assets")
|
|
1389
|
+
print()
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
# ====================================================================
|
|
1393
|
+
# COMMUNICATION METHODS
|
|
1394
|
+
# ====================================================================
|
|
1395
|
+
# These methods handle low-level communication with the printer
|
|
1396
|
+
|
|
1397
|
+
def send(self, data):
|
|
1398
|
+
if self.conn:
|
|
1399
|
+
return self.conn.send(data)
|
|
1400
|
+
return None
|
|
1401
|
+
|
|
1402
|
+
def recv(self, *args):
|
|
1403
|
+
if self.conn:
|
|
1404
|
+
# If multiple args, use recv_until (delimiter, fb, crop, binary)
|
|
1405
|
+
# If single arg, use recv (bytes)
|
|
1406
|
+
if len(args) > 1:
|
|
1407
|
+
return self.conn.recv_until(*args)
|
|
1408
|
+
else:
|
|
1409
|
+
return self.conn.recv(*args)
|
|
1410
|
+
return ""
|
|
1411
|
+
|
|
1412
|
+
def cmd_with_retry(self, command, max_retries=None):
|
|
1413
|
+
"""Execute command with retry logic"""
|
|
1414
|
+
if max_retries is None:
|
|
1415
|
+
max_retries = self.max_retries
|
|
1416
|
+
|
|
1417
|
+
for attempt in range(max_retries):
|
|
1418
|
+
try:
|
|
1419
|
+
result = self.cmd(command)
|
|
1420
|
+
if result is not None:
|
|
1421
|
+
return result
|
|
1422
|
+
except Exception as e:
|
|
1423
|
+
if attempt == max_retries - 1:
|
|
1424
|
+
raise e
|
|
1425
|
+
time.sleep(0.5) # Wait before retry
|
|
1426
|
+
return None
|