printerxpl-forge 6.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. src/version.py +36 -0
src/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