feat: record path-through stats

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

View File

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

View File

@ -64,3 +64,12 @@ msgstr "原始 User-Agent"
msgid "Modified User-Agent" msgid "Modified User-Agent"
msgstr "重写后 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()) destAddrPort, srcAddr, originalUA, r.cache.Len())
r.cache.Add(destAddrPort, destAddrPort) r.cache.Add(destAddrPort, destAddrPort)
} }
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
Host: destAddrPort,
UA: originalUA,
})
if err := req.Write(dst); err != nil { if err := req.Write(dst); err != nil {
logrus.Errorf("[%s][%s] write error: %s", destAddrPort, srcAddr, err.Error()) logrus.Errorf("[%s][%s] write error: %s", destAddrPort, srcAddr, err.Error())
return err return err
@ -166,10 +170,10 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddrPort strin
return err return err
} }
statistics.AddStat(&statistics.StatRecord{ statistics.AddRewriteRecord(&statistics.RewriteRecord{
Host: destAddrPort, Host: destAddrPort,
OriginUA: originalUA, OriginalUA: originalUA,
MockedUA: mockedUA, 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. // buildNewUA returns either a partial replacement (regex) or full overwrite.
func (r *Rewriter) buildNewUA(originUA string) string { 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) newUA, err := r.uaRegex.Replace(originUA, r.payloadUA, -1, -1)
if err != nil { if err != nil {
logrus.Errorf("User-Agent Replace Error: %s, use full overwrite", err.Error()) 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 // Start statistics worker
go statistics.StartStatWorker() go statistics.StartRecorder()
var client net.Conn var client net.Conn
for { 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 package statistics
import ( import (
"fmt"
"os"
"sort"
"time" "time"
"github.com/sirupsen/logrus"
) )
type StatRecord struct {
Host string
Count int
OriginUA string
MockedUA string
}
var ( var (
statChan = make(chan StatRecord, 3000) rewriteRecordChan = make(chan RewriteRecord, 2000)
stats = make(map[string]*StatRecord) passThroughRecordChan = make(chan PassThroughRecord, 2000)
) )
const statsFile = "/var/log/ua3f/stats" func StartRecorder() {
func StartStatWorker() {
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case dest := <-statChan: case record := <-rewriteRecordChan:
if record, exists := stats[dest.Host]; exists { if r, exists := rewriteRecords[record.Host]; exists {
record.Count++ r.Count++
record.OriginUA = dest.OriginUA r.OriginalUA = record.OriginalUA
record.MockedUA = dest.MockedUA r.MockedUA = record.MockedUA
} else { } else {
stats[dest.Host] = &StatRecord{ rewriteRecords[record.Host] = &RewriteRecord{
Host: dest.Host, Host: record.Host,
Count: 1, Count: 1,
OriginUA: dest.OriginUA, OriginalUA: record.OriginalUA,
MockedUA: dest.MockedUA, 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,
} }
} }
case <-ticker.C: 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)
}
}