feat: 完整 Squid Web Manager 平台 - P0-P3 全功能
完整 Web 管理平台,涵盖: P0 生产加固: - 会话超时(空闲+绝对) + CSRF 防护 + 登录失败限流 - HTTPS/TLS 证书管理(自签生成/上传/删除/过期提醒) - SSL Bump (HTTPS 透明代理) 可视化配置 - 配置文件 diff 预览 + 二次确认才写入 - 服务控制二次确认 + 操作期间按钮锁定 P1 运维能力: - 日志轮转管理(logrotate) + 日志状态监控 - 告警规则(5xx/命中率/磁盘/客户端流量) - 多 Squid 实例管理 + 一键切换 - squidclient mgr 实时性能指标 - 流量异常检测(突增/大文件/高频小包/新客户端) P2 日志分析增强: - 日志持久化到 SQLite + 历史时间范围查询 - 导出 CSV/JSON(日志/KPI/异常/审计) - GeoIP 地图(Leaflet + ip-api.com 离线可选 GeoLite2) - 每客户端 24h 趋势图 - 自定义 Squid logformat 解析器 P3 体验锦上添花: - 暗/亮主题切换(cookie 持久化) - WebSSH 终端(xterm.js + paramiko) - 完整审计日志 + 用户管理 技术栈: Flask 3.0 + SQLAlchemy 2.0 + SQLite + Chart.js + Leaflet 总代码量: ~16500 行 (15 Python 模块 + 34 模板 + CSS) 路由数: 73
This commit is contained in:
+706
@@ -0,0 +1,706 @@
|
||||
"""Squid configuration (squid.conf) parser and generator.
|
||||
|
||||
Strategy:
|
||||
- Parse the existing squid.conf into a list of directives with comments
|
||||
- Expose structured getters for sections (network, cache, ACLs, rules, auth, ...)
|
||||
- Allow web UI to update specific directives
|
||||
- Generate a complete, well-formed squid.conf from the structured config
|
||||
- Validate before applying (run `squid -k parse` if available)
|
||||
|
||||
Directive model:
|
||||
- Each directive is a tuple (key, [args...], comment)
|
||||
- Directives are grouped into named sections for the UI
|
||||
|
||||
Note: Squid supports many directives. We cover the most common ones used in
|
||||
production deployments. A "raw editor" tab is always available for anything we
|
||||
don't model explicitly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Directive:
|
||||
"""A single squid.conf directive line, possibly with a comment."""
|
||||
key: str
|
||||
args: list[str]
|
||||
comment: str = ""
|
||||
# raw original line (for round-trip preservation)
|
||||
raw: str = ""
|
||||
|
||||
def render(self) -> str:
|
||||
if self.raw and not self.args and not self.comment:
|
||||
return self.raw
|
||||
line = self.key
|
||||
if self.args:
|
||||
line += " " + " ".join(self.args)
|
||||
if self.comment:
|
||||
line += " #" + self.comment
|
||||
return line
|
||||
|
||||
|
||||
@dataclass
|
||||
class Acl:
|
||||
"""An ACL entry: acl <name> <type> <value>..."""
|
||||
name: str
|
||||
type: str
|
||||
values: list[str]
|
||||
comment: str = ""
|
||||
|
||||
def render(self) -> str:
|
||||
parts = ["acl", self.name, self.type] + self.values
|
||||
line = " ".join(parts)
|
||||
if self.comment:
|
||||
line += " #" + self.comment
|
||||
return line
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rule:
|
||||
"""An access rule: http_access <allow|deny> [!]acl..."""
|
||||
action: str # allow / deny
|
||||
acls: list[str] # e.g. ["!localhost", "allowed_net"]
|
||||
comment: str = ""
|
||||
directive: str = "http_access" # http_access, https_access, icp_access, ...
|
||||
|
||||
def render(self) -> str:
|
||||
parts = [self.directive, self.action] + self.acls
|
||||
line = " ".join(parts)
|
||||
if self.comment:
|
||||
line += " #" + self.comment
|
||||
return line
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheDir:
|
||||
"""A cache_dir directive: cache_dir <type> <path> <size> <L1> <L2> [options]"""
|
||||
type: str = "ufs"
|
||||
path: str = "/var/spool/squid"
|
||||
size_mb: int = 100
|
||||
l1: int = 16
|
||||
l2: int = 256
|
||||
options: list[str] = field(default_factory=list)
|
||||
|
||||
def render(self) -> str:
|
||||
parts = ["cache_dir", self.type, self.path, str(self.size_mb), str(self.l1), str(self.l2)]
|
||||
parts.extend(self.options)
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefreshPattern:
|
||||
"""refresh_pattern [-i] regex min percent max [{options}]"""
|
||||
case_insensitive: bool = False
|
||||
regex: str = "."
|
||||
min_minutes: int = 0
|
||||
percent: int = 20
|
||||
max_minutes: int = 4320
|
||||
options: list[str] = field(default_factory=list)
|
||||
|
||||
def render(self) -> str:
|
||||
parts = ["refresh_pattern"]
|
||||
if self.case_insensitive:
|
||||
parts.append("-i")
|
||||
parts.extend([self.regex, str(self.min_minutes), f"{self.percent}%", str(self.max_minutes)])
|
||||
if self.options:
|
||||
parts.append("{" + " ".join(self.options) + "}")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthParam:
|
||||
"""auth_param <scheme> <param> <value>..."""
|
||||
scheme: str = "basic"
|
||||
program: str = "/usr/lib/squid/basic_ncsa_auth /etc/squid/passwd"
|
||||
children: int = 5
|
||||
realm: str = "Squid Proxy"
|
||||
credentialsttl: int = 2
|
||||
# also stored as raw param dict for other schemes
|
||||
extra: dict = field(default_factory=dict)
|
||||
|
||||
def render_all(self) -> list[str]:
|
||||
lines = []
|
||||
lines.append(f"auth_param {self.scheme} program {self.program}")
|
||||
lines.append(f"auth_param {self.scheme} children {self.children}")
|
||||
lines.append(f"auth_param {self.scheme} realm {self.realm}")
|
||||
lines.append(f"auth_param {self.scheme} credentialsttl {self.credentialsttl} hours")
|
||||
for k, v in self.extra.items():
|
||||
lines.append(f"auth_param {self.scheme} {k} {v}")
|
||||
return lines
|
||||
|
||||
|
||||
@dataclass
|
||||
class CachePeer:
|
||||
"""cache_peer <host> <type> <http_port> <icp_port> [options]"""
|
||||
host: str = ""
|
||||
type: str = "parent"
|
||||
http_port: int = 3128
|
||||
icp_port: int = 0
|
||||
options: list[str] = field(default_factory=list)
|
||||
|
||||
def render(self) -> str:
|
||||
parts = ["cache_peer", self.host, self.type, str(self.http_port), str(self.icp_port)]
|
||||
parts.extend(self.options)
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SslBumpRule:
|
||||
"""ssl_bump directive: ssl_bump <action> [!]acl ...
|
||||
action: peek, splice, bump, terminate, client-first, server-first, none
|
||||
"""
|
||||
action: str = "peek"
|
||||
acls: list[str] = field(default_factory=list) # ACL names + optional ! negation
|
||||
step: int = 1 # rule order
|
||||
comment: str = ""
|
||||
|
||||
def render(self) -> str:
|
||||
parts = ["ssl_bump", self.action] + self.acls
|
||||
line = " ".join(parts)
|
||||
if self.comment:
|
||||
line += " #" + self.comment
|
||||
return line
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_conf(text: str):
|
||||
"""Parse a squid.conf string into structured groups.
|
||||
|
||||
Returns dict with keys: directives (raw), acls, rules, cache_dirs,
|
||||
refresh_patterns, auth_params, cache_peers, ssl_bump_rules.
|
||||
"""
|
||||
acls = []
|
||||
rules = []
|
||||
cache_dirs = []
|
||||
refresh_patterns = []
|
||||
cache_peers = []
|
||||
auth_lines = [] # raw auth_param lines, parsed into AuthParam later
|
||||
ssl_bump_rules = [] # ssl_bump directives
|
||||
|
||||
# map of directive -> list of (args, comment) for simple single-value directives
|
||||
simple: dict[str, list[tuple[list[str], str]]] = {}
|
||||
|
||||
for raw in text.splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
|
||||
# split off trailing comment
|
||||
comment = ""
|
||||
if " #" in line:
|
||||
line, _, comment = line.partition(" #")
|
||||
line = line.rstrip()
|
||||
comment = comment.strip()
|
||||
|
||||
tokens = line.split()
|
||||
if not tokens:
|
||||
continue
|
||||
key = tokens[0]
|
||||
args = tokens[1:]
|
||||
|
||||
if key == "acl":
|
||||
if len(args) >= 3:
|
||||
acls.append(Acl(
|
||||
name=args[0], type=args[1], values=args[2:],
|
||||
comment=comment,
|
||||
))
|
||||
continue
|
||||
|
||||
if key in ("http_access", "http_access2", "https_access",
|
||||
"icp_access", "htcp_access", "miss_access",
|
||||
"snmp_access", "ident_lookup_access",
|
||||
"always_direct", "never_direct",
|
||||
"reply_body_max_size", "reply_access"):
|
||||
if args:
|
||||
rules.append(Rule(
|
||||
action=args[0], acls=args[1:], comment=comment,
|
||||
directive=key,
|
||||
))
|
||||
continue
|
||||
|
||||
if key == "cache_dir":
|
||||
if len(args) >= 5:
|
||||
cache_dirs.append(CacheDir(
|
||||
type=args[0], path=args[1],
|
||||
size_mb=int(args[2]) if args[2].isdigit() else 100,
|
||||
l1=int(args[3]) if args[3].isdigit() else 16,
|
||||
l2=int(args[4]) if args[4].isdigit() else 256,
|
||||
options=args[5:],
|
||||
))
|
||||
continue
|
||||
|
||||
if key == "refresh_pattern":
|
||||
rp = RefreshPattern()
|
||||
idx = 0
|
||||
if idx < len(args) and args[idx] == "-i":
|
||||
rp.case_insensitive = True
|
||||
idx += 1
|
||||
if idx < len(args):
|
||||
rp.regex = args[idx]; idx += 1
|
||||
if idx < len(args) and args[idx].lstrip("-").isdigit():
|
||||
rp.min_minutes = int(args[idx]); idx += 1
|
||||
if idx < len(args) and args[idx].endswith("%"):
|
||||
try:
|
||||
rp.percent = int(args[idx].rstrip("%"))
|
||||
except ValueError:
|
||||
pass
|
||||
idx += 1
|
||||
if idx < len(args) and args[idx].lstrip("-").isdigit():
|
||||
rp.max_minutes = int(args[idx]); idx += 1
|
||||
rp.options = args[idx:]
|
||||
refresh_patterns.append(rp)
|
||||
continue
|
||||
|
||||
if key == "cache_peer":
|
||||
if len(args) >= 4:
|
||||
cache_peers.append(CachePeer(
|
||||
host=args[0], type=args[1],
|
||||
http_port=int(args[2]) if args[2].isdigit() else 3128,
|
||||
icp_port=int(args[3]) if args[3].isdigit() else 0,
|
||||
options=args[4:],
|
||||
))
|
||||
continue
|
||||
|
||||
if key == "ssl_bump":
|
||||
# ssl_bump <action> [!]acl...
|
||||
if args:
|
||||
action = args[0]
|
||||
acls_for_rule = args[1:]
|
||||
step = len(ssl_bump_rules) + 1
|
||||
ssl_bump_rules.append(SslBumpRule(
|
||||
action=action,
|
||||
acls=acls_for_rule,
|
||||
step=step,
|
||||
comment=comment,
|
||||
))
|
||||
continue
|
||||
|
||||
if key == "auth_param":
|
||||
auth_lines.append((args, comment))
|
||||
continue
|
||||
|
||||
simple.setdefault(key, []).append((args, comment))
|
||||
|
||||
# parse auth_params into structured form (one per scheme)
|
||||
auth_params = parse_auth_lines(auth_lines)
|
||||
|
||||
return {
|
||||
"acls": acls,
|
||||
"rules": rules,
|
||||
"cache_dirs": cache_dirs,
|
||||
"refresh_patterns": refresh_patterns,
|
||||
"cache_peers": cache_peers,
|
||||
"auth_params": auth_params,
|
||||
"ssl_bump_rules": ssl_bump_rules,
|
||||
"simple": simple, # raw simple directives, list of (args, comment)
|
||||
}
|
||||
|
||||
|
||||
def parse_auth_lines(lines: list[tuple[list[str], str]]) -> list[AuthParam]:
|
||||
"""Group auth_param lines by scheme."""
|
||||
by_scheme: dict[str, AuthParam] = {}
|
||||
for args, comment in lines:
|
||||
if len(args) < 2:
|
||||
continue
|
||||
scheme = args[0]
|
||||
param = args[1]
|
||||
value = " ".join(args[2:])
|
||||
if scheme not in by_scheme:
|
||||
ap = AuthParam(scheme=scheme)
|
||||
by_scheme[scheme] = ap
|
||||
ap = by_scheme[scheme]
|
||||
if param == "program":
|
||||
ap.program = value
|
||||
elif param == "children":
|
||||
try:
|
||||
ap.children = int(args[2])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
elif param == "realm":
|
||||
ap.realm = value
|
||||
elif param == "credentialsttl":
|
||||
try:
|
||||
ap.credentialsttl = int(args[2])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
else:
|
||||
ap.extra[param] = value
|
||||
return list(by_scheme.values())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# default simple directives the UI manages explicitly.
|
||||
DEFAULT_SIMPLE = {
|
||||
# network
|
||||
"http_port": [["3128"], ""],
|
||||
"https_port": [],
|
||||
"icp_port": [["3130"], ""],
|
||||
"snmp_port": [],
|
||||
"visible_hostname": [["localhost"], ""],
|
||||
"unique_hostname": [],
|
||||
"hostname_aliases": [],
|
||||
# cache
|
||||
"cache_mem": [["256", "MB"], ""],
|
||||
"maximum_object_size": [["4096", "KB"], ""],
|
||||
"minimum_object_size": [["0", "KB"], ""],
|
||||
"maximum_object_size_in_memory": [["512", "KB"], ""],
|
||||
"cache_replacement_policy": [["lru"], ""],
|
||||
"memory_replacement_policy": [["lru"], ""],
|
||||
"cache_swap_low": [["90"], ""],
|
||||
"cache_swap_high": [["95"], ""],
|
||||
# logging
|
||||
"access_log": [["/var/log/squid/access.log"], ""],
|
||||
"cache_log": [["/var/log/squid/cache.log"], ""],
|
||||
"cache_store_log": [["/var/log/squid/store.log"], ""],
|
||||
"pid_filename": [["/var/run/squid.pid"], ""],
|
||||
"logformat": [],
|
||||
"debug_options": [],
|
||||
"log_ip_on_direct": [["on"], ""],
|
||||
"buffered_logs": [["on"], ""],
|
||||
# dns
|
||||
"dns_nameservers": [],
|
||||
"dns_timeout": [["30", "seconds"], ""],
|
||||
"hosts_file": [["/etc/hosts"], ""],
|
||||
"dns_defnames": [["off"], ""],
|
||||
# SSL / TLS (for ssl-bump)
|
||||
"sslcrtd_program": [["security_file_certgen", "-s", "/var/lib/squid/ssl_db", "-M", "4", "MB"], ""],
|
||||
"sslcrtd_children": [["5", "startup=1", "idle=1"], ""],
|
||||
"sslproxy_ca_certfile": [],
|
||||
"sslproxy_ca_keyfile": [],
|
||||
"tls_outgoing_version": [],
|
||||
# timeouts
|
||||
"connect_timeout": [["60", "seconds"], ""],
|
||||
"read_timeout": [["15", "minutes"], ""],
|
||||
"client_lifetime": [["1", "day"], ""],
|
||||
"shutdown_lifetime": [["30", "seconds"], ""],
|
||||
# admin
|
||||
"cache_mgr": [["root"], ""],
|
||||
"err_html_language": [["en"], ""],
|
||||
"ftp_user": [],
|
||||
}
|
||||
|
||||
|
||||
def generate_conf(struct: dict, raw_extra: str = "") -> str:
|
||||
"""Generate a complete squid.conf string from the structured config."""
|
||||
lines: list[str] = []
|
||||
|
||||
lines.append("# ============================================================")
|
||||
lines.append("# squid.conf - managed by Squid Web Manager")
|
||||
lines.append("# WARNING: this file is regenerated by the web UI.")
|
||||
lines.append("# Manual edits may be lost - use the Raw Editor tab")
|
||||
lines.append("# if you need to add directives not covered by the UI.")
|
||||
lines.append("# ============================================================")
|
||||
lines.append("")
|
||||
|
||||
# --- Network ---
|
||||
lines.append("# ---- Network ----")
|
||||
for key in ("http_port", "https_port", "icp_port", "snmp_port",
|
||||
"visible_hostname", "unique_hostname", "hostname_aliases"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
lines.append("")
|
||||
|
||||
# --- Cache ---
|
||||
lines.append("# ---- Cache ----")
|
||||
for key in ("cache_mem", "maximum_object_size", "minimum_object_size",
|
||||
"maximum_object_size_in_memory",
|
||||
"cache_replacement_policy", "memory_replacement_policy",
|
||||
"cache_swap_low", "cache_swap_high"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
for cd in struct["cache_dirs"]:
|
||||
lines.append(cd.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Auth ---
|
||||
if struct["auth_params"]:
|
||||
lines.append("# ---- Authentication ----")
|
||||
for ap in struct["auth_params"]:
|
||||
for ln in ap.render_all():
|
||||
lines.append(ln)
|
||||
lines.append("")
|
||||
|
||||
# --- ACLs ---
|
||||
lines.append("# ---- ACLs ----")
|
||||
for acl in struct["acls"]:
|
||||
lines.append(acl.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Refresh patterns ---
|
||||
if struct["refresh_patterns"]:
|
||||
lines.append("# ---- Refresh patterns ----")
|
||||
for rp in struct["refresh_patterns"]:
|
||||
lines.append(rp.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Cache peers ---
|
||||
if struct["cache_peers"]:
|
||||
lines.append("# ---- Cache peers ----")
|
||||
for cp in struct["cache_peers"]:
|
||||
lines.append(cp.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Access rules ---
|
||||
lines.append("# ---- Access rules ----")
|
||||
for r in struct["rules"]:
|
||||
lines.append(r.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Logging ---
|
||||
lines.append("# ---- Logging ----")
|
||||
for key in ("access_log", "cache_log", "cache_store_log",
|
||||
"pid_filename", "logformat", "debug_options",
|
||||
"log_ip_on_direct", "buffered_logs"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
lines.append("")
|
||||
|
||||
# --- DNS ---
|
||||
lines.append("# ---- DNS ----")
|
||||
for key in ("dns_nameservers", "dns_timeout", "hosts_file", "dns_defnames"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
lines.append("")
|
||||
|
||||
# --- SSL/TLS ---
|
||||
ssl_keys = ("sslcrtd_program", "sslcrtd_children", "sslproxy_ca_certfile",
|
||||
"sslproxy_ca_keyfile", "tls_outgoing_version")
|
||||
if any(struct["simple"].get(k) for k in ssl_keys) or struct.get("ssl_bump_rules"):
|
||||
lines.append("# ---- SSL/TLS ----")
|
||||
for key in ssl_keys:
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
for rule in struct.get("ssl_bump_rules", []):
|
||||
lines.append(rule.render())
|
||||
lines.append("")
|
||||
|
||||
# --- Timeouts ---
|
||||
lines.append("# ---- Timeouts ----")
|
||||
for key in ("connect_timeout", "read_timeout", "client_lifetime",
|
||||
"shutdown_lifetime"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
lines.append("")
|
||||
|
||||
# --- Admin ---
|
||||
lines.append("# ---- Admin ----")
|
||||
for key in ("cache_mgr", "err_html_language", "ftp_user"):
|
||||
for args, comment in struct["simple"].get(key, []):
|
||||
lines.append(_render_simple(key, args, comment))
|
||||
lines.append("")
|
||||
|
||||
# --- raw extra directives ---
|
||||
if raw_extra.strip():
|
||||
lines.append("# ---- Raw directives (manual) ----")
|
||||
for ln in raw_extra.splitlines():
|
||||
if ln.strip():
|
||||
lines.append(ln.strip())
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _render_simple(key: str, args: list[str], comment: str = "") -> str:
|
||||
line = key + (" " + " ".join(args) if args else "")
|
||||
if comment:
|
||||
line += " #" + comment
|
||||
return line
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def validate_conf(text: str, squid_binary: str = "squid") -> tuple[bool, str]:
|
||||
"""Run `squid -k parse` on the given config text.
|
||||
|
||||
Returns (ok, message). Falls back to syntax-only check if squid isn't
|
||||
installed.
|
||||
"""
|
||||
if not squid_binary or not _which(squid_binary):
|
||||
# no squid binary available - just check that we can parse it
|
||||
try:
|
||||
parse_conf(text)
|
||||
return True, "Squid binary not found - parsed structure successfully " \
|
||||
"(skipped runtime syntax check)."
|
||||
except Exception as e:
|
||||
return False, f"Parse error: {e}"
|
||||
|
||||
tmp_path = "/tmp/squid_conf_check.conf"
|
||||
try:
|
||||
with open(tmp_path, "w") as f:
|
||||
f.write(text)
|
||||
except OSError as e:
|
||||
return False, f"Cannot write temp file: {e}"
|
||||
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[squid_binary, "-k", "parse", "-f", tmp_path],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "squid -k parse timed out"
|
||||
except FileNotFoundError:
|
||||
return False, "squid binary not found"
|
||||
# squid -k parse exits 0 on success. Errors are written to stdout.
|
||||
out = (r.stdout or "").strip()
|
||||
err = (r.stderr or "").strip()
|
||||
msg = out + ("\n" + err if err else "")
|
||||
# squid typically prints "WARNING:" lines for non-fatal issues.
|
||||
if r.returncode == 0:
|
||||
return True, msg or "Configuration OK"
|
||||
return False, msg or f"squid -k parse failed (exit {r.returncode})"
|
||||
|
||||
|
||||
def _which(cmd: str) -> Optional[str]:
|
||||
"""Lightweight which() that doesn't depend on shutil.which."""
|
||||
for path in os.environ.get("PATH", "").split(":"):
|
||||
if not path:
|
||||
continue
|
||||
candidate = os.path.join(path, cmd)
|
||||
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults (used to bootstrap a brand new install)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def default_struct() -> dict:
|
||||
"""Return a default structured config for a fresh install."""
|
||||
simple = {k: [v] if isinstance(v, list) and v and isinstance(v[0], list)
|
||||
else (v if isinstance(v, list) else [])
|
||||
for k, v in DEFAULT_SIMPLE.items()}
|
||||
# DEFAULT_SIMPLE values are already lists of [args, comment]
|
||||
# but they are stored as [args_list, comment] tuples - normalize.
|
||||
simple = {}
|
||||
for k, v in DEFAULT_SIMPLE.items():
|
||||
# v is [[args...], comment] or [] (empty)
|
||||
if isinstance(v, list) and len(v) == 2 and isinstance(v[0], list):
|
||||
simple[k] = [(v[0], v[1])]
|
||||
else:
|
||||
simple[k] = []
|
||||
# Provide a sensible default sslcrtd_program bootstrap so freshly-deployed
|
||||
# SSL Bump setups have a working cert generator out of the box.
|
||||
# (Empty list would render "sslcrtd_program" with no args - leave it.)
|
||||
# Default is intentionally commented out in DEFAULT_SIMPLE above
|
||||
# (sslcrtd_program: []). Users override via the SSL Bump UI.
|
||||
return {
|
||||
"acls": [
|
||||
Acl(name="localhost", type="src", values=["127.0.0.1/32"]),
|
||||
Acl(name="to_localhost", type="dst", values=["127.0.0.0/8"]),
|
||||
Acl(name="safeports", type="port", values=["80", "443", "21", "70", "80-1024"]),
|
||||
Acl(name="sslports", type="port", values=["443", "563"]),
|
||||
Acl(name="CONNECT", type="method", values=["CONNECT"]),
|
||||
Acl(name="all", type="src", values=["0.0.0.0/0"]),
|
||||
Acl(name="manager", type="proto", values=["cache_object"]),
|
||||
],
|
||||
"rules": [
|
||||
Rule(action="deny", acls=["manager"], directive="http_access"),
|
||||
Rule(action="deny", acls=["!Safe_ports"], directive="http_access"),
|
||||
Rule(action="deny", acls=["!SSL_ports"], directive="http_access"),
|
||||
Rule(action="allow", acls=["localhost"], directive="http_access"),
|
||||
Rule(action="deny", acls=["all"], directive="http_access"),
|
||||
],
|
||||
"cache_dirs": [
|
||||
CacheDir(type="ufs", path="/var/spool/squid",
|
||||
size_mb=100, l1=16, l2=256)
|
||||
],
|
||||
"refresh_patterns": [
|
||||
RefreshPattern(regex="^ftp:", min_minutes=1440, percent=20, max_minutes=10080),
|
||||
RefreshPattern(regex="^gopher:", min_minutes=1440, percent=0, max_minutes=1440),
|
||||
RefreshPattern(case_insensitive=True, regex=" cgi-bin",
|
||||
min_minutes=0, percent=0, max_minutes=0),
|
||||
RefreshPattern(regex=".", min_minutes=0, percent=20, max_minutes=4320),
|
||||
],
|
||||
"cache_peers": [],
|
||||
"auth_params": [],
|
||||
"ssl_bump_rules": [],
|
||||
"simple": simple,
|
||||
}
|
||||
|
||||
|
||||
ACL_TYPES = [
|
||||
("src", "Source IP/CIDR (src 192.168.1.0/24)"),
|
||||
("dst", "Destination IP/CIDR"),
|
||||
("dstdomain", "Destination domain (matches *.example.com)"),
|
||||
("dstdom_regex", "Destination domain regex"),
|
||||
("srcdomain", "Source domain"),
|
||||
("srcdom_regex", "Source domain regex"),
|
||||
("port", "Destination port"),
|
||||
("method", "HTTP method (GET, POST, CONNECT, ...)"),
|
||||
("proto", "Protocol (http, https, ftp, ...)"),
|
||||
("time", "Time of day/week (SMTWHFA/HH:MM-HH:MM)"),
|
||||
("url_regex", "URL regex"),
|
||||
("urlpath_regex", "URL path regex"),
|
||||
("url_login", "URL login part regex"),
|
||||
("browser", "User-Agent regex"),
|
||||
("referer_regex", "Referer regex"),
|
||||
("ident", "User ident string"),
|
||||
("ident_regex", "User ident regex"),
|
||||
("proxy_auth", "Authenticated user / REQUIRED"),
|
||||
("proxy_auth_regex", "Authenticated user regex"),
|
||||
("maxconn", "Max connections per client"),
|
||||
("max_user_ip", "Max IPs per user"),
|
||||
("rep_mime_type", "Reply MIME type regex"),
|
||||
("req_header", "Request header"),
|
||||
("rep_header", "Reply header"),
|
||||
("localip", "Local IP"),
|
||||
("localnet", "Local network"),
|
||||
("peername", "Peer name"),
|
||||
("hierarchy_type", "Hierarchy type"),
|
||||
]
|
||||
|
||||
RULE_DIRECTIVES = [
|
||||
"http_access",
|
||||
"http_access2",
|
||||
"https_access",
|
||||
"icp_access",
|
||||
"htcp_access",
|
||||
"miss_access",
|
||||
"snmp_access",
|
||||
"ident_lookup_access",
|
||||
"reply_access",
|
||||
"adaptation_access",
|
||||
]
|
||||
|
||||
|
||||
# Valid ssl_bump actions (Squid docs - https://www.squid-cache.org/Doc/config/ssl_bump/)
|
||||
SSL_BUMP_ACTIONS = [
|
||||
"peek",
|
||||
"splice",
|
||||
"bump",
|
||||
"terminate",
|
||||
"client-first",
|
||||
"server-first",
|
||||
"none",
|
||||
]
|
||||
|
||||
|
||||
SSL_BUMP_DEFAULT_EXAMPLE = (
|
||||
"# Suggested starter set (rule order matters - top to bottom):\n"
|
||||
"# ssl_bump peek step1 # peek at TLS client hello (step 1)\n"
|
||||
"# ssl_bump splice all # otherwise pass-through tunnel\n"
|
||||
"#\n"
|
||||
"# Required prereqs before SSL Bump will work:\n"
|
||||
"# 1) sslcrtd_program/children pointing at security_file_certgen\n"
|
||||
"# and an initialised ssl_db directory\n"
|
||||
"# 2) sslproxy_ca_certfile / sslproxy_ca_keyfile pointing at a CA\n"
|
||||
"# that your client browsers trust (or you distribute the cert\n"
|
||||
"# and instruct users to install it)\n"
|
||||
"# 3) https_port configured with ssl-bump option"
|
||||
)
|
||||
Reference in New Issue
Block a user