diff --git a/build.sh b/build.sh index 54a2f47..7f7c605 100755 --- a/build.sh +++ b/build.sh @@ -64,12 +64,16 @@ ipkg_build=ipkg-build.sh mkdir -p $opkg_template/usr/bin mkdir -p $opkg_template/usr/lib/lua/luci/controller mkdir -p $opkg_template/usr/lib/lua/luci/model/cbi +mkdir -p $opkg_template/usr/lib/lua/luci/view/ua3f +mkdir -p $opkg_template/usr/lib/lua/luci/i18n mkdir -p $opkg_template/etc/init.d mkdir -p $opkg_template/etc/config cp openwrt/files/luci/controller.lua $opkg_template/usr/lib/lua/luci/controller/ua3f.lua cp openwrt/files/luci/cbi.lua $opkg_template/usr/lib/lua/luci/model/cbi/ua3f.lua +cp openwrt/files/luci/statistics.htm $opkg_template/usr/lib/lua/luci/view/ua3f/statistics.htm cp openwrt/files/ua3f.init $opkg_template/etc/init.d/ua3f cp openwrt/files/ua3f.uci $opkg_template/etc/config/ua3f +./po2lmo openwrt/po/zh_cn/ua3f.po $opkg_template/usr/lib/lua/luci/i18n/ua3f.zh-cn.lmo for goarch in "amd64" "arm" "arm64" "mipsle" "mips64" "riscv64" "386" "mipsle-softfloat" "mipsle-hardfloat" "armv7" "armv8"; do obj_name=$project_name-$release_version-$goarch mv $dist/bin/$obj_name $opkg_template/usr/bin/ua3f diff --git a/ipkg/CONTROL/control-e b/ipkg/CONTROL/control-e index 8dc7168..1d7ab1b 100644 --- a/ipkg/CONTROL/control-e +++ b/ipkg/CONTROL/control-e @@ -7,5 +7,5 @@ License: GPL-3.0-only Section: net SourceDateEpoch: 1711267200 Architecture: all -Installed-Size: 3686400 +Installed-Size: 3696640 Description: Implementation of the next generation of HTTP User-Agent modification methodology. diff --git a/openwrt/Makefile b/openwrt/Makefile index 3c3a597..4625dd9 100644 --- a/openwrt/Makefile +++ b/openwrt/Makefile @@ -37,6 +37,7 @@ endef define Build/Prepare $(CP) ../src/* $(PKG_BUILD_DIR) + po2lmo ./po/zh_cn/ua3f.po $(PKG_BUILD_DIR)/ua3f.zh-cn.lmo endef define Package/ua3f/conffiles @@ -57,6 +58,10 @@ define Package/ua3f/install $(INSTALL_CONF) ./files/luci/cbi.lua $(1)/usr/lib/lua/luci/model/cbi/ua3f.lua $(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller/ $(INSTALL_CONF) ./files/luci/controller.lua $(1)/usr/lib/lua/luci/controller/ua3f.lua + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/ua3f/ + $(INSTALL_CONF) ./files/luci/statistics.htm $(1)/usr/lib/lua/luci/view/ua3f/statistics.htm + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n/ + $(INSTALL_DATA) $(PKG_BUILD_DIR)/ua3f.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/ua3f.zh-cn.lmo endef diff --git a/openwrt/files/luci/cbi.lua b/openwrt/files/luci/cbi.lua index 32b8a3c..6b7ff8f 100644 --- a/openwrt/files/luci/cbi.lua +++ b/openwrt/files/luci/cbi.lua @@ -9,60 +9,63 @@ ua3f = Map("ua3f", ]] ) -enable = ua3f:section(NamedSection, "enabled", "ua3f", "Status") -main = ua3f:section(NamedSection, "main", "ua3f", "Settings") +status = ua3f:section(NamedSection, "enabled", "ua3f", translate("Status")) +general = ua3f:section(NamedSection, "main", "ua3f", translate("General")) -enable:option(Flag, "enabled", "Enabled") -status = enable:option(DummyValue, "status", "Status") -status.rawhtml = true -status.cfgvalue = function(self, section) +status:option(Flag, "enabled", translate("Enabled")) + +running = status:option(DummyValue, "running", translate("Status")) +running.rawhtml = true +running.cfgvalue = function(self, section) local pid = luci.sys.exec("pidof ua3f") if pid == "" then - return "" .. "Stopped" .. "" + return "" else - return "" .. "Running" .. "" + return "" end end -main:tab("general", "General Settings") -main:tab("log", "Log") +general:tab("general", translate("Settings")) +general:tab("stats", translate("Statistics")) +general:tab("log", translate("Log")) -port = main:taboption("general", Value, "port", "Port") +port = general:taboption("general", Value, "port", translate("Port")) port.placeholder = "1080" -bind = main:taboption("general", Value, "bind", "Bind Address") +bind = general:taboption("general", Value, "bind", translate("Bind Address")) bind:value("127.0.0.1") bind:value("0.0.0.0") -log_level = main:taboption("general", ListValue, "log_level", "Log Level") +log_level = general:taboption("general", ListValue, "log_level", translate("Log Level")) log_level:value("debug") log_level:value("info") log_level:value("warn") log_level:value("error") log_level:value("fatal") log_level:value("panic") -log = main:taboption("log", TextValue, "") + +ua = general:taboption("general", Value, "ua", translate("User-Agent")) +ua.placeholder = "FFF" + +uaRegexPattern = general:taboption("general", Value, "ua_regex", translate("User-Agent Regex Pattern")) +uaRegexPattern.placeholder = "(iPhone|iPad|Android|Macintosh|Windows|Linux|Apple|Mac OS X|Mobile)" +uaRegexPattern.description = translate("Regular expression pattern for matching User-Agent") + +partialRepalce = general:taboption("general", Flag, "partial_replace", translate("Partial Replace")) +partialRepalce.description = + translate("Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty") +partialRepalce.default = "0" + +log = general:taboption("log", TextValue, "") log.readonly = true log.cfgvalue = function(self, section) return luci.sys.exec("cat /var/log/ua3f/ua3f.log") end log.rows = 30 -ua = main:taboption("general", Value, "ua", "User-Agent") -ua.placeholder = "FFF" - -uaRegexPattern = main:taboption("general", Value, "ua_regex", "User-Agent Regex Pattern") -uaRegexPattern.placeholder = "(iPhone|iPad|Android|Macintosh|Windows|Linux|Apple|Mac OS X|Mobile)" -uaRegexPattern.description = "Regular expression pattern for matching User-Agent" - -partialRepalce = main:taboption("general", Flag, "partial_replace", "Partial Replace") -partialRepalce.description = -"Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty" -partialRepalce.default = "0" - -local apply = luci.http.formvalue("cbi.apply") -if apply then - io.popen("/etc/init.d/ua3f restart") -end +stats = general:taboption("stats", DummyValue, "") +stats.template = "ua3f/statistics" return ua3f diff --git a/openwrt/files/luci/controller.lua b/openwrt/files/luci/controller.lua index 569b3bd..2baff4f 100644 --- a/openwrt/files/luci/controller.lua +++ b/openwrt/files/luci/controller.lua @@ -1,5 +1,5 @@ module("luci.controller.ua3f", package.seeall) function index() - entry({"admin", "services", "ua3f"}, cbi("ua3f"), "UA3F", 1) + entry({"admin", "services", "ua3f"}, cbi("ua3f"), _("UA3F"), 1) end \ No newline at end of file diff --git a/openwrt/files/luci/statistics.htm b/openwrt/files/luci/statistics.htm new file mode 100644 index 0000000..1ed7870 --- /dev/null +++ b/openwrt/files/luci/statistics.htm @@ -0,0 +1,82 @@ +<% +local stats = {} +local file = io.open("/var/log/ua3f/stats", "r") +if file then + for line in file:lines() do + local host, count, origin_ua, mocked_ua = line:match("^(%S+)%s+(%d+)%s+(.-)SEQSEQ(.-)%s*$") + if host and count then + table.insert(stats, {host = host, count = count, origin_ua = origin_ua, mocked_ua = mocked_ua}) + end + end + file:close() +end + +local function rowstyle(i) + return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1" +end +%> + +

<%:Statistics%>

+
<%:User-Agent Mock Statistics%>
+ + + + + + + + + + <% for i, item in ipairs(stats) do %> + + + + + + + <% end %> +
<%:Host%><%:Modified Count%><%:Original User-Agent%><%:Mocked User-Agent%>
<%= item.host %><%= item.count %><%= item.origin_ua %><%= item.mocked_ua %>
+ + + + diff --git a/openwrt/po/zh_cn/ua3f.po b/openwrt/po/zh_cn/ua3f.po new file mode 100644 index 0000000..609e876 --- /dev/null +++ b/openwrt/po/zh_cn/ua3f.po @@ -0,0 +1,66 @@ +"Language: zh_CN\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Status" +msgstr "状态" + +msgid "General" +msgstr "常规" + +msgid "Enabled" +msgstr "启用" + +msgid "Running" +msgstr "运行中" + +msgid "Stopped" +msgstr "未运行" + +msgid "Settings" +msgstr "设置" + +msgid "Statistics" +msgstr "统计信息" + +msgid "Log" +msgstr "运行日志" + +msgid "Port" +msgstr "端口" + +msgid "Bind Address" +msgstr "绑定地址" + +msgid "Log Level" +msgstr "日志级别" + +msgid "User-Agent" +msgstr "User-Agent" + +msgid "User-Agent Regex Pattern" +msgstr "User-Agent 正则表达式" + +msgid "Regular expression pattern for matching User-Agent" +msgstr "用于匹配 User-Agent,只有匹配成功的 User-Agent 才会被修改,为空则匹配所有 User-Agent" + +msgid "Partial Replace" +msgstr "部分替换" + +msgid "Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty" +msgstr "仅替换 User-Agent 正则匹配的部分,仅在 User-Agent 正则表达式非空时有效" + +msgid "User-Agent Mock Statistics" +msgstr "User-Agent 修改次数实时统计" + +msgid "Host" +msgstr "地址" + +msgid "Modified Count" +msgstr "修改次数" + +msgid "Original User-Agent" +msgstr "原始 User-Agent" + +msgid "Mocked User-Agent" +msgstr "修改后 User-Agent" \ No newline at end of file diff --git a/po2lmo b/po2lmo new file mode 100755 index 0000000..8b5e9ef Binary files /dev/null and b/po2lmo differ diff --git a/src/http/http.go b/src/internal/http/http.go similarity index 100% rename from src/http/http.go rename to src/internal/http/http.go diff --git a/src/log/log.go b/src/internal/log/log.go similarity index 96% rename from src/log/log.go rename to src/internal/log/log.go index 0c62a66..bbea0ce 100644 --- a/src/log/log.go +++ b/src/internal/log/log.go @@ -11,6 +11,8 @@ import ( "gopkg.in/natefinch/lumberjack.v2" ) +const log_file = "/var/log/ua3f/ua3f.log" + type uctFormatter struct { } @@ -27,7 +29,6 @@ func (formatter *uctFormatter) Format(entry *logrus.Entry) ([]byte, error) { } func SetLogConf(level string) { - log_file := "/var/log/ua3f/ua3f.log" writer1 := &bytes.Buffer{} writer2 := os.Stdout writer3 := &lumberjack.Logger{ diff --git a/src/internal/statistics/statistics.go b/src/internal/statistics/statistics.go new file mode 100644 index 0000000..35cb9e7 --- /dev/null +++ b/src/internal/statistics/statistics.go @@ -0,0 +1,78 @@ +package statistics + +import ( + "fmt" + "os" + "sort" + "time" + + "github.com/sirupsen/logrus" +) + +type StatRecord struct { + Host string + Count int + OriginUA string + MockedUA string +} + +var ( + statChan = make(chan StatRecord, 3000) + stats = make(map[string]*StatRecord) +) + +const statsFile = "/var/log/ua3f/stats" + +func StartStatWorker() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case dest := <-statChan: + if record, exists := stats[dest.Host]; exists { + record.Count++ + record.OriginUA = dest.OriginUA + record.MockedUA = dest.MockedUA + } else { + stats[dest.Host] = &StatRecord{ + Host: dest.Host, + Count: 1, + OriginUA: dest.OriginUA, + MockedUA: dest.MockedUA, + } + } + case <-ticker.C: + dumpStatsToFile() + } + } +} + +func AddStat(dest *StatRecord) { + select { + case statChan <- *dest: + default: + } +} + +func dumpStatsToFile() { + f, err := os.Create(statsFile) + if err != nil { + logrus.Errorf("create stats file error: %v", err) + return + } + defer f.Close() + + var statList []StatRecord + for _, record := range stats { + statList = append(statList, *record) + } + sort.SliceStable(statList, func(i, j int) bool { + return statList[i].Count > statList[j].Count + }) + + for _, record := range statList { + line := fmt.Sprintf("%s %d %sSEQSEQ%s\n", record.Host, record.Count, record.OriginUA, record.MockedUA) + f.WriteString(line) + } +} diff --git a/src/main.go b/src/main.go index 766c41c..646ff86 100644 --- a/src/main.go +++ b/src/main.go @@ -15,7 +15,8 @@ import ( "github.com/dlclark/regexp2" "github.com/hashicorp/golang-lru/v2/expirable" "github.com/sirupsen/logrus" - "github.com/sunbk201/ua3f/log" + "github.com/sunbk201/ua3f/internal/log" + "github.com/sunbk201/ua3f/internal/statistics" ) var ( @@ -79,6 +80,9 @@ func main() { logrus.Fatal("Invalid User-Agent Regex Pattern: ", err) return } + + go statistics.StartStatWorker() + for { client, err := server.Accept() if err != nil { @@ -468,11 +472,17 @@ func transfer(dst net.Conn, src net.Conn, destAddrPort string) { return } logrus.Debug(fmt.Sprintf("[%s][%s] Hit User-Agent: %s", destAddrPort, src.(*net.TCPConn).RemoteAddr().String(), uaStr)) - request.Header.Set("User-Agent", buildNewUA(uaStr, payload, uaRegexp, enablePartialReplace)) + mockedUA := buildNewUA(uaStr, payload, uaRegexp, enablePartialReplace) + request.Header.Set("User-Agent", mockedUA) err = request.Write(dst) if err != nil { logrus.Error(fmt.Sprintf("[%s][%s] write error after replace user-agent: %s", destAddrPort, src.(*net.TCPConn).RemoteAddr().String(), err.Error())) break } + statistics.AddStat(&statistics.StatRecord{ + Host: destAddrPort, + OriginUA: uaStr, + MockedUA: mockedUA, + }) } }