Add FastLink_Lite custom rules enhancement
- Add custom rules editor to LUCI web interface - Support 32 rule types (DOMAIN, IP-CIDR, GEOSITE, etc.) - Rule CRUD, sorting, batch import, templates - netflow-rules script for mihomo config injection - Standalone IPK repackaging support
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""Build LuCI template version from working file."""
|
||||
with open(r'E:\AI Project\WangAgent\_tmp_extract\router_live.htm', 'r', encoding='utf-8') as f:
|
||||
c = f.read()
|
||||
|
||||
# Remove standalone HTML wrapper (everything before <style> and after </script>)
|
||||
# Current file structure: <!DOCTYPE html>\n<html...><head...><style>...</style>...<script>...</script></body></html>
|
||||
# LuCI template needs: <%+header%>\n<style>...</style>\n...\n<script>...</script>\n<%+footer%>
|
||||
|
||||
# Find the actual content
|
||||
style_start = c.find('<style>')
|
||||
script_end = c.find('</script>') + 9
|
||||
|
||||
# Remove the outer HTML wrapper
|
||||
content = c[style_start:script_end]
|
||||
|
||||
# Add LuCI template tags
|
||||
content = '<%+header%>\n' + content + '\n<%+footer%>'
|
||||
|
||||
# Replace API URLs
|
||||
old_api = "var apiUrl = '<%=luci.dispatcher.build_url(\"admin\", \"services\", \"netflow\", \"api\")%>';"
|
||||
new_api = "var apiUrl = '/cgi-bin/luci/admin/services/netflow/api';"
|
||||
if old_api not in content:
|
||||
# It was already hardcoded, update to LuCI format
|
||||
content = content.replace(
|
||||
"var apiUrl = '/cgi-bin/luci/admin/services/netflow/api';",
|
||||
"var apiUrl = '<%=luci.dispatcher.build_url(\"admin\", \"services\", \"netflow\", \"api\")%>';"
|
||||
)
|
||||
|
||||
old_upload = "xhr.open('POST', '<%=luci.dispatcher.build_url(\"admin\",\"services\",\"netflow\",\"upload_core\")%>', true);"
|
||||
new_upload = "xhr.open('POST', '/cgi-bin/luci/admin/services/netflow/upload_core', true);"
|
||||
# The upload URL is usually hardcoded now, let's use LuCI format instead
|
||||
content = content.replace(new_upload, old_upload)
|
||||
|
||||
# Clean up old tab navigation remnants
|
||||
import re
|
||||
content = re.sub(r'/\* Tab Navigation \*/\s*\nfunction nfSwitchTab\(btn\)\{.*?\n\}', '', content, flags=re.DOTALL)
|
||||
content = re.sub(r'\s*<!-- Tab Navigation -->\s*\n\s*<div id="nf-tab-bar">.*?</div>\s*\n', '', content, flags=re.DOTALL)
|
||||
content = re.sub(r'\s*<!-- Tab content wrappers -->\s*\n\s*(<div class="nf-tab-full-card".*?</div>\s*\n)+', '', content, flags=re.DOTALL)
|
||||
|
||||
# Ensure LF
|
||||
content = content.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
out = r'E:\AI Project\WangAgent\_tmp_extract\router_main_lf.htm'
|
||||
with open(out, 'w', encoding='utf-8', newline='') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'Template: {len(content)} bytes, LF: {chr(13)+chr(10) not in content[:300]}')
|
||||
print(f'Has <%+header%>: {"<%+header%>" in content}')
|
||||
print(f'Has <%+footer%>: {"<%+footer%>" in content}')
|
||||
print(f'Has standalone DOCTYPE: {"<!DOCTYPE" in content}')
|
||||
print(f'nf-ipv6-proxy-toggle: {content.count("nf-ipv6-proxy-toggle")}')
|
||||
@@ -0,0 +1,29 @@
|
||||
import paramiko, os, time
|
||||
time.sleep(3)
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.get_host_keys().clear()
|
||||
c.connect('10.10.10.3', 22, 'root', 'zxc9607.', timeout=30, allow_agent=False, look_for_keys=False)
|
||||
print('Connected')
|
||||
|
||||
with c.open_sftp() as s:
|
||||
s.put(r'E:\AI Project\WangAgent\luci\luci-app-flclient-v2.ipk', '/tmp/v2.ipk')
|
||||
|
||||
cmds = (
|
||||
'opkg remove luci-app-flclient 2>/dev/null; '
|
||||
'rm -rf /tmp/luci-* 2>/dev/null; '
|
||||
'opkg install /tmp/v2.ipk 2>&1; '
|
||||
'/etc/init.d/uhttpd restart 2>&1; '
|
||||
'echo DONE'
|
||||
)
|
||||
si, so, se = c.exec_command(cmds)
|
||||
print(so.read().decode('utf-8', 'replace'))
|
||||
err = se.read().decode('utf-8', 'replace')
|
||||
if err.strip(): print('ERR:', err)
|
||||
|
||||
# Verify files
|
||||
si, so, se = c.exec_command('ls -la /www/netflow/index.html /usr/lib/lua/luci/controller/netflow.lua 2>/dev/null')
|
||||
print(so.read().decode().strip())
|
||||
|
||||
c.close()
|
||||
print('Deployed!')
|
||||
@@ -0,0 +1,107 @@
|
||||
# repack_netflow_ipk.py — 将修改后的 control/ 和 data/ 目录重新打包为 IPK (tar.gz 格式)
|
||||
# OpenWrt IPK 格式: 外层 tar.gz,内含 debian-binary + control.tar.gz + data.tar.gz
|
||||
# 用法: python repack_netflow_ipk.py <data_dir> <control_dir> <output.ipk>
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import tarfile
|
||||
|
||||
# 需要在 tar 中设为 755 权限的文件
|
||||
EXEC_FILES = {
|
||||
'./usr/bin/netflow',
|
||||
'./usr/bin/netflow-rules',
|
||||
'./usr/bin/netflow_x86_64',
|
||||
'./usr/bin/netflow_aarch64',
|
||||
'./usr/bin/netflow_arm',
|
||||
'./usr/bin/netflow_mips',
|
||||
'./usr/bin/netflow_mipsel',
|
||||
'./etc/init.d/netflow',
|
||||
'./etc/uci-defaults/luci-app-netflow',
|
||||
}
|
||||
|
||||
# control.tar.gz 中的脚本全部需要 755
|
||||
CONTROL_SCRIPTS = {'./preinst', './postinst', './prerm', './postrm'}
|
||||
|
||||
|
||||
def fix_perms(tarinfo, is_control=False):
|
||||
"""设置 Unix 权限"""
|
||||
if tarinfo.isdir():
|
||||
tarinfo.mode = 0o755
|
||||
elif is_control and tarinfo.name in CONTROL_SCRIPTS:
|
||||
tarinfo.mode = 0o755
|
||||
elif tarinfo.name in EXEC_FILES:
|
||||
tarinfo.mode = 0o755
|
||||
else:
|
||||
tarinfo.mode = 0o644
|
||||
return tarinfo
|
||||
|
||||
|
||||
def make_tar_gz_bytes(source_dir, is_control=False):
|
||||
"""将目录打包为 tar.gz,返回 bytes"""
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode='w:gz', format=tarfile.GNU_FORMAT) as tf:
|
||||
for root, dirs, files in os.walk(source_dir):
|
||||
for d in sorted(dirs):
|
||||
fpath = os.path.join(root, d)
|
||||
arcname = './' + os.path.relpath(fpath, source_dir).replace('\\', '/')
|
||||
ti = tf.gettarinfo(fpath, arcname)
|
||||
fix_perms(ti, is_control)
|
||||
ti.uid = ti.gid = 0
|
||||
ti.uname = ti.gname = 'root'
|
||||
tf.addfile(ti)
|
||||
|
||||
for fn in sorted(files):
|
||||
fpath = os.path.join(root, fn)
|
||||
arcname = './' + os.path.relpath(fpath, source_dir).replace('\\', '/')
|
||||
ti = tf.gettarinfo(fpath, arcname)
|
||||
fix_perms(ti, is_control)
|
||||
ti.uid = ti.gid = 0
|
||||
ti.uname = ti.gname = 'root'
|
||||
with open(fpath, 'rb') as f:
|
||||
tf.addfile(ti, f)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print('用法: python repack_netflow_ipk.py <data_dir> <control_dir> <output.ipk>')
|
||||
sys.exit(1)
|
||||
|
||||
data_dir = sys.argv[1]
|
||||
control_dir = sys.argv[2]
|
||||
output_ipk = sys.argv[3]
|
||||
|
||||
# 1. 生成 debian-binary
|
||||
debian_data = b'2.0\n'
|
||||
|
||||
# 2. 打包 control.tar.gz
|
||||
control_tgz = make_tar_gz_bytes(control_dir, is_control=True)
|
||||
|
||||
# 3. 打包 data.tar.gz
|
||||
data_tgz = make_tar_gz_bytes(data_dir)
|
||||
|
||||
# 4. 创建外层 tar.gz (IPK 格式)
|
||||
with tarfile.open(output_ipk, 'w:gz', format=tarfile.GNU_FORMAT) as outer:
|
||||
for name, data, mode in [
|
||||
('./debian-binary', debian_data, 0o644),
|
||||
('./control.tar.gz', control_tgz, 0o644),
|
||||
('./data.tar.gz', data_tgz, 0o644),
|
||||
]:
|
||||
ti = tarfile.TarInfo(name)
|
||||
ti.size = len(data)
|
||||
ti.mode = mode
|
||||
ti.uid = ti.gid = 0
|
||||
ti.uname = ti.gname = 'root'
|
||||
ti.mtime = 0
|
||||
outer.addfile(ti, io.BytesIO(data))
|
||||
|
||||
size = os.path.getsize(output_ipk)
|
||||
print(f'IPK 打包完成: {output_ipk}')
|
||||
print(f' 大小: {size:,} bytes')
|
||||
print(f' 控制文件: {len(control_tgz):,} bytes')
|
||||
print(f' 数据文件: {len(data_tgz):,} bytes')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user