pygpt-net 2.6.19.post1__py3-none-any.whl → 2.6.20__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.
- pygpt_net/CHANGELOG.txt +5 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/app.py +3 -1
- pygpt_net/data/config/config.json +2 -2
- pygpt_net/data/config/models.json +2 -2
- pygpt_net/plugin/server/__init__.py +12 -0
- pygpt_net/plugin/server/config.py +301 -0
- pygpt_net/plugin/server/plugin.py +111 -0
- pygpt_net/plugin/server/worker.py +1057 -0
- pygpt_net/ui/base/config_dialog.py +17 -3
- pygpt_net/ui/widget/option/checkbox.py +16 -2
- {pygpt_net-2.6.19.post1.dist-info → pygpt_net-2.6.20.dist-info}/METADATA +19 -2
- {pygpt_net-2.6.19.post1.dist-info → pygpt_net-2.6.20.dist-info}/RECORD +16 -12
- {pygpt_net-2.6.19.post1.dist-info → pygpt_net-2.6.20.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.19.post1.dist-info → pygpt_net-2.6.20.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.19.post1.dist-info → pygpt_net-2.6.20.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.08.22 10:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shlex
|
|
16
|
+
import subprocess
|
|
17
|
+
import ftplib
|
|
18
|
+
import smtplib
|
|
19
|
+
import time
|
|
20
|
+
import stat as pystat
|
|
21
|
+
|
|
22
|
+
from email.message import EmailMessage
|
|
23
|
+
from PySide6.QtCore import Slot
|
|
24
|
+
|
|
25
|
+
from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkerSignals(BaseSignals):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Worker(BaseWorker):
|
|
33
|
+
"""
|
|
34
|
+
Server plugin worker: SSH, SFTP, FTP, Telnet, SMTP.
|
|
35
|
+
Credentials are fetched via self.get_server_config(server, port).
|
|
36
|
+
When prefer_system_ssh is enabled, uses system ssh/scp/sftp and system keys.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Global map SERVICE -> PORT (defaults)
|
|
40
|
+
SERVICE_PORTS = {
|
|
41
|
+
"ssh": 22,
|
|
42
|
+
"sftp": 22,
|
|
43
|
+
"ftp": 21,
|
|
44
|
+
"telnet": 23,
|
|
45
|
+
"smtp": 25,
|
|
46
|
+
"smtps": 465,
|
|
47
|
+
"submission": 587,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def __init__(self, *args, **kwargs):
|
|
51
|
+
super(Worker, self).__init__()
|
|
52
|
+
self.signals = WorkerSignals()
|
|
53
|
+
self.args = args
|
|
54
|
+
self.kwargs = kwargs
|
|
55
|
+
self.plugin = None
|
|
56
|
+
self.cmds = None
|
|
57
|
+
self.ctx = None
|
|
58
|
+
self.msg = None
|
|
59
|
+
|
|
60
|
+
# ---------------------- Runner ----------------------
|
|
61
|
+
|
|
62
|
+
@Slot()
|
|
63
|
+
def run(self):
|
|
64
|
+
try:
|
|
65
|
+
responses = []
|
|
66
|
+
for item in self.cmds:
|
|
67
|
+
if self.is_stopped():
|
|
68
|
+
break
|
|
69
|
+
try:
|
|
70
|
+
response = None
|
|
71
|
+
if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
|
|
72
|
+
|
|
73
|
+
# -------- Core FS / Exec --------
|
|
74
|
+
if item["cmd"] == "srv_exec":
|
|
75
|
+
response = self.cmd_srv_exec(item)
|
|
76
|
+
elif item["cmd"] == "srv_ls":
|
|
77
|
+
response = self.cmd_srv_ls(item)
|
|
78
|
+
elif item["cmd"] == "srv_get":
|
|
79
|
+
response = self.cmd_srv_get(item)
|
|
80
|
+
elif item["cmd"] == "srv_put":
|
|
81
|
+
response = self.cmd_srv_put(item)
|
|
82
|
+
elif item["cmd"] == "srv_rm":
|
|
83
|
+
response = self.cmd_srv_rm(item)
|
|
84
|
+
elif item["cmd"] == "srv_mkdir":
|
|
85
|
+
response = self.cmd_srv_mkdir(item)
|
|
86
|
+
elif item["cmd"] == "srv_stat":
|
|
87
|
+
response = self.cmd_srv_stat(item)
|
|
88
|
+
|
|
89
|
+
# -------- SMTP --------
|
|
90
|
+
elif item["cmd"] == "smtp_send":
|
|
91
|
+
response = self.cmd_smtp_send(item)
|
|
92
|
+
|
|
93
|
+
if response:
|
|
94
|
+
responses.append(response)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
98
|
+
|
|
99
|
+
if responses:
|
|
100
|
+
self.reply_more(responses)
|
|
101
|
+
if self.msg is not None:
|
|
102
|
+
self.status(self.msg)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.error(e)
|
|
105
|
+
finally:
|
|
106
|
+
self.cleanup()
|
|
107
|
+
|
|
108
|
+
# ---------------------- Common helpers ----------------------
|
|
109
|
+
|
|
110
|
+
def _timeout(self) -> int:
|
|
111
|
+
try:
|
|
112
|
+
return int(self.plugin.get_option_value("net_timeout") or 30)
|
|
113
|
+
except Exception:
|
|
114
|
+
return 30
|
|
115
|
+
|
|
116
|
+
def prepare_path(self, path: str) -> str:
|
|
117
|
+
if path in [".", "./"]:
|
|
118
|
+
return self.plugin.window.core.config.get_user_dir("data")
|
|
119
|
+
if self.is_absolute_path(path):
|
|
120
|
+
return path
|
|
121
|
+
return os.path.join(self.plugin.window.core.config.get_user_dir("data"), path)
|
|
122
|
+
|
|
123
|
+
def is_absolute_path(self, path: str) -> bool:
|
|
124
|
+
return os.path.isabs(path)
|
|
125
|
+
|
|
126
|
+
def _prefer_system_ssh(self) -> bool:
|
|
127
|
+
return bool(self.plugin.get_option_value("prefer_system_ssh") or False)
|
|
128
|
+
|
|
129
|
+
def _ssh_bins(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"ssh": (self.plugin.get_option_value("ssh_binary") or "ssh").strip(),
|
|
132
|
+
"scp": (self.plugin.get_option_value("scp_binary") or "scp").strip(),
|
|
133
|
+
"sftp": (self.plugin.get_option_value("sftp_binary") or "sftp").strip(),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def _ssh_options(self) -> list[str]:
|
|
137
|
+
# Always enforce BatchMode=yes to avoid hanging on password prompt
|
|
138
|
+
extra = (self.plugin.get_option_value("ssh_options") or "").strip()
|
|
139
|
+
opts = ["-o", "BatchMode=yes"]
|
|
140
|
+
if extra:
|
|
141
|
+
opts += shlex.split(extra)
|
|
142
|
+
return opts
|
|
143
|
+
|
|
144
|
+
def _ssh_auto_add_hostkey(self) -> bool:
|
|
145
|
+
return bool(self.plugin.get_option_value("ssh_auto_add_hostkey") or True)
|
|
146
|
+
|
|
147
|
+
def _ftp_use_tls_default(self) -> bool:
|
|
148
|
+
return bool(self.plugin.get_option_value("ftp_use_tls_default") or False)
|
|
149
|
+
|
|
150
|
+
def _ftp_passive_default(self) -> bool:
|
|
151
|
+
return bool(self.plugin.get_option_value("ftp_passive_default") or True)
|
|
152
|
+
|
|
153
|
+
def _smtp_defaults(self) -> dict:
|
|
154
|
+
return {
|
|
155
|
+
"use_tls": bool(self.plugin.get_option_value("smtp_use_tls_default") or True),
|
|
156
|
+
"use_ssl": bool(self.plugin.get_option_value("smtp_use_ssl_default") or False),
|
|
157
|
+
"from_addr": (self.plugin.get_option_value("smtp_from_default") or "").strip(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _ensure_paramiko(self):
|
|
161
|
+
try:
|
|
162
|
+
import paramiko # noqa
|
|
163
|
+
except Exception:
|
|
164
|
+
raise RuntimeError("Paramiko not installed. Install: pip install paramiko")
|
|
165
|
+
|
|
166
|
+
def _service_from_port(self, port: int) -> str:
|
|
167
|
+
for k, v in self.SERVICE_PORTS.items():
|
|
168
|
+
if int(v) == int(port):
|
|
169
|
+
return k
|
|
170
|
+
return "unknown"
|
|
171
|
+
|
|
172
|
+
def _server_config(self, server: str, port: int) -> dict:
|
|
173
|
+
cfg = self.get_server_config(server, int(port)) # raises RuntimeError with available server names if not found
|
|
174
|
+
if not cfg.get("server") or not cfg.get("login"):
|
|
175
|
+
raise RuntimeError("Server config incomplete (server/login).")
|
|
176
|
+
# Never expose credentials
|
|
177
|
+
return cfg
|
|
178
|
+
|
|
179
|
+
def _shell_quote(self, s: str) -> str:
|
|
180
|
+
return shlex.quote(str(s))
|
|
181
|
+
|
|
182
|
+
def _build_remote_cmd(self, command: str, cwd: str | None = None, env: dict | None = None) -> str:
|
|
183
|
+
parts = []
|
|
184
|
+
if env:
|
|
185
|
+
exports = " ".join([f'{k}={self._shell_quote(v)}' for k, v in env.items()])
|
|
186
|
+
parts.append(f"export {exports}")
|
|
187
|
+
if cwd:
|
|
188
|
+
parts.append(f"cd {self._shell_quote(cwd)}")
|
|
189
|
+
parts.append(command)
|
|
190
|
+
joined = " && ".join(parts)
|
|
191
|
+
# Use bash -lc to ensure login-like shell behaviors (PATH, etc.)
|
|
192
|
+
return f"bash -lc {self._shell_quote(joined)}"
|
|
193
|
+
|
|
194
|
+
def get_server_config(self, server: str, port: int) -> dict:
|
|
195
|
+
"""
|
|
196
|
+
Get server configuration for given server and port.
|
|
197
|
+
|
|
198
|
+
:param server: server name or host
|
|
199
|
+
:param port: server port
|
|
200
|
+
:return: dict with server configuration (server, login, password, port)
|
|
201
|
+
"""
|
|
202
|
+
servers = self.plugin.get_option_value("servers")
|
|
203
|
+
available_ids = []
|
|
204
|
+
for srv in servers:
|
|
205
|
+
if not srv.get("enabled"):
|
|
206
|
+
continue
|
|
207
|
+
available_ids.append(srv.get("name"))
|
|
208
|
+
config = {}
|
|
209
|
+
for srv in servers:
|
|
210
|
+
if not srv.get("enabled"):
|
|
211
|
+
continue
|
|
212
|
+
if ((srv.get("name").lower() == server.lower()
|
|
213
|
+
or srv.get("host").lower() == server.lower())
|
|
214
|
+
and int(srv.get("port", 0)) == port):
|
|
215
|
+
config = {
|
|
216
|
+
"server": srv.get("host"),
|
|
217
|
+
"login": srv.get("login"),
|
|
218
|
+
"password": srv.get("password"),
|
|
219
|
+
"port": int(srv.get("port", 0)),
|
|
220
|
+
}
|
|
221
|
+
break
|
|
222
|
+
if not config:
|
|
223
|
+
raise RuntimeError(
|
|
224
|
+
f"Server configuration not found for {server}:{port}. Available servers: {', '.join(available_ids)}")
|
|
225
|
+
return config
|
|
226
|
+
|
|
227
|
+
# ---------------------- SSH (system) ----------------------
|
|
228
|
+
|
|
229
|
+
def _ssh_exec_system(self, host: str, port: int, user: str, command: str, cwd: str | None,
|
|
230
|
+
env: dict | None) -> dict:
|
|
231
|
+
bins = self._ssh_bins()
|
|
232
|
+
base = [bins["ssh"]] + self._ssh_options() + ["-p", str(port)]
|
|
233
|
+
remote = f"{user}@{host}"
|
|
234
|
+
rcmd = self._build_remote_cmd(command, cwd=cwd, env=env)
|
|
235
|
+
try:
|
|
236
|
+
r = subprocess.run(base + [remote, rcmd], capture_output=True, text=True, timeout=self._timeout())
|
|
237
|
+
return {"rc": r.returncode, "stdout": r.stdout, "stderr": r.stderr}
|
|
238
|
+
except subprocess.TimeoutExpired as e:
|
|
239
|
+
raise RuntimeError(f"SSH timeout after {self._timeout()}s: {e}")
|
|
240
|
+
except FileNotFoundError:
|
|
241
|
+
raise RuntimeError("ssh binary not found.")
|
|
242
|
+
|
|
243
|
+
def _scp_get_system(self, host: str, port: int, user: str, remote_path: str, local_path: str) -> dict:
|
|
244
|
+
bins = self._ssh_bins()
|
|
245
|
+
base = [bins["scp"]] + self._ssh_options() + ["-P", str(port)]
|
|
246
|
+
remote = f"{user}@{host}:{remote_path}"
|
|
247
|
+
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
|
248
|
+
try:
|
|
249
|
+
r = subprocess.run(base + [remote, local_path], capture_output=True, text=True, timeout=self._timeout())
|
|
250
|
+
if r.returncode != 0:
|
|
251
|
+
raise RuntimeError(f"SCP get failed: {r.stderr.strip()}")
|
|
252
|
+
size = os.path.getsize(local_path) if os.path.exists(local_path) else None
|
|
253
|
+
return {"saved_path": local_path, "size": size}
|
|
254
|
+
except subprocess.TimeoutExpired as e:
|
|
255
|
+
raise RuntimeError(f"SCP timeout after {self._timeout()}s: {e}")
|
|
256
|
+
except FileNotFoundError:
|
|
257
|
+
raise RuntimeError("scp binary not found.")
|
|
258
|
+
|
|
259
|
+
def _scp_put_system(self, host: str, port: int, user: str, local_path: str, remote_path: str) -> dict:
|
|
260
|
+
bins = self._ssh_bins()
|
|
261
|
+
base = [bins["scp"]] + self._ssh_options() + ["-P", str(port)]
|
|
262
|
+
if not os.path.exists(local_path):
|
|
263
|
+
raise RuntimeError(f"Local path not found: {local_path}")
|
|
264
|
+
remote = f"{user}@{host}:{remote_path}"
|
|
265
|
+
try:
|
|
266
|
+
r = subprocess.run(base + [local_path, remote], capture_output=True, text=True, timeout=self._timeout())
|
|
267
|
+
if r.returncode != 0:
|
|
268
|
+
raise RuntimeError(f"SCP put failed: {r.stderr.strip()}")
|
|
269
|
+
return {"uploaded": True, "source": local_path, "dest": remote_path}
|
|
270
|
+
except subprocess.TimeoutExpired as e:
|
|
271
|
+
raise RuntimeError(f"SCP timeout after {self._timeout()}s: {e}")
|
|
272
|
+
except FileNotFoundError:
|
|
273
|
+
raise RuntimeError("scp binary not found.")
|
|
274
|
+
|
|
275
|
+
def _ssh_ls_system(self, host: str, port: int, user: str, path: str | None) -> dict:
|
|
276
|
+
# Best-effort parse of `ls -la`. Not perfect on all systems.
|
|
277
|
+
p = path or "."
|
|
278
|
+
cmd = f'ls -la {self._shell_quote(p)}'
|
|
279
|
+
res = self._ssh_exec_system(host, port, user, cmd, cwd=None, env=None)
|
|
280
|
+
if res["rc"] != 0:
|
|
281
|
+
raise RuntimeError(f"ls failed: {res['stderr'].strip()}")
|
|
282
|
+
entries = []
|
|
283
|
+
for line in res["stdout"].splitlines():
|
|
284
|
+
if not line or line.startswith("total "):
|
|
285
|
+
continue
|
|
286
|
+
parts = line.split(maxsplit=8)
|
|
287
|
+
if len(parts) < 9:
|
|
288
|
+
continue
|
|
289
|
+
perms, _, owner, group, size, month, day, time_or_year, name = parts
|
|
290
|
+
if name in [".", ".."]:
|
|
291
|
+
continue
|
|
292
|
+
ftype = "file"
|
|
293
|
+
if perms.startswith("d"):
|
|
294
|
+
ftype = "dir"
|
|
295
|
+
elif perms.startswith("l"):
|
|
296
|
+
ftype = "link"
|
|
297
|
+
# size may fail to int if localized
|
|
298
|
+
try:
|
|
299
|
+
size_val = int(size)
|
|
300
|
+
except Exception:
|
|
301
|
+
size_val = None
|
|
302
|
+
entries.append({"name": name, "type": ftype, "size": size_val, "raw": line})
|
|
303
|
+
return {"entries": entries, "raw": res["stdout"]}
|
|
304
|
+
|
|
305
|
+
# ---------------------- SSH / SFTP (paramiko) ----------------------
|
|
306
|
+
|
|
307
|
+
def _ssh_exec_paramiko(self, host: str, port: int, user: str, password: str | None, command: str, cwd: str | None,
|
|
308
|
+
env: dict | None) -> dict:
|
|
309
|
+
self._ensure_paramiko()
|
|
310
|
+
import paramiko
|
|
311
|
+
client = paramiko.SSHClient()
|
|
312
|
+
if self._ssh_auto_add_hostkey():
|
|
313
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
314
|
+
try:
|
|
315
|
+
client.connect(
|
|
316
|
+
host,
|
|
317
|
+
port=port,
|
|
318
|
+
username=user,
|
|
319
|
+
password=password or None,
|
|
320
|
+
timeout=self._timeout(),
|
|
321
|
+
allow_agent=True,
|
|
322
|
+
look_for_keys=True,
|
|
323
|
+
)
|
|
324
|
+
rcmd = self._build_remote_cmd(command, cwd=cwd, env=env)
|
|
325
|
+
stdin, stdout, stderr = client.exec_command(rcmd, timeout=self._timeout())
|
|
326
|
+
out = stdout.read().decode("utf-8", errors="replace")
|
|
327
|
+
err = stderr.read().decode("utf-8", errors="replace")
|
|
328
|
+
rc = stdout.channel.recv_exit_status()
|
|
329
|
+
return {"rc": rc, "stdout": out, "stderr": err}
|
|
330
|
+
finally:
|
|
331
|
+
try:
|
|
332
|
+
client.close()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
def _sftp_opener(self, host: str, port: int, user: str, password: str | None):
|
|
337
|
+
self._ensure_paramiko()
|
|
338
|
+
import paramiko
|
|
339
|
+
client = paramiko.SSHClient()
|
|
340
|
+
if self._ssh_auto_add_hostkey():
|
|
341
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
342
|
+
client.connect(
|
|
343
|
+
host,
|
|
344
|
+
port=port,
|
|
345
|
+
username=user,
|
|
346
|
+
password=password or None,
|
|
347
|
+
timeout=self._timeout(),
|
|
348
|
+
allow_agent=True,
|
|
349
|
+
look_for_keys=True,
|
|
350
|
+
)
|
|
351
|
+
sftp = client.open_sftp()
|
|
352
|
+
return client, sftp
|
|
353
|
+
|
|
354
|
+
def _sftp_ls(self, host: str, port: int, user: str, password: str | None, path: str | None) -> dict:
|
|
355
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
356
|
+
try:
|
|
357
|
+
p = path or "."
|
|
358
|
+
attrs = sftp.listdir_attr(p)
|
|
359
|
+
out = []
|
|
360
|
+
for a in attrs:
|
|
361
|
+
name = getattr(a, "filename", None)
|
|
362
|
+
mode = a.st_mode if hasattr(a, "st_mode") else 0
|
|
363
|
+
ftype = "file"
|
|
364
|
+
if pystat.S_ISDIR(mode):
|
|
365
|
+
ftype = "dir"
|
|
366
|
+
elif pystat.S_ISLNK(mode):
|
|
367
|
+
ftype = "link"
|
|
368
|
+
out.append({
|
|
369
|
+
"name": name,
|
|
370
|
+
"type": ftype,
|
|
371
|
+
"size": getattr(a, "st_size", None),
|
|
372
|
+
"mtime": getattr(a, "st_mtime", None),
|
|
373
|
+
"mode": mode,
|
|
374
|
+
})
|
|
375
|
+
return {"entries": out}
|
|
376
|
+
finally:
|
|
377
|
+
try:
|
|
378
|
+
sftp.close()
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
try:
|
|
382
|
+
client.close()
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
def _sftp_get(self, host: str, port: int, user: str, password: str | None, remote_path: str,
|
|
387
|
+
local_path: str) -> dict:
|
|
388
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
389
|
+
try:
|
|
390
|
+
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
|
391
|
+
sftp.get(remote_path, local_path)
|
|
392
|
+
size = os.path.getsize(local_path)
|
|
393
|
+
return {"saved_path": local_path, "size": size}
|
|
394
|
+
finally:
|
|
395
|
+
try:
|
|
396
|
+
sftp.close()
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
try:
|
|
400
|
+
client.close()
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
def _sftp_put(self, host: str, port: int, user: str, password: str | None, local_path: str, remote_path: str,
|
|
405
|
+
make_dirs: bool) -> dict:
|
|
406
|
+
if not os.path.exists(local_path):
|
|
407
|
+
raise RuntimeError(f"Local path not found: {local_path}")
|
|
408
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
409
|
+
try:
|
|
410
|
+
if make_dirs:
|
|
411
|
+
# naive mkdir -p
|
|
412
|
+
dir_part = os.path.dirname(remote_path)
|
|
413
|
+
if dir_part:
|
|
414
|
+
self._sftp_mkdirs(sftp, dir_part)
|
|
415
|
+
sftp.put(local_path, remote_path)
|
|
416
|
+
return {"uploaded": True, "source": local_path, "dest": remote_path}
|
|
417
|
+
finally:
|
|
418
|
+
try:
|
|
419
|
+
sftp.close()
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
try:
|
|
423
|
+
client.close()
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
def _sftp_mkdirs(self, sftp, remote_dir: str):
|
|
428
|
+
parts = []
|
|
429
|
+
while True:
|
|
430
|
+
head, tail = os.path.split(remote_dir)
|
|
431
|
+
if tail:
|
|
432
|
+
parts.append(tail)
|
|
433
|
+
remote_dir = head
|
|
434
|
+
else:
|
|
435
|
+
if head:
|
|
436
|
+
parts.append(head)
|
|
437
|
+
break
|
|
438
|
+
parts = list(reversed([p for p in parts if p not in ["", "/"]]))
|
|
439
|
+
cur = ""
|
|
440
|
+
for p in parts:
|
|
441
|
+
if cur == "":
|
|
442
|
+
if remote_dir.startswith("/"):
|
|
443
|
+
cur = f"/{p}"
|
|
444
|
+
else:
|
|
445
|
+
cur = p
|
|
446
|
+
else:
|
|
447
|
+
cur = f"{cur}/{p}"
|
|
448
|
+
try:
|
|
449
|
+
sftp.stat(cur)
|
|
450
|
+
except Exception:
|
|
451
|
+
try:
|
|
452
|
+
sftp.mkdir(cur)
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
def _sftp_rm(self, host: str, port: int, user: str, password: str | None, remote_path: str) -> dict:
|
|
457
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
458
|
+
try:
|
|
459
|
+
st = sftp.stat(remote_path)
|
|
460
|
+
if pystat.S_ISDIR(st.st_mode):
|
|
461
|
+
# Non-recursive rmdir
|
|
462
|
+
sftp.rmdir(remote_path)
|
|
463
|
+
return {"removed": True, "path": remote_path, "type": "dir"}
|
|
464
|
+
else:
|
|
465
|
+
sftp.remove(remote_path)
|
|
466
|
+
return {"removed": True, "path": remote_path, "type": "file"}
|
|
467
|
+
finally:
|
|
468
|
+
try:
|
|
469
|
+
sftp.close()
|
|
470
|
+
except Exception:
|
|
471
|
+
pass
|
|
472
|
+
try:
|
|
473
|
+
client.close()
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
def _sftp_mkdir(self, host: str, port: int, user: str, password: str | None, remote_path: str,
|
|
478
|
+
exist_ok: bool) -> dict:
|
|
479
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
480
|
+
try:
|
|
481
|
+
try:
|
|
482
|
+
sftp.mkdir(remote_path)
|
|
483
|
+
except IOError:
|
|
484
|
+
if not exist_ok:
|
|
485
|
+
raise
|
|
486
|
+
return {"mkdir": True, "path": remote_path}
|
|
487
|
+
finally:
|
|
488
|
+
try:
|
|
489
|
+
sftp.close()
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
try:
|
|
493
|
+
client.close()
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
def _sftp_stat(self, host: str, port: int, user: str, password: str | None, remote_path: str) -> dict:
|
|
498
|
+
client, sftp = self._sftp_opener(host, port, user, password)
|
|
499
|
+
try:
|
|
500
|
+
st = sftp.stat(remote_path)
|
|
501
|
+
mode = st.st_mode
|
|
502
|
+
ftype = "file"
|
|
503
|
+
if pystat.S_ISDIR(mode):
|
|
504
|
+
ftype = "dir"
|
|
505
|
+
elif pystat.S_ISLNK(mode):
|
|
506
|
+
ftype = "link"
|
|
507
|
+
return {
|
|
508
|
+
"path": remote_path,
|
|
509
|
+
"type": ftype,
|
|
510
|
+
"size": getattr(st, "st_size", None),
|
|
511
|
+
"mtime": getattr(st, "st_mtime", None),
|
|
512
|
+
"mode": mode,
|
|
513
|
+
}
|
|
514
|
+
finally:
|
|
515
|
+
try:
|
|
516
|
+
sftp.close()
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
try:
|
|
520
|
+
client.close()
|
|
521
|
+
except Exception:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
# ---------------------- FTP / FTPS (stdlib) ----------------------
|
|
525
|
+
|
|
526
|
+
def _ftp_connect(self, host: str, port: int, user: str, password: str | None, use_tls: bool, passive: bool):
|
|
527
|
+
if use_tls:
|
|
528
|
+
ftp = ftplib.FTP_TLS()
|
|
529
|
+
else:
|
|
530
|
+
ftp = ftplib.FTP()
|
|
531
|
+
ftp.connect(host, port, timeout=self._timeout())
|
|
532
|
+
ftp.login(user=user, passwd=password or "")
|
|
533
|
+
if use_tls:
|
|
534
|
+
try:
|
|
535
|
+
ftp.prot_p() # secure data channel
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
ftp.set_pasv(passive)
|
|
539
|
+
return ftp
|
|
540
|
+
|
|
541
|
+
def _ftp_ls(self, host: str, port: int, user: str, password: str | None, path: str | None) -> dict:
|
|
542
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
543
|
+
passive = self._ftp_passive_default()
|
|
544
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
545
|
+
try:
|
|
546
|
+
p = path or "."
|
|
547
|
+
entries = []
|
|
548
|
+
# Prefer MLSD if supported
|
|
549
|
+
try:
|
|
550
|
+
for name, facts in ftp.mlsd(p):
|
|
551
|
+
ftype = facts.get("type", "file")
|
|
552
|
+
if ftype == "dir":
|
|
553
|
+
t = "dir"
|
|
554
|
+
elif ftype == "file":
|
|
555
|
+
t = "file"
|
|
556
|
+
else:
|
|
557
|
+
t = ftype
|
|
558
|
+
size = None
|
|
559
|
+
try:
|
|
560
|
+
size = int(facts.get("size")) if "size" in facts else None
|
|
561
|
+
except Exception:
|
|
562
|
+
pass
|
|
563
|
+
mtime = None
|
|
564
|
+
try:
|
|
565
|
+
# facts['modify'] like YYYYMMDDHHMMSS
|
|
566
|
+
mod = facts.get("modify")
|
|
567
|
+
if mod and len(mod) >= 14:
|
|
568
|
+
# convert to epoch best-effort
|
|
569
|
+
mtime = int(time.mktime(time.strptime(mod, "%Y%m%d%H%M%S")))
|
|
570
|
+
except Exception:
|
|
571
|
+
pass
|
|
572
|
+
entries.append({"name": name, "type": t, "size": size, "mtime": mtime})
|
|
573
|
+
except Exception:
|
|
574
|
+
# Fallback to NLST (names only)
|
|
575
|
+
names = ftp.nlst(p)
|
|
576
|
+
for name in names:
|
|
577
|
+
base = os.path.basename(name)
|
|
578
|
+
entries.append({"name": base, "type": "unknown"})
|
|
579
|
+
return {"entries": entries}
|
|
580
|
+
finally:
|
|
581
|
+
try:
|
|
582
|
+
ftp.quit()
|
|
583
|
+
except Exception:
|
|
584
|
+
pass
|
|
585
|
+
|
|
586
|
+
def _ftp_get(self, host: str, port: int, user: str, password: str | None, remote_path: str,
|
|
587
|
+
local_path: str) -> dict:
|
|
588
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
589
|
+
passive = self._ftp_passive_default()
|
|
590
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
591
|
+
try:
|
|
592
|
+
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
|
593
|
+
with open(local_path, "wb") as fh:
|
|
594
|
+
ftp.retrbinary(f"RETR {remote_path}", fh.write)
|
|
595
|
+
size = os.path.getsize(local_path)
|
|
596
|
+
return {"saved_path": local_path, "size": size}
|
|
597
|
+
finally:
|
|
598
|
+
try:
|
|
599
|
+
ftp.quit()
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
def _ftp_put(self, host: str, port: int, user: str, password: str | None, local_path: str,
|
|
604
|
+
remote_path: str) -> dict:
|
|
605
|
+
if not os.path.exists(local_path):
|
|
606
|
+
raise RuntimeError(f"Local path not found: {local_path}")
|
|
607
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
608
|
+
passive = self._ftp_passive_default()
|
|
609
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
610
|
+
try:
|
|
611
|
+
with open(local_path, "rb") as fh:
|
|
612
|
+
ftp.storbinary(f"STOR {remote_path}", fh)
|
|
613
|
+
return {"uploaded": True, "source": local_path, "dest": remote_path}
|
|
614
|
+
finally:
|
|
615
|
+
try:
|
|
616
|
+
ftp.quit()
|
|
617
|
+
except Exception:
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
def _ftp_rm(self, host: str, port: int, user: str, password: str | None, remote_path: str) -> dict:
|
|
621
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
622
|
+
passive = self._ftp_passive_default()
|
|
623
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
624
|
+
try:
|
|
625
|
+
try:
|
|
626
|
+
ftp.delete(remote_path)
|
|
627
|
+
return {"removed": True, "path": remote_path, "type": "file"}
|
|
628
|
+
except ftplib.error_perm:
|
|
629
|
+
# maybe it's a dir (non-recursive)
|
|
630
|
+
ftp.rmd(remote_path)
|
|
631
|
+
return {"removed": True, "path": remote_path, "type": "dir"}
|
|
632
|
+
finally:
|
|
633
|
+
try:
|
|
634
|
+
ftp.quit()
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
def _ftp_mkdir(self, host: str, port: int, user: str, password: str | None, remote_path: str,
|
|
639
|
+
exist_ok: bool) -> dict:
|
|
640
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
641
|
+
passive = self._ftp_passive_default()
|
|
642
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
643
|
+
try:
|
|
644
|
+
try:
|
|
645
|
+
ftp.mkd(remote_path)
|
|
646
|
+
except ftplib.error_perm:
|
|
647
|
+
if not exist_ok:
|
|
648
|
+
raise
|
|
649
|
+
return {"mkdir": True, "path": remote_path}
|
|
650
|
+
finally:
|
|
651
|
+
try:
|
|
652
|
+
ftp.quit()
|
|
653
|
+
except Exception:
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
def _ftp_stat(self, host: str, port: int, user: str, password: str | None, remote_path: str) -> dict:
|
|
657
|
+
use_tls = self._ftp_use_tls_default() or (int(port) in [990])
|
|
658
|
+
passive = self._ftp_passive_default()
|
|
659
|
+
ftp = self._ftp_connect(host, port, user, password, use_tls, passive)
|
|
660
|
+
try:
|
|
661
|
+
# Try MLST single path
|
|
662
|
+
try:
|
|
663
|
+
resp = []
|
|
664
|
+
ftp.sendcmd("TYPE I") # binary
|
|
665
|
+
ftp.sendcmd(f"MLST {remote_path}")
|
|
666
|
+
# ftplib doesn't parse MLST easily; use voidcmd and handle later is messy.
|
|
667
|
+
except Exception:
|
|
668
|
+
pass
|
|
669
|
+
# Fallback: list parent and find entry
|
|
670
|
+
parent = os.path.dirname(remote_path) or "."
|
|
671
|
+
name = os.path.basename(remote_path)
|
|
672
|
+
info = None
|
|
673
|
+
try:
|
|
674
|
+
for n, facts in ftp.mlsd(parent):
|
|
675
|
+
if n == name:
|
|
676
|
+
info = facts
|
|
677
|
+
break
|
|
678
|
+
except Exception:
|
|
679
|
+
pass
|
|
680
|
+
if info:
|
|
681
|
+
t = info.get("type", "file")
|
|
682
|
+
ftype = "dir" if t == "dir" else ("file" if t == "file" else t)
|
|
683
|
+
size = None
|
|
684
|
+
try:
|
|
685
|
+
size = int(info.get("size")) if "size" in info else None
|
|
686
|
+
except Exception:
|
|
687
|
+
pass
|
|
688
|
+
mtime = None
|
|
689
|
+
try:
|
|
690
|
+
mod = info.get("modify")
|
|
691
|
+
if mod and len(mod) >= 14:
|
|
692
|
+
mtime = int(time.mktime(time.strptime(mod, "%Y%m%d%H%M%S")))
|
|
693
|
+
except Exception:
|
|
694
|
+
pass
|
|
695
|
+
return {"path": remote_path, "type": ftype, "size": size, "mtime": mtime}
|
|
696
|
+
# As last resort, try size cmd (files only)
|
|
697
|
+
try:
|
|
698
|
+
ftp.sendcmd("TYPE I")
|
|
699
|
+
size = ftp.size(remote_path)
|
|
700
|
+
return {"path": remote_path, "type": "file", "size": size}
|
|
701
|
+
except Exception:
|
|
702
|
+
pass
|
|
703
|
+
# Unknown
|
|
704
|
+
return {"path": remote_path, "type": "unknown"}
|
|
705
|
+
finally:
|
|
706
|
+
try:
|
|
707
|
+
ftp.quit()
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
# ---------------------- Telnet (stdlib, best-effort) ----------------------
|
|
712
|
+
|
|
713
|
+
def _telnet_exec(self, host: str, port: int, user: str, password: str | None, command: str, login_prompt: str,
|
|
714
|
+
password_prompt: str, prompt: str) -> dict:
|
|
715
|
+
try:
|
|
716
|
+
import telnetlib # deprecated in newer Python, but still available in many
|
|
717
|
+
except Exception:
|
|
718
|
+
raise RuntimeError("telnetlib not available. Use SSH instead.")
|
|
719
|
+
timeout = self._timeout()
|
|
720
|
+
tn = telnetlib.Telnet(host, port, timeout=timeout)
|
|
721
|
+
try:
|
|
722
|
+
if login_prompt:
|
|
723
|
+
tn.read_until(login_prompt.encode("utf-8"), timeout=timeout)
|
|
724
|
+
tn.write((user + "\n").encode("utf-8"))
|
|
725
|
+
if password_prompt is not None:
|
|
726
|
+
tn.read_until(password_prompt.encode("utf-8"), timeout=timeout)
|
|
727
|
+
tn.write(((password or "") + "\n").encode("utf-8"))
|
|
728
|
+
if prompt:
|
|
729
|
+
tn.read_until(prompt.encode("utf-8"), timeout=timeout)
|
|
730
|
+
tn.write((command + "\n").encode("utf-8"))
|
|
731
|
+
out = tn.read_until(prompt.encode("utf-8"), timeout=timeout)
|
|
732
|
+
# crude strip
|
|
733
|
+
text = out.decode("utf-8", errors="replace")
|
|
734
|
+
return {"rc": 0, "stdout": text, "stderr": ""}
|
|
735
|
+
finally:
|
|
736
|
+
try:
|
|
737
|
+
tn.close()
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
# ---------------------- SMTP (stdlib) ----------------------
|
|
742
|
+
|
|
743
|
+
def _smtp_send_mail(self, host: str, port: int, user: str, password: str | None, mail: dict) -> dict:
|
|
744
|
+
defaults = self._smtp_defaults()
|
|
745
|
+
use_tls = bool(mail.get("use_tls", defaults["use_tls"]))
|
|
746
|
+
use_ssl = bool(mail.get("use_ssl", defaults["use_ssl"]))
|
|
747
|
+
from_addr = (mail.get("from_addr") or defaults["from_addr"] or user).strip()
|
|
748
|
+
to = mail.get("to")
|
|
749
|
+
if isinstance(to, str):
|
|
750
|
+
to = [to]
|
|
751
|
+
cc = mail.get("cc") or []
|
|
752
|
+
bcc = mail.get("bcc") or []
|
|
753
|
+
subject = mail.get("subject") or ""
|
|
754
|
+
body = mail.get("body") or ""
|
|
755
|
+
html = bool(mail.get("html") or False)
|
|
756
|
+
|
|
757
|
+
if not to:
|
|
758
|
+
raise RuntimeError("Param 'to' required for smtp_send.")
|
|
759
|
+
|
|
760
|
+
msg = EmailMessage()
|
|
761
|
+
msg["From"] = from_addr
|
|
762
|
+
msg["To"] = ", ".join(to)
|
|
763
|
+
if cc:
|
|
764
|
+
msg["Cc"] = ", ".join(cc)
|
|
765
|
+
msg["Subject"] = subject
|
|
766
|
+
if html:
|
|
767
|
+
msg.add_alternative(body, subtype="html")
|
|
768
|
+
else:
|
|
769
|
+
msg.set_content(body)
|
|
770
|
+
|
|
771
|
+
# Attachments (local files)
|
|
772
|
+
for apath in (mail.get("attachments") or []):
|
|
773
|
+
lp = self.prepare_path(apath)
|
|
774
|
+
if not os.path.exists(lp):
|
|
775
|
+
raise RuntimeError(f"Attachment not found: {lp}")
|
|
776
|
+
with open(lp, "rb") as f:
|
|
777
|
+
data = f.read()
|
|
778
|
+
maintype, subtype = "application", "octet-stream"
|
|
779
|
+
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=os.path.basename(lp))
|
|
780
|
+
|
|
781
|
+
if use_ssl or int(port) in [465]:
|
|
782
|
+
smtp = smtplib.SMTP_SSL(host, int(port), timeout=self._timeout())
|
|
783
|
+
else:
|
|
784
|
+
smtp = smtplib.SMTP(host, int(port), timeout=self._timeout())
|
|
785
|
+
try:
|
|
786
|
+
smtp.ehlo()
|
|
787
|
+
if use_tls and not use_ssl:
|
|
788
|
+
smtp.starttls()
|
|
789
|
+
smtp.ehlo()
|
|
790
|
+
if user:
|
|
791
|
+
smtp.login(user, password or "")
|
|
792
|
+
rcpt = to + (cc if isinstance(cc, list) else []) + (bcc if isinstance(bcc, list) else [])
|
|
793
|
+
smtp.send_message(msg, from_addr=from_addr, to_addrs=rcpt)
|
|
794
|
+
return {"sent": True, "to": rcpt}
|
|
795
|
+
finally:
|
|
796
|
+
try:
|
|
797
|
+
smtp.quit()
|
|
798
|
+
except Exception:
|
|
799
|
+
pass
|
|
800
|
+
|
|
801
|
+
# ---------------------- Commands ----------------------
|
|
802
|
+
|
|
803
|
+
def cmd_srv_exec(self, item: dict) -> dict:
|
|
804
|
+
p = item.get("params", {}) or {}
|
|
805
|
+
server = p.get("server")
|
|
806
|
+
port = int(p.get("port") or 0)
|
|
807
|
+
command = p.get("command")
|
|
808
|
+
cwd = p.get("cwd")
|
|
809
|
+
env = p.get("env") or {}
|
|
810
|
+
if not (server and port and command):
|
|
811
|
+
return self.make_response(item, "Params 'server', 'port' and 'command' required.")
|
|
812
|
+
|
|
813
|
+
cfg = self._server_config(server, port)
|
|
814
|
+
host = cfg.get("server")
|
|
815
|
+
user = cfg.get("login")
|
|
816
|
+
password = cfg.get("password")
|
|
817
|
+
|
|
818
|
+
svc = self._service_from_port(port)
|
|
819
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
820
|
+
if self._prefer_system_ssh():
|
|
821
|
+
res = self._ssh_exec_system(host, port, user, command, cwd=cwd, env=env)
|
|
822
|
+
return self.make_response(item, res)
|
|
823
|
+
else:
|
|
824
|
+
res = self._ssh_exec_paramiko(host, port, user, password, command, cwd=cwd, env=env)
|
|
825
|
+
return self.make_response(item, res)
|
|
826
|
+
elif svc == "telnet" or int(port) == 23:
|
|
827
|
+
login_prompt = (self.plugin.get_option_value("telnet_login_prompt") or "login:").strip()
|
|
828
|
+
password_prompt = (self.plugin.get_option_value("telnet_password_prompt") or "Password:").strip()
|
|
829
|
+
prompt = (self.plugin.get_option_value("telnet_prompt") or "$ ").strip()
|
|
830
|
+
res = self._telnet_exec(host, port, user, password, command, login_prompt, password_prompt, prompt)
|
|
831
|
+
return self.make_response(item, res)
|
|
832
|
+
else:
|
|
833
|
+
return self.make_response(item, f"srv_exec not supported for port {port} ({svc}).")
|
|
834
|
+
|
|
835
|
+
def cmd_srv_ls(self, item: dict) -> dict:
|
|
836
|
+
p = item.get("params", {}) or {}
|
|
837
|
+
server = p.get("server")
|
|
838
|
+
port = int(p.get("port") or 0)
|
|
839
|
+
path = p.get("path") or "."
|
|
840
|
+
if not (server and port):
|
|
841
|
+
return self.make_response(item, "Params 'server' and 'port' required.")
|
|
842
|
+
cfg = self._server_config(server, port)
|
|
843
|
+
host = cfg.get("server")
|
|
844
|
+
user = cfg.get("login")
|
|
845
|
+
password = cfg.get("password")
|
|
846
|
+
svc = self._service_from_port(port)
|
|
847
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
848
|
+
if self._prefer_system_ssh():
|
|
849
|
+
res = self._ssh_ls_system(host, port, user, path)
|
|
850
|
+
return self.make_response(item, res)
|
|
851
|
+
else:
|
|
852
|
+
res = self._sftp_ls(host, port, user, password, path)
|
|
853
|
+
return self.make_response(item, res)
|
|
854
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
855
|
+
res = self._ftp_ls(host, port, user, password, path)
|
|
856
|
+
return self.make_response(item, res)
|
|
857
|
+
else:
|
|
858
|
+
return self.make_response(item, f"srv_ls not supported for port {port} ({svc}).")
|
|
859
|
+
|
|
860
|
+
def cmd_srv_get(self, item: dict) -> dict:
|
|
861
|
+
p = item.get("params", {}) or {}
|
|
862
|
+
server = p.get("server")
|
|
863
|
+
port = int(p.get("port") or 0)
|
|
864
|
+
remote_path = p.get("remote_path")
|
|
865
|
+
local_path = p.get("local_path")
|
|
866
|
+
overwrite = bool(p.get("overwrite", True))
|
|
867
|
+
if not (server and port and remote_path):
|
|
868
|
+
return self.make_response(item, "Params 'server', 'port' and 'remote_path' required.")
|
|
869
|
+
if not local_path:
|
|
870
|
+
local_path = self.prepare_path(os.path.basename(remote_path))
|
|
871
|
+
else:
|
|
872
|
+
local_path = self.prepare_path(local_path)
|
|
873
|
+
if os.path.exists(local_path) and not overwrite:
|
|
874
|
+
return self.make_response(item, f"Local path exists and overwrite=False: {local_path}")
|
|
875
|
+
|
|
876
|
+
cfg = self._server_config(server, port)
|
|
877
|
+
host = cfg.get("server")
|
|
878
|
+
user = cfg.get("login")
|
|
879
|
+
password = cfg.get("password")
|
|
880
|
+
svc = self._service_from_port(port)
|
|
881
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
882
|
+
if self._prefer_system_ssh():
|
|
883
|
+
res = self._scp_get_system(host, port, user, remote_path, local_path)
|
|
884
|
+
return self.make_response(item, res)
|
|
885
|
+
else:
|
|
886
|
+
res = self._sftp_get(host, port, user, password, remote_path, local_path)
|
|
887
|
+
return self.make_response(item, res)
|
|
888
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
889
|
+
res = self._ftp_get(host, port, user, password, remote_path, local_path)
|
|
890
|
+
return self.make_response(item, res)
|
|
891
|
+
else:
|
|
892
|
+
return self.make_response(item, f"srv_get not supported for port {port} ({svc}).")
|
|
893
|
+
|
|
894
|
+
def cmd_srv_put(self, item: dict) -> dict:
|
|
895
|
+
p = item.get("params", {}) or {}
|
|
896
|
+
server = p.get("server")
|
|
897
|
+
port = int(p.get("port") or 0)
|
|
898
|
+
local_path = p.get("local_path")
|
|
899
|
+
remote_path = p.get("remote_path")
|
|
900
|
+
make_dirs = bool(p.get("make_dirs", True))
|
|
901
|
+
if not (server and port and local_path):
|
|
902
|
+
return self.make_response(item, "Params 'server', 'port' and 'local_path' required.")
|
|
903
|
+
lp = self.prepare_path(local_path)
|
|
904
|
+
if not os.path.exists(lp):
|
|
905
|
+
return self.make_response(item, f"Local path not found: {lp}")
|
|
906
|
+
if not remote_path:
|
|
907
|
+
remote_path = os.path.basename(lp)
|
|
908
|
+
|
|
909
|
+
cfg = self._server_config(server, port)
|
|
910
|
+
host = cfg.get("server")
|
|
911
|
+
user = cfg.get("login")
|
|
912
|
+
password = cfg.get("password")
|
|
913
|
+
svc = self._service_from_port(port)
|
|
914
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
915
|
+
if self._prefer_system_ssh():
|
|
916
|
+
# System scp cannot create remote dirs automatically; rely on user setting up path
|
|
917
|
+
res = self._scp_put_system(host, port, user, lp, remote_path)
|
|
918
|
+
return self.make_response(item, res)
|
|
919
|
+
else:
|
|
920
|
+
res = self._sftp_put(host, port, user, password, lp, remote_path, make_dirs=make_dirs)
|
|
921
|
+
return self.make_response(item, res)
|
|
922
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
923
|
+
res = self._ftp_put(host, port, user, password, lp, remote_path)
|
|
924
|
+
return self.make_response(item, res)
|
|
925
|
+
else:
|
|
926
|
+
return self.make_response(item, f"srv_put not supported for port {port} ({svc}).")
|
|
927
|
+
|
|
928
|
+
def cmd_srv_rm(self, item: dict) -> dict:
|
|
929
|
+
p = item.get("params", {}) or {}
|
|
930
|
+
server = p.get("server")
|
|
931
|
+
port = int(p.get("port") or 0)
|
|
932
|
+
remote_path = p.get("remote_path")
|
|
933
|
+
if not (server and port and remote_path):
|
|
934
|
+
return self.make_response(item, "Params 'server', 'port' and 'remote_path' required.")
|
|
935
|
+
cfg = self._server_config(server, port)
|
|
936
|
+
host = cfg.get("server")
|
|
937
|
+
user = cfg.get("login")
|
|
938
|
+
password = cfg.get("password")
|
|
939
|
+
svc = self._service_from_port(port)
|
|
940
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
941
|
+
if self._prefer_system_ssh():
|
|
942
|
+
# Use rm/rmdir via SSH (non-recursive)
|
|
943
|
+
cmd = f"if [ -d {self._shell_quote(remote_path)} ]; then rmdir {self._shell_quote(remote_path)}; else rm -f {self._shell_quote(remote_path)}; fi"
|
|
944
|
+
res = self._ssh_exec_system(host, port, user, cmd, cwd=None, env=None)
|
|
945
|
+
if res["rc"] != 0:
|
|
946
|
+
raise RuntimeError(f"remove failed: {res['stderr'].strip()}")
|
|
947
|
+
return self.make_response(item, {"removed": True, "path": remote_path})
|
|
948
|
+
else:
|
|
949
|
+
res = self._sftp_rm(host, port, user, password, remote_path)
|
|
950
|
+
return self.make_response(item, res)
|
|
951
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
952
|
+
res = self._ftp_rm(host, port, user, password, remote_path)
|
|
953
|
+
return self.make_response(item, res)
|
|
954
|
+
else:
|
|
955
|
+
return self.make_response(item, f"srv_rm not supported for port {port} ({svc}).")
|
|
956
|
+
|
|
957
|
+
def cmd_srv_mkdir(self, item: dict) -> dict:
|
|
958
|
+
p = item.get("params", {}) or {}
|
|
959
|
+
server = p.get("server")
|
|
960
|
+
port = int(p.get("port") or 0)
|
|
961
|
+
remote_path = p.get("remote_path")
|
|
962
|
+
exist_ok = bool(p.get("exist_ok", True))
|
|
963
|
+
if not (server and port and remote_path):
|
|
964
|
+
return self.make_response(item, "Params 'server', 'port' and 'remote_path' required.")
|
|
965
|
+
cfg = self._server_config(server, port)
|
|
966
|
+
host = cfg.get("server")
|
|
967
|
+
user = cfg.get("login")
|
|
968
|
+
password = cfg.get("password")
|
|
969
|
+
svc = self._service_from_port(port)
|
|
970
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
971
|
+
if self._prefer_system_ssh():
|
|
972
|
+
cmd = f"mkdir {'-p ' if exist_ok else ''}{self._shell_quote(remote_path)}"
|
|
973
|
+
res = self._ssh_exec_system(host, port, user, cmd, cwd=None, env=None)
|
|
974
|
+
if res["rc"] != 0:
|
|
975
|
+
raise RuntimeError(f"mkdir failed: {res['stderr'].strip()}")
|
|
976
|
+
return self.make_response(item, {"mkdir": True, "path": remote_path})
|
|
977
|
+
else:
|
|
978
|
+
res = self._sftp_mkdir(host, port, user, password, remote_path, exist_ok=exist_ok)
|
|
979
|
+
return self.make_response(item, res)
|
|
980
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
981
|
+
res = self._ftp_mkdir(host, port, user, password, remote_path, exist_ok=exist_ok)
|
|
982
|
+
return self.make_response(item, res)
|
|
983
|
+
else:
|
|
984
|
+
return self.make_response(item, f"srv_mkdir not supported for port {port} ({svc}).")
|
|
985
|
+
|
|
986
|
+
def cmd_srv_stat(self, item: dict) -> dict:
|
|
987
|
+
p = item.get("params", {}) or {}
|
|
988
|
+
server = p.get("server")
|
|
989
|
+
port = int(p.get("port") or 0)
|
|
990
|
+
remote_path = p.get("remote_path")
|
|
991
|
+
if not (server and port and remote_path):
|
|
992
|
+
return self.make_response(item, "Params 'server', 'port' and 'remote_path' required.")
|
|
993
|
+
cfg = self._server_config(server, port)
|
|
994
|
+
host = cfg.get("server")
|
|
995
|
+
user = cfg.get("login")
|
|
996
|
+
password = cfg.get("password")
|
|
997
|
+
svc = self._service_from_port(port)
|
|
998
|
+
if svc in ["ssh", "sftp", "unknown"] or int(port) == 22:
|
|
999
|
+
if self._prefer_system_ssh():
|
|
1000
|
+
# Try 'stat' then fallback to test
|
|
1001
|
+
cmd = f"(stat -c '%F|%s|%Y' {self._shell_quote(remote_path)} 2>/dev/null) || echo 'UNKNOWN|||';"
|
|
1002
|
+
res = self._ssh_exec_system(host, port, user, cmd, cwd=None, env=None)
|
|
1003
|
+
if res["rc"] != 0:
|
|
1004
|
+
raise RuntimeError(f"stat failed: {res['stderr'].strip()}")
|
|
1005
|
+
line = res["stdout"].strip().splitlines()[-1] if res["stdout"] else "UNKNOWN|||"
|
|
1006
|
+
kind, size, mtime = (line.split("|") + ["", "", ""])[:3]
|
|
1007
|
+
ftype = "file"
|
|
1008
|
+
if "directory" in kind.lower():
|
|
1009
|
+
ftype = "dir"
|
|
1010
|
+
elif "link" in kind.lower():
|
|
1011
|
+
ftype = "link"
|
|
1012
|
+
size_val = None
|
|
1013
|
+
try:
|
|
1014
|
+
size_val = int(size) if size else None
|
|
1015
|
+
except Exception:
|
|
1016
|
+
pass
|
|
1017
|
+
mtime_val = None
|
|
1018
|
+
try:
|
|
1019
|
+
mtime_val = int(mtime) if mtime else None
|
|
1020
|
+
except Exception:
|
|
1021
|
+
pass
|
|
1022
|
+
return self.make_response(item,
|
|
1023
|
+
{"path": remote_path, "type": ftype, "size": size_val, "mtime": mtime_val})
|
|
1024
|
+
else:
|
|
1025
|
+
res = self._sftp_stat(host, port, user, password, remote_path)
|
|
1026
|
+
return self.make_response(item, res)
|
|
1027
|
+
elif svc == "ftp" or int(port) in [21, 990]:
|
|
1028
|
+
res = self._ftp_stat(host, port, user, password, remote_path)
|
|
1029
|
+
return self.make_response(item, res)
|
|
1030
|
+
else:
|
|
1031
|
+
return self.make_response(item, f"srv_stat not supported for port {port} ({svc}).")
|
|
1032
|
+
|
|
1033
|
+
def cmd_smtp_send(self, item: dict) -> dict:
|
|
1034
|
+
p = item.get("params", {}) or {}
|
|
1035
|
+
server = p.get("server")
|
|
1036
|
+
port = int(p.get("port") or 0)
|
|
1037
|
+
if not (server and port):
|
|
1038
|
+
return self.make_response(item, "Params 'server' and 'port' required.")
|
|
1039
|
+
cfg = self._server_config(server, port)
|
|
1040
|
+
host = cfg.get("server")
|
|
1041
|
+
user = cfg.get("login")
|
|
1042
|
+
password = cfg.get("password")
|
|
1043
|
+
|
|
1044
|
+
mail = {
|
|
1045
|
+
"from_addr": p.get("from_addr"),
|
|
1046
|
+
"to": p.get("to"),
|
|
1047
|
+
"cc": p.get("cc"),
|
|
1048
|
+
"bcc": p.get("bcc"),
|
|
1049
|
+
"subject": p.get("subject"),
|
|
1050
|
+
"body": p.get("body"),
|
|
1051
|
+
"html": bool(p.get("html") or False),
|
|
1052
|
+
"attachments": p.get("attachments") or [],
|
|
1053
|
+
"use_tls": p.get("use_tls"),
|
|
1054
|
+
"use_ssl": p.get("use_ssl"),
|
|
1055
|
+
}
|
|
1056
|
+
res = self._smtp_send_mail(host, port, user, password, mail)
|
|
1057
|
+
return self.make_response(item, res)
|