feat: record path-through stats

This commit is contained in:
SunBK201 2025-10-27 20:47:03 +08:00
parent 1af71e6a2e
commit 4b4571de2a
7 changed files with 212 additions and 84 deletions

View File

@ -1,14 +1,26 @@
<%
local stats = {}
local file = io.open("/var/log/ua3f/stats", "r")
if file then
for line in file:lines() do
local rewrite_stats = {}
local rewrite_stats_file = io.open("/var/log/ua3f/rewrite_stats", "r")
if rewrite_stats_file then
for line in rewrite_stats_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})
table.insert(rewrite_stats, {host = host, count = count, origin_ua = origin_ua, mocked_ua = mocked_ua})
end
end
file:close()
rewrite_stats_file:close()
end
local pass_stats = {}
local pass_stats_file = io.open("/var/log/ua3f/passthrough_stats", "r")
if pass_stats_file then
for line in pass_stats_file:lines() do
local host, count, ua = line:match("^(%S+)%s(%d+)%s(.+)$")
if ua and count then
table.insert(pass_stats, {ua = ua, count = count, host = host})
end
end
pass_stats_file:close()
end
local function rowstyle(i)
@ -17,9 +29,9 @@ end
%>
<h3><%:Statistics%></h3>
<div class="cbi-section-descr"><%:User-Agent Rewrite Statistics%></div>
<table id="stats-table" class="table cbi-section-table">
<div class="cbi-section-descr"><%:User-Agent Rewrite Statistics%></div>
<table id="rewrite-stats-table" class="table cbi-section-table">
<tr class="tr table-titles">
<th class="th"><%:Host%></th>
<th class="th"><%:Rewrite Count%></th>
@ -27,7 +39,7 @@ end
<th class="th"><%:Modified User-Agent%></th>
</tr>
<% for i, item in ipairs(stats) do %>
<% for i, item in ipairs(rewrite_stats) do %>
<tr class="tr <%= rowstyle(i) %>">
<td class="td" data-title="<%:Host%>"><span><%= item.host %></span></td>
<td class="td" data-title="<%:Rewrite Count%>"><%= item.count %></td>
@ -37,6 +49,24 @@ end
<% end %>
</table>
<div class="cbi-section-descr"><%:User-Agent Pass-Through Statistics%></div>
<table id="pass-stats-table" class="table cbi-section-table">
<tr class="tr table-titles">
<th class="th"><%:User-Agent%></th>
<th class="th"><%:Pass-Through Count%></th>
<th class="th"><%:Last Host%></th>
</tr>
<% for i, item in ipairs(pass_stats) do %>
<tr class="tr <%= rowstyle(i) %>">
<td class="td" data-title="<%:User-Agent%>"><span><%= item.ua %></span></td>
<td class="td" data-title="<%:Pass-Through Count%>"><%= item.count %></td>
<td class="td" data-title="<%:Last Host%>"><span><%= item.host %></span></td>
</tr>
<% end %>
</table>
<script type="text/javascript">
async function updateStats() {
try {
@ -45,10 +75,14 @@ end
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const newTable = doc.querySelector("#stats-table");
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
const newPassTable = doc.querySelector("#pass-stats-table");
if (newTable) {
document.querySelector("#stats-table").innerHTML = newTable.innerHTML;
if (newRewriteTable) {
document.querySelector("#rewrite-stats-table").innerHTML = newRewriteTable.innerHTML;
}
if (newPassTable) {
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
}
} catch (err) {
console.error("update stats error:", err);
@ -58,25 +92,40 @@ end
</script>
<style>
#stats-table th:nth-child(1),
#stats-table td:nth-child(1) {
#rewrite-stats-table th:nth-child(1),
#rewrite-stats-table td:nth-child(1) {
width: 20%;
}
#stats-table th:nth-child(2),
#stats-table td:nth-child(2) {
#rewrite-stats-table th:nth-child(2),
#rewrite-stats-table td:nth-child(2) {
width: 10%;
text-align: center;
}
#stats-table th:nth-child(3),
#stats-table td:nth-child(3) {
#rewrite-stats-table th:nth-child(3),
#rewrite-stats-table td:nth-child(3) {
width: 35%;
}
#stats-table th:nth-child(4),
#stats-table td:nth-child(4) {
#rewrite-stats-table th:nth-child(4),
#rewrite-stats-table td:nth-child(4) {
width: 35%;
}
#pass-stats-table th:nth-child(1),
#pass-stats-table td:nth-child(1) {
width: 50%;
}
#pass-stats-table th:nth-child(2),
#pass-stats-table td:nth-child(2) {
width: 20%;
text-align: center;
}
#pass-stats-table th:nth-child(3),
#pass-stats-table td:nth-child(3) {
width: 30%;
}
</style>

View File

