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/protocols/raw.py ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ RAW Protocol Support for PrinterXPL-Forge
5
+ ======================================
6
+ Direct TCP/IP printing on port 9100 (AppSocket / JetDirect).
7
+
8
+ Supports IPv4 and IPv6 transparently.
9
+ """
10
+
11
+ # Author : Andre Henrique (@mrhenrike)
12
+ # GitHub : https://github.com/mrhenrike
13
+ # LinkedIn : https://linkedin.com/in/mrhenrike
14
+ # X/Twitter : https://x.com/mrhenrike
15
+
16
+ from __future__ import annotations
17
+
18
+ import socket
19
+ import time
20
+ from typing import Optional
21
+
22
+
23
+ def _resolve_address(host: str) -> tuple:
24
+ """
25
+ Resolve *host* to a (family, addr) pair.
26
+
27
+ Handles:
28
+ - Plain IPv4 addresses / hostnames → AF_INET
29
+ - IPv6 literals (with or without [])→ AF_INET6
30
+ - Dual-stack hostnames → prefer IPv4, fall back to IPv6
31
+ """
32
+ # Strip brackets from IPv6 literals like [::1]
33
+ host_clean = host.strip('[]')
34
+
35
+ # Attempt getaddrinfo — returns list of (family, type, proto, canonname, sockaddr)
36
+ try:
37
+ infos = socket.getaddrinfo(host_clean, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
38
+ except socket.gaierror as exc:
39
+ raise ConnectionError(f"Cannot resolve host '{host}': {exc}") from exc
40
+
41
+ if not infos:
42
+ raise ConnectionError(f"No address found for host '{host}'")
43
+
44
+ # Prefer IPv4 to avoid surprises on single-stack networks
45
+ for info in infos:
46
+ if info[0] == socket.AF_INET:
47
+ return socket.AF_INET, info[4][0]
48
+
49
+ # Fall back to first result (IPv6 or whatever is available)
50
+ info = infos[0]
51
+ return info[0], info[4][0]
52
+
53
+
54
+ class RAWProtocol:
55
+ """
56
+ RAW / AppSocket / JetDirect protocol implementation (default port 9100).
57
+
58
+ This is the most common protocol for sending PJL, PostScript and PCL
59
+ payloads directly to a network printer.
60
+
61
+ Supports both IPv4 and IPv6.
62
+ """
63
+
64
+ DEFAULT_PORT = 9100
65
+
66
+ def __init__(self, host: str, port: Optional[int] = None, timeout: float = 30.0):
67
+ self.host = host
68
+ self.port = port or self.DEFAULT_PORT
69
+ self.timeout = timeout
70
+ self.sock: Optional[socket.socket] = None
71
+ self._family: int = socket.AF_INET
72
+
73
+ # ------------------------------------------------------------------
74
+ def connect(self) -> bool:
75
+ """
76
+ Open a TCP connection to the printer.
77
+
78
+ Returns True on success, False on failure.
79
+ Auto-selects IPv4 or IPv6 based on host resolution.
80
+ """
81
+ try:
82
+ family, addr = _resolve_address(self.host)
83
+ self._family = family
84
+ self.sock = socket.socket(family, socket.SOCK_STREAM)
85
+ self.sock.settimeout(self.timeout)
86
+ self.sock.connect((addr, self.port))
87
+ return True
88
+ except Exception:
89
+ if self.sock:
90
+ try:
91
+ self.sock.close()
92
+ except Exception:
93
+ pass
94
+ self.sock = None
95
+ return False
96
+
97
+ def send(self, data: bytes | str) -> None:
98
+ """Send raw bytes (or a string which will be UTF-8 encoded)."""
99
+ if not self.sock:
100
+ raise ConnectionError("Not connected — call connect() first")
101
+ if isinstance(data, str):
102
+ data = data.encode('utf-8', errors='replace')
103
+ self.sock.sendall(data)
104
+
105
+ def recv(self, size: int = 4096, timeout: Optional[float] = None) -> bytes:
106
+ """
107
+ Receive up to *size* bytes.
108
+
109
+ *timeout* overrides the socket timeout for this single call.
110
+ """
111
+ if not self.sock:
112
+ raise ConnectionError("Not connected")
113
+ if timeout is not None:
114
+ old = self.sock.gettimeout()
115
+ self.sock.settimeout(timeout)
116
+ try:
117
+ return self.sock.recv(size)
118
+ finally:
119
+ if timeout is not None:
120
+ self.sock.settimeout(old)
121
+
122
+ def recv_all(self, timeout: float = 2.0) -> bytes:
123
+ """
124
+ Drain all available data with a short read-loop until the printer
125
+ stops sending (identified by a read timeout).
126
+ """
127
+ buf = b''
128
+ self.sock.settimeout(timeout)
129
+ try:
130
+ while True:
131
+ chunk = self.sock.recv(4096)
132
+ if not chunk:
133
+ break
134
+ buf += chunk
135
+ except (socket.timeout, BlockingIOError):
136
+ pass
137
+ return buf
138
+
139
+ def close(self) -> None:
140
+ """Close the underlying socket."""
141
+ if self.sock:
142
+ try:
143
+ self.sock.close()
144
+ except Exception:
145
+ pass
146
+ self.sock = None
147
+
148
+ @property
149
+ def is_connected(self) -> bool:
150
+ """Return True if the socket is currently open."""
151
+ return self.sock is not None
152
+
153
+ @property
154
+ def is_ipv6(self) -> bool:
155
+ """Return True if the active connection uses IPv6."""
156
+ return self._family == socket.AF_INET6
157
+
158
+ # ------------------------------------------------------------------
159
+ def __enter__(self) -> 'RAWProtocol':
160
+ self.connect()
161
+ return self
162
+
163
+ def __exit__(self, *_) -> None:
164
+ self.close()
165
+
166
+ def __repr__(self) -> str:
167
+ proto = 'IPv6' if self.is_ipv6 else 'IPv4'
168
+ state = 'connected' if self.is_connected else 'disconnected'
169
+ return f"<RAWProtocol {self.host}:{self.port} [{proto}] {state}>"
170
+
171
+
172
+ # Backward-compatibility alias
173
+ RawProtocol = RAWProtocol
src/protocols/smb.py ADDED
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ SMB Protocol Support for PrinterXPL-Forge
5
+ ========================================
6
+ Server Message Block printing on ports 445 / 139.
7
+
8
+ Two backends are tried in priority order:
9
+ 1. pysmb (pure-Python, SMB1 / SMB2)
10
+ 2. smbclient system binary (fallback via subprocess)
11
+
12
+ Both IPv4 and IPv6 are supported.
13
+
14
+ Usage example::
15
+
16
+ with SMBProtocol('192.168.1.5', share='print$') as smb:
17
+ if smb.connect():
18
+ shares = smb.list_shares()
19
+ printers = smb.list_printers()
20
+ smb.print_file('payload.ps', b'%!PS ...')
21
+ """
22
+
23
+ # Author : Andre Henrique (@mrhenrike)
24
+ # GitHub : https://github.com/mrhenrike
25
+ # LinkedIn : https://linkedin.com/in/mrhenrike
26
+ # X/Twitter : https://x.com/mrhenrike
27
+
28
+ from __future__ import annotations
29
+
30
+ import io
31
+ import os
32
+ import shutil
33
+ import socket
34
+ import subprocess
35
+ import tempfile
36
+ import uuid
37
+ from typing import Dict, List, Optional
38
+
39
+ # ── Optional pysmb backend ───────────────────────────────────────────────────
40
+ try:
41
+ from smb.SMBConnection import SMBConnection as _SMBConnection
42
+ from smb.smb_structs import OperationFailure as _SMBOpFail
43
+ _PYSMB_AVAILABLE = True
44
+ except ImportError:
45
+ _PYSMB_AVAILABLE = False
46
+ _SMBOpFail = Exception # type: ignore
47
+
48
+
49
+ class SMBProtocol:
50
+ """
51
+ SMB protocol implementation for printer enumeration and file printing.
52
+
53
+ Supports:
54
+ - Share enumeration
55
+ - Printer share discovery
56
+ - File printing via SMB
57
+ - SMB printer fingerprinting (OS, machine name)
58
+ """
59
+
60
+ DEFAULT_PORT = 445
61
+ ALT_PORT = 139
62
+
63
+ def __init__(
64
+ self,
65
+ host: str,
66
+ port: Optional[int] = None,
67
+ timeout: float = 30.0,
68
+ share: str = 'print$',
69
+ username: str = 'guest',
70
+ password: str = '',
71
+ domain: str = '',
72
+ ):
73
+ self.host = host.strip('[]')
74
+ self.port = port or self.DEFAULT_PORT
75
+ self.timeout = timeout
76
+ self.share = share
77
+ self.username = username
78
+ self.password = password
79
+ self.domain = domain
80
+
81
+ self._conn: Optional[_SMBConnection] = None
82
+ self._connected: bool = False
83
+ self._server_name: str = ''
84
+ self._os_info: str = ''
85
+ self._backend: str = 'none'
86
+
87
+ # ------------------------------------------------------------------
88
+ # Connection
89
+ # ------------------------------------------------------------------
90
+
91
+ def connect(self) -> bool:
92
+ """
93
+ Attempt SMB connection using pysmb (preferred) or a TCP probe.
94
+
95
+ Returns True on success, False on failure.
96
+ """
97
+ if _PYSMB_AVAILABLE:
98
+ return self._connect_pysmb()
99
+ return self._connect_raw_tcp()
100
+
101
+ def _connect_pysmb(self) -> bool:
102
+ """Connect using pysmb library (SMB1/SMB2)."""
103
+ client_name = 'PrinterXPL-Forge-' + uuid.uuid4().hex[:6]
104
+ try:
105
+ # Resolve server NetBIOS name (use hostname or IP as fallback)
106
+ server_name = self._resolve_netbios_name() or self.host
107
+ self._conn = _SMBConnection(
108
+ self.username,
109
+ self.password,
110
+ client_name,
111
+ server_name,
112
+ domain = self.domain,
113
+ use_ntlm_v2 = True,
114
+ is_direct_tcp = (self.port == 445),
115
+ )
116
+ connected = self._conn.connect(self.host, self.port, timeout=int(self.timeout))
117
+ if connected:
118
+ self._connected = True
119
+ self._backend = 'pysmb'
120
+ self._server_name = server_name
121
+ return connected
122
+ except Exception:
123
+ self._conn = None
124
+ return False
125
+
126
+ def _connect_raw_tcp(self) -> bool:
127
+ """Fallback: just probe whether the port is open."""
128
+ for port in (self.port, self.ALT_PORT):
129
+ try:
130
+ s = socket.create_connection((self.host, port), timeout=self.timeout)
131
+ s.close()
132
+ self.port = port
133
+ self._backend = 'tcp-probe'
134
+ self._connected = True
135
+ return True
136
+ except OSError:
137
+ continue
138
+ return False
139
+
140
+ def _resolve_netbios_name(self) -> str:
141
+ """Try to obtain the remote machine's NetBIOS / hostname."""
142
+ try:
143
+ return socket.gethostbyaddr(self.host)[0].split('.')[0].upper()
144
+ except Exception:
145
+ return ''
146
+
147
+ # ------------------------------------------------------------------
148
+ # Enumeration
149
+ # ------------------------------------------------------------------
150
+
151
+ def list_shares(self) -> List[Dict[str, str]]:
152
+ """
153
+ List all SMB shares on the target.
154
+
155
+ Returns a list of dicts with keys: name, type, comments.
156
+ """
157
+ if not self._connected:
158
+ raise RuntimeError("Not connected — call connect() first")
159
+
160
+ if self._conn:
161
+ try:
162
+ raw = self._conn.listShares()
163
+ return [
164
+ {
165
+ 'name': s.name,
166
+ 'type': str(s.type),
167
+ 'comments': s.comments,
168
+ }
169
+ for s in raw
170
+ ]
171
+ except Exception as exc:
172
+ raise RuntimeError(f"listShares failed: {exc}") from exc
173
+
174
+ # No pysmb — try smbclient binary
175
+ return self._smbclient_list_shares()
176
+
177
+ def list_printers(self) -> List[Dict[str, str]]:
178
+ """
179
+ Filter shares that are printer shares (type 0x00000001 in SMB).
180
+
181
+ Returns a list of printer share dicts.
182
+ """
183
+ shares = self.list_shares()
184
+ printers = []
185
+ for s in shares:
186
+ # pysmb returns type as string; '1' or '65536' (print$ uses type 3)
187
+ name = s.get('name', '').lower()
188
+ if ('print' in name or s.get('type', '') in ('1', '65536', '3')):
189
+ printers.append(s)
190
+ return printers
191
+
192
+ def list_files(self, path: str = '/') -> List[str]:
193
+ """List files in a share path (requires an active pysmb connection)."""
194
+ if not self._conn:
195
+ raise RuntimeError("pysmb backend required for file listing")
196
+ try:
197
+ entries = self._conn.listPath(self.share, path)
198
+ return [e.filename for e in entries if e.filename not in ('.', '..')]
199
+ except _SMBOpFail as exc:
200
+ raise PermissionError(f"Cannot list {self.share}{path}: {exc}") from exc
201
+
202
+ # ------------------------------------------------------------------
203
+ # Printing
204
+ # ------------------------------------------------------------------
205
+
206
+ def print_file(
207
+ self,
208
+ remote_filename: str,
209
+ data: bytes,
210
+ print_share: Optional[str] = None,
211
+ ) -> bool:
212
+ """
213
+ Send *data* as a print job to the SMB printer share.
214
+
215
+ Args:
216
+ remote_filename: Spool filename on the share (e.g. 'job.ps').
217
+ data: Raw bytes to send (PostScript, PCL, etc.).
218
+ print_share: Share name to use; defaults to self.share.
219
+
220
+ Returns True on success.
221
+ """
222
+ share = print_share or self.share
223
+
224
+ if self._conn and self._backend == 'pysmb':
225
+ return self._pysmb_print(share, remote_filename, data)
226
+
227
+ return self._smbclient_print(share, data)
228
+
229
+ def _pysmb_print(self, share: str, filename: str, data: bytes) -> bool:
230
+ """Upload data to printer share using pysmb."""
231
+ try:
232
+ buf = io.BytesIO(data)
233
+ self._conn.storeFile(share, filename, buf)
234
+ return True
235
+ except Exception as exc:
236
+ raise RuntimeError(f"SMB store failed: {exc}") from exc
237
+
238
+ def _smbclient_print(self, share: str, data: bytes) -> bool:
239
+ """Print via smbclient binary (subprocess fallback)."""
240
+ smbclient = shutil.which('smbclient')
241
+ if not smbclient:
242
+ raise RuntimeError(
243
+ "pysmb not available and smbclient not found. "
244
+ "Install with: apt install smbclient | brew install samba"
245
+ )
246
+ # Write data to a temp file, then spool via smbclient
247
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.prn') as tmp:
248
+ tmp.write(data)
249
+ tmp_path = tmp.name
250
+ try:
251
+ cmd = [smbclient, f'//{self.host}/{share}',
252
+ '-U', f'{self.username}%{self.password}',
253
+ '-c', f'print {tmp_path}']
254
+ result = subprocess.run(cmd, capture_output=True, text=True,
255
+ timeout=self.timeout)
256
+ if result.returncode != 0:
257
+ raise RuntimeError(f"smbclient error: {result.stderr.strip()}")
258
+ return True
259
+ finally:
260
+ try:
261
+ os.unlink(tmp_path)
262
+ except OSError:
263
+ pass
264
+
265
+ # ------------------------------------------------------------------
266
+ # Fingerprinting
267
+ # ------------------------------------------------------------------
268
+
269
+ def get_info(self) -> Dict[str, str]:
270
+ """
271
+ Return basic SMB session information.
272
+
273
+ Includes: host, port, backend, connected, server_name, os_info, share.
274
+ """
275
+ return {
276
+ 'host': self.host,
277
+ 'port': str(self.port),
278
+ 'backend': self._backend,
279
+ 'connected': str(self._connected),
280
+ 'server_name': self._server_name,
281
+ 'os_info': self._os_info,
282
+ 'share': self.share,
283
+ 'pysmb': str(_PYSMB_AVAILABLE),
284
+ }
285
+
286
+ def fingerprint(self) -> str:
287
+ """
288
+ Attempt to obtain OS/machine fingerprint from the SMB session.
289
+
290
+ Returns a human-readable string.
291
+ """
292
+ if self._conn and self._backend == 'pysmb':
293
+ try:
294
+ # pysmb stores server OS info after a successful connection
295
+ info = self._conn.getAttributes(self.share, '/')
296
+ return f"Server: {self._server_name}"
297
+ except Exception:
298
+ pass
299
+ return f"SMB host {self.host}:{self.port} [{self._backend}]"
300
+
301
+ # ------------------------------------------------------------------
302
+ # Context manager
303
+ # ------------------------------------------------------------------
304
+
305
+ def close(self) -> None:
306
+ """Close the SMB connection."""
307
+ if self._conn:
308
+ try:
309
+ self._conn.close()
310
+ except Exception:
311
+ pass
312
+ self._conn = None
313
+ self._connected = False
314
+
315
+ def __enter__(self) -> 'SMBProtocol':
316
+ self.connect()
317
+ return self
318
+
319
+ def __exit__(self, *_) -> None:
320
+ self.close()
321
+
322
+ def __repr__(self) -> str:
323
+ state = 'connected' if self._connected else 'disconnected'
324
+ return (
325
+ f"<SMBProtocol {self.host}:{self.port} "
326
+ f"share={self.share!r} backend={self._backend!r} {state}>"
327
+ )
328
+
329
+
330
+ # ── Module-level convenience function ─────────────────────────────────────────
331
+
332
+ def print_via_smb(
333
+ host: str,
334
+ share: str,
335
+ data: bytes,
336
+ username: str = 'guest',
337
+ password: str = '',
338
+ port: int = 445,
339
+ timeout: float = 30.0,
340
+ ) -> bool:
341
+ """
342
+ One-shot SMB print helper.
343
+
344
+ Args:
345
+ host: Printer IP or hostname.
346
+ share: SMB printer share name (e.g. 'print$').
347
+ data: Raw bytes to spool (PostScript, PJL, etc.).
348
+ username: SMB username (default: guest).
349
+ password: SMB password (default: empty).
350
+ port: SMB port (default: 445).
351
+ timeout: Connection timeout in seconds.
352
+
353
+ Returns True on success.
354
+ """
355
+ with SMBProtocol(host, port=port, timeout=timeout,
356
+ share=share, username=username, password=password) as smb:
357
+ if not smb._connected:
358
+ raise ConnectionError(f"Cannot connect to SMB on {host}:{port}")
359
+ return smb.print_file('printjob.prn', data)