- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display - FC原生调用为主,prompt-JSON保底 - 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search) - 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件) - 清理 constants.py 死代码 - 版本号更新为 7.0.0
296 lines
11 KiB
Python
296 lines
11 KiB
Python
import requests
|
|
import time
|
|
import socket
|
|
import ssl
|
|
import json
|
|
from urllib.parse import urlparse
|
|
from colorama import Fore, Style
|
|
|
|
|
|
class Liugin:
|
|
"""网络工具插件"""
|
|
|
|
_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
|
|
|
def __init__(self):
|
|
self.cli = None
|
|
self.timeout = 10
|
|
self.max_retries = 2
|
|
|
|
def set_cli(self, cli):
|
|
self.cli = cli
|
|
self.cli.register_liugin_command('net', self.command_handler)
|
|
self.cli.register_liugin_command('ping', lambda a: self.handle(f"ping {a}"))
|
|
self.cli.register_liugin_command('website', lambda a: self.handle(f"status {a}"))
|
|
|
|
def command_handler(self, args):
|
|
parts = args.strip().split(maxsplit=1)
|
|
if not parts:
|
|
return "用法: network_tools <操作> <参数>"
|
|
return self.handle(f"{parts[0].lower()} {parts[1] if len(parts) > 1 else ''}")
|
|
|
|
def get_tool_info(self):
|
|
return {
|
|
"name": "network_tools",
|
|
"description": "网络工具:ping/get/status/headers/ip/dns/ssl/port/post/put/delete",
|
|
"keywords": ["网络", "ping", "get", "post", "dns", "ssl", "port", "ip", "headers", "status"],
|
|
"usage": (
|
|
"network_tools <操作> <参数>\n\n"
|
|
"读取: get/status/headers <URL>\n"
|
|
"诊断: ping/dns/ssl <域名>\n"
|
|
"扫描: port <域名> [端口范围]\n"
|
|
"请求: post/put/delete <URL> [JSON体]\n"
|
|
"解析: ip <域名>"
|
|
),
|
|
}
|
|
|
|
def get_mcp_definition(self):
|
|
return {
|
|
"name": "network_tools",
|
|
"description": "网络工具:读取网页、诊断连接、发送HTTP请求、DNS/SSL/端口扫描",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operation": {
|
|
"type": "string",
|
|
"enum": ["ping", "get", "status", "headers", "ip", "dns", "ssl", "port", "post", "put", "delete"],
|
|
},
|
|
"target": {"type": "string", "description": "URL、域名或IP"},
|
|
"extra": {"type": "string", "description": "附加参数:JSON body 或端口范围"}
|
|
},
|
|
"required": ["operation", "target"]
|
|
}
|
|
}
|
|
|
|
def convert_mcp_args(self, arguments):
|
|
op = arguments.get("operation", "")
|
|
target = arguments.get("target", "")
|
|
extra = arguments.get("extra", "")
|
|
return f"{op} {target} {extra}".strip()
|
|
|
|
def handle(self, args):
|
|
try:
|
|
parts = args.strip().split(maxsplit=2)
|
|
if not parts:
|
|
return "参数不足"
|
|
op = parts[0].lower()
|
|
target = parts[1].strip() if len(parts) > 1 else ""
|
|
extra = parts[2].strip() if len(parts) > 2 else ""
|
|
|
|
if op == "post":
|
|
return self._api(target, "POST", extra)
|
|
elif op == "put":
|
|
return self._api(target, "PUT", extra)
|
|
elif op == "delete":
|
|
return self._api(target, "DELETE", extra)
|
|
elif op == "ping":
|
|
return self._ping(target)
|
|
elif op == "get":
|
|
return self._get(target)
|
|
elif op == "status":
|
|
return self._status(target)
|
|
elif op == "headers":
|
|
return self._headers(target)
|
|
elif op == "ip":
|
|
return self._ip(target)
|
|
elif op == "dns":
|
|
return self._dns(target)
|
|
elif op == "ssl":
|
|
return self._ssl(target)
|
|
elif op == "port":
|
|
return self._port(target, extra)
|
|
else:
|
|
return f"不支持: {op}。支持: ping/get/status/headers/ip/dns/ssl/port/post/put/delete"
|
|
except Exception as e:
|
|
return f"错误: {e}"
|
|
|
|
def _url(self, t):
|
|
return t if t.startswith(('http://', 'https://')) else 'https://' + t
|
|
|
|
def _req(self, url, method='GET', data=None, ct=None):
|
|
headers = {'User-Agent': self._UA, 'Accept': '*/*'}
|
|
if ct:
|
|
headers['Content-Type'] = ct
|
|
for i in range(self.max_retries + 1):
|
|
try:
|
|
r = requests.request(method, url, timeout=self.timeout, headers=headers, allow_redirects=True,
|
|
json=data if ct == 'application/json' else None,
|
|
data=data if (ct and ct != 'application/json') else None)
|
|
if not r.encoding or r.encoding.lower() == 'iso-8859-1':
|
|
r.encoding = r.apparent_encoding or 'utf-8'
|
|
return r
|
|
except requests.exceptions.Timeout:
|
|
if i < self.max_retries:
|
|
time.sleep(1)
|
|
continue
|
|
return None
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
def _ping(self, t):
|
|
url = self._url(t)
|
|
start = time.time()
|
|
r = self._req(url)
|
|
ms = (time.time() - start) * 1000
|
|
if not r:
|
|
return f"Ping {t}: 连接失败"
|
|
status = "成功" if r.status_code < 400 else f"HTTP {r.status_code}"
|
|
extra = f"\n 重定向: {r.url}" if r.url != url else ""
|
|
return f"Ping {t}:\n 状态: {status}\n 延迟: {ms:.0f}ms\n HTTP: {r.status_code}{extra}"
|
|
|
|
def _get(self, t):
|
|
url = self._url(t)
|
|
r = self._req(url)
|
|
if not r:
|
|
return f"GET {t}: 失败"
|
|
content = r.text
|
|
return f"GET {url} [{r.status_code}]:\n{content}"
|
|
|
|
def _status(self, t):
|
|
url = self._url(t)
|
|
r = self._req(url)
|
|
if not r:
|
|
return f"STATUS {t}: 失败"
|
|
return (
|
|
f"STATUS {url}:\n"
|
|
f" 状态码: {r.status_code} {r.reason}\n"
|
|
f" 延迟: {r.elapsed.total_seconds()*1000:.0f}ms\n"
|
|
f" 大小: {len(r.content)} bytes\n"
|
|
f" 服务器: {r.headers.get('Server', '?')}\n"
|
|
f" 类型: {r.headers.get('Content-Type', '?')}\n"
|
|
f" 最终URL: {r.url}"
|
|
)
|
|
|
|
def _headers(self, t):
|
|
url = self._url(t)
|
|
r = self._req(url, method='HEAD')
|
|
if not r:
|
|
r = self._req(url)
|
|
if not r:
|
|
return f"HEADERS {t}: 失败"
|
|
lines = [f"HEADERS {url}:"]
|
|
for k, v in r.headers.items():
|
|
lines.append(f" {k}: {v}")
|
|
return "\n".join(lines)
|
|
|
|
def _ip(self, t):
|
|
if t.startswith(('http://', 'https://')):
|
|
t = urlparse(t).netloc
|
|
try:
|
|
ips = list(set(addr[4][0] for addr in socket.getaddrinfo(t, None)))
|
|
return f"IP {t}:\n" + "\n".join(f" {ip}" for ip in ips)
|
|
except socket.gaierror:
|
|
return f"IP {t}: 解析失败"
|
|
|
|
def _dns(self, t):
|
|
if t.startswith(('http://', 'https://')):
|
|
t = urlparse(t).netloc
|
|
lines = [f"DNS {t}:"]
|
|
try:
|
|
seen = set()
|
|
for item in socket.getaddrinfo(t, None):
|
|
ip = item[4][0]
|
|
fam = "IPv4" if item[0] == socket.AF_INET else "IPv6"
|
|
if (ip, fam) not in seen:
|
|
seen.add((ip, fam))
|
|
lines.append(f" {fam}: {ip}")
|
|
except socket.gaierror as e:
|
|
lines.append(f" 失败: {e}")
|
|
fqdn = socket.getfqdn(t)
|
|
if fqdn != t:
|
|
lines.append(f" FQDN: {fqdn}")
|
|
return "\n".join(lines)
|
|
|
|
def _ssl(self, t):
|
|
if t.startswith(('http://', 'https://')):
|
|
t = urlparse(t).netloc
|
|
host = t.split(':')[0]
|
|
lines = [f"SSL {t}:"]
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
|
|
s.settimeout(self.timeout)
|
|
s.connect((host, 443))
|
|
cert = s.getpeercert()
|
|
subj = dict(x[0] for x in cert.get('subject', []))
|
|
iss = dict(x[0] for x in cert.get('issuer', []))
|
|
lines.append(f" 主题: {subj.get('commonName', '?')}")
|
|
lines.append(f" 颁发: {iss.get('organizationName', iss.get('commonName', '?'))}")
|
|
lines.append(f" 生效: {cert.get('notBefore', '?')}")
|
|
lines.append(f" 过期: {cert.get('notAfter', '?')}")
|
|
san = cert.get('subjectAltName', [])
|
|
if san:
|
|
doms = [v for _, v in san[:5]]
|
|
lines.append(f" SAN: {', '.join(doms)}{'...' if len(san) > 5 else ''}")
|
|
lines.append(f" 状态: 有效")
|
|
except ssl.SSLCertVerificationError as e:
|
|
lines.append(f" 状态: 验证失败 - {e}")
|
|
except socket.timeout:
|
|
lines.append(f" 状态: 超时")
|
|
except ConnectionRefusedError:
|
|
lines.append(f" 状态: 连接被拒绝")
|
|
except Exception as e:
|
|
lines.append(f" 状态: {e}")
|
|
return "\n".join(lines)
|
|
|
|
def _port(self, t, rng=''):
|
|
if t.startswith(('http://', 'https://')):
|
|
t = urlparse(t).netloc.split(':')[0]
|
|
if '-' in rng:
|
|
try:
|
|
s, e = rng.split('-', 1)
|
|
s, e = max(1, int(s)), min(65535, int(e))
|
|
except ValueError:
|
|
s, e = 1, 1024
|
|
else:
|
|
s, e = 1, 1024
|
|
try:
|
|
ip = socket.gethostbyname(t)
|
|
except socket.gaierror:
|
|
return f"PORT {t}: 解析失败"
|
|
common = {21:'FTP',22:'SSH',23:'Telnet',25:'SMTP',53:'DNS',80:'HTTP',110:'POP3',143:'IMAP',443:'HTTPS',993:'IMAPS',995:'POP3S',3306:'MySQL',3389:'RDP',5432:'PostgreSQL',6379:'Redis',8080:'HTTP-Alt',8443:'HTTPS-Alt',27017:'MongoDB'}
|
|
open_p = []
|
|
for p in range(s, e + 1):
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(0.3)
|
|
if sock.connect_ex((ip, p)) == 0:
|
|
open_p.append((p, common.get(p, '?')))
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
lines = [f"PORT {t} ({s}-{e}), IP={ip}:"]
|
|
if open_p:
|
|
for p, svc in sorted(open_p):
|
|
lines.append(f" {p}/{svc}")
|
|
else:
|
|
lines.append(f" 无开放端口")
|
|
return "\n".join(lines)
|
|
|
|
def _api(self, url, method, body_str):
|
|
url = self._url(url)
|
|
data = None
|
|
ct = None
|
|
if body_str:
|
|
try:
|
|
data = json.loads(body_str)
|
|
ct = 'application/json'
|
|
except json.JSONDecodeError:
|
|
data = body_str
|
|
ct = 'text/plain'
|
|
r = self._req(url, method=method, data=data, ct=ct)
|
|
if not r:
|
|
return f"{method} {url}: 失败"
|
|
lines = [f"{method} {url} [{r.status_code}]"]
|
|
ctype = r.headers.get('Content-Type', '')
|
|
if 'json' in ctype:
|
|
try:
|
|
body = json.dumps(r.json(), ensure_ascii=False, indent=2)
|
|
lines.append(body)
|
|
except Exception:
|
|
lines.append(r.text)
|
|
else:
|
|
lines.append(r.text)
|
|
return "\n".join(lines)
|