@ -64,3 +64,12 @@ msgstr "原始 User-Agent"
msgid "Modified User-Agent"
msgstr "重写后 User-Agent"
msgid "User-Agent Pass-Through Statistics"
msgstr "User-Agent 放行次数实时统计"
msgid "Pass-Through Count"
msgstr "放行次数"
msgid "Last Host"
msgstr "最后访问地址"

View File

@ -148,6 +148,10 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddrPort strin
destAddrPort, srcAddr, originalUA, r.cache.Len())
r.cache.Add(destAddrPort, destAddrPort)
}
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
Host: destAddrPort,
UA: originalUA,
})
if err := req.Write(dst); err != nil {
logrus.Errorf("[%s][%s] write error: %s", destAddrPort, srcAddr, err.Error())
return err
@ -166,9 +170,9 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddrPort strin
return err
}
statistics.AddStat(&statistics.StatRecord{
statistics.AddRewriteRecord(&statistics.RewriteRecord{
Host: destAddrPort,
OriginUA: originalUA,
OriginalUA: originalUA,
MockedUA: mockedUA,
})
}
@ -196,7 +200,7 @@ func (r *Rewriter) isHTTP(reader *bufio.Reader) (bool, error) {
// buildNewUA returns either a partial replacement (regex) or full overwrite.
func (r *Rewriter) buildNewUA(originUA string) string {
if r.enablePartialReplace && r.uaRegex != nil {
if r.enablePartialReplace && r.uaRegex != nil && r.pattern != "" {
newUA, err := r.uaRegex.Replace(originUA, r.payloadUA, -1, -1)
if err != nil {
logrus.Errorf("User-Agent Replace Error: %s, use full overwrite", err.Error())

View File

@ -56,7 +56,7 @@ func (s *Server) Start() (err error) {
}
// Start statistics worker
go statistics.StartStatWorker()
go statistics.StartRecorder()
var client net.Conn
for {

View File

@ -0,0 +1,48 @@
package statistics
import (
"fmt"
"os"
"sort"
"github.com/sirupsen/logrus"
)
const passthroughStatsFile = "/var/log/ua3f/passthrough_stats"
type PassThroughRecord struct {
Host string
UA string
Count int
}
var passThroughRecords = make(map[string]*PassThroughRecord)
func AddPassThroughRecord(record *PassThroughRecord) {
select {
case passThroughRecordChan <- *record:
default:
}
}
func dumpPassThroughRecords() {
f, err := os.Create(passthroughStatsFile)
if err != nil {
logrus.Errorf("create stats file error: %v", err)
return
}
defer f.Close()
var statList []PassThroughRecord
for _, record := range passThroughRecords {
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 %s\n", record.Host, record.Count, record.UA)
f.WriteString(line)
}
}

View File

@ -0,0 +1,49 @@
package statistics
import (
"fmt"
"os"
"sort"
"github.com/sirupsen/logrus"
)
const rewriteStatsFile = "/var/log/ua3f/rewrite_stats"
type RewriteRecord struct {
Host string
Count int
OriginalUA string
MockedUA string
}
var rewriteRecords = make(map[string]*RewriteRecord)
func AddRewriteRecord(record *RewriteRecord) {
select {
case rewriteRecordChan <- *record:
default:
}
}
func dumpRewriteRecords() {
f, err := os.Create(rewriteStatsFile)
if err != nil {
logrus.Errorf("create stats file error: %v", err)
return
}
defer f.Close()
var statList []RewriteRecord
for _, record := range rewriteRecords {
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.OriginalUA, record.MockedUA)
f.WriteString(line)
}
}

View File

@ -1,78 +1,47 @@
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)
rewriteRecordChan = make(chan RewriteRecord, 2000)
passThroughRecordChan = make(chan PassThroughRecord, 2000)
)
const statsFile = "/var/log/ua3f/stats"
func StartStatWorker() {
func StartRecorder() {
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
case record := <-rewriteRecordChan:
if r, exists := rewriteRecords[record.Host]; exists {
r.Count++
r.OriginalUA = record.OriginalUA
r.MockedUA = record.MockedUA
} else {
stats[dest.Host] = &StatRecord{
Host: dest.Host,
rewriteRecords[record.Host] = &RewriteRecord{
Host: record.Host,
Count: 1,
OriginalUA: record.OriginalUA,
MockedUA: record.MockedUA,
}
}
case record := <-passThroughRecordChan:
if r, exists := passThroughRecords[record.UA]; exists {
r.Count++
r.Host = record.Host
} else {
passThroughRecords[record.UA] = &PassThroughRecord{
Host: record.Host,
UA: record.UA,
Count: 1,
OriginUA: dest.OriginUA,
MockedUA: dest.MockedUA,
}
}
case <-ticker.C:
dumpStatsToFile()
dumpRewriteRecords()
dumpPassThroughRecords()
}
}
}
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)
}
}