mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 08:44:29 +00:00
feat: support http mode
This commit is contained in:
parent
29840b64fb
commit
2c8763a918
@ -1,10 +1,10 @@
|
||||
# UA3F
|
||||
|
||||
UA3F 是下一代 HTTP User-Agent 重写工具,作为一个 SOCKS5/TPROXY/REDIRECT 服务部署在路由器等设备进行透明 User-Agent 重写。
|
||||
UA3F 是下一代 HTTP User-Agent 重写工具,作为一个 HTTP、SOCKS5、TPROXY、REDIRECT 代理服务对 HTTP 进行 User-Agent 透明重写。
|
||||
|
||||
## 特性
|
||||
|
||||
- 支持多种服务模式:SOCKS5、TPROXY、REDIRECT
|
||||
- 支持多种服务模式:HTTP、SOCKS5、TPROXY、REDIRECT
|
||||
- 支持正则表达式规则匹配重写 User-Agent
|
||||
- 自定义重写 User-Agent 内容
|
||||
- LRU 高速缓存非 HTTP 流量,加速非 HTTP 流量转发
|
||||
@ -56,7 +56,7 @@ UA3F 支持 LuCI Web 页面,可以打开 Services -> UA3F 进行相关配置
|
||||
> [!NOTE]
|
||||
> 设置说明:
|
||||
>
|
||||
> - Server Mode (服务模式): 支持 `SOCKS5`、`TPROXY`、`REDIRECT` 三种模式,默认 `SOCKS5`
|
||||
> - Server Mode (服务模式): 支持 `HTTP`、`SOCKS5`、`TPROXY`、`REDIRECT`,默认 `SOCKS5`
|
||||
> - Port (监听端口): 默认 `1080`
|
||||
> - Bind Address (绑定地址): 默认 `0.0.0.0`
|
||||
> - Log Level (日志等级): 默认 `error`, 如果需要调试排查错误可以设置为 `debug`
|
||||
@ -87,7 +87,7 @@ sudo -u shellcrash /usr/bin/ua3f
|
||||
|
||||
相关命令行启动参数:
|
||||
|
||||
- `-m <mode>`: 服务模式,支持 SOCKS5、TPROXY、REDIRECT,默认 SOCKS5
|
||||
- `-m <mode>`: 服务模式,支持 HTTP、SOCKS5、TPROXY、REDIRECT,默认 SOCKS5
|
||||
- `-b <bind addr>`: 自定义绑定监听地址,默认 127.0.0.1
|
||||
- `-p <port>`: 端口号,默认 1080
|
||||
- `-l <log level>`: 日志等级,默认 info,可选:debug,默认日志位置:`/var/log/ua3f.log`
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ServerModeHTTP = "HTTP"
|
||||
ServerModeSocks5 = "SOCKS5"
|
||||
ServerModeTProxy = "TPROXY"
|
||||
ServerModeRedirect = "REDIRECT"
|
||||
@ -35,13 +36,13 @@ func Parse() (*Config, bool) {
|
||||
showVer bool
|
||||
)
|
||||
|
||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: SOCKS5, TPROXY, REDIRECT (default: SOCKS5)")
|
||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: HTTP, SOCKS5, TPROXY, REDIRECT (default: SOCKS5)")
|
||||
flag.StringVar(&bindAddr, "b", "127.0.0.1", "bind address (default: 127.0.0.1)")
|
||||
flag.IntVar(&port, "p", 1080, "port")
|
||||
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
|
||||
flag.StringVar(&uaPattern, "r", "", "UA-Pattern")
|
||||
flag.BoolVar(&partial, "s", false, "Enable Regex Partial Replace")
|
||||
flag.StringVar(&loglevel, "l", "error", "Log level (default: error)")
|
||||
flag.StringVar(&loglevel, "l", "info", "Log level (default: info)")
|
||||
flag.BoolVar(&showVer, "v", false, "show version")
|
||||
flag.Parse()
|
||||
|
||||
|
||||
@ -83,6 +83,74 @@ func New(cfg *config.Config) (*Rewriter, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RewriteAndForward rewrites the User-Agent header if needed and forwards the request.
|
||||
// Returns pass=true if the request has been forwarded as-is (no rewrite).
|
||||
func (r *Rewriter) RewriteAndForward(dst net.Conn, req *http.Request, destAddr string, srcAddr string) (pass bool, err error) {
|
||||
|
||||
originalUA := req.Header.Get("User-Agent")
|
||||
|
||||
// No UA header: pass-through after writing this first request
|
||||
if originalUA == "" {
|
||||
r.Cache.Add(destAddr, destAddr)
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, "Not found User-Agent, Add LRU Relay Cache")
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
}
|
||||
pass = true
|
||||
return
|
||||
}
|
||||
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Original User-Agent: %s", originalUA))
|
||||
|
||||
isWhitelist := r.inWhitelist(originalUA)
|
||||
matches := true
|
||||
if r.pattern != "" {
|
||||
matches, err = r.uaRegex.MatchString(originalUA)
|
||||
if err != nil {
|
||||
log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Pattern Match Error: %s", err.Error()))
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
|
||||
// If UA is whitelisted or does not match target pattern, write once then pass-through.
|
||||
if isWhitelist || !matches {
|
||||
if !matches {
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Not Hit User-Agent Regex: %s", originalUA))
|
||||
}
|
||||
if isWhitelist {
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent Whitelist: %s", originalUA))
|
||||
r.Cache.Add(destAddr, destAddr)
|
||||
}
|
||||
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||
Host: destAddr,
|
||||
UA: originalUA,
|
||||
})
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
}
|
||||
pass = true
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite UA and forward the request (including body)
|
||||
rewritedUA := r.buildNewUA(originalUA)
|
||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Rewrite User-Agent from (%s) to (%s)", originalUA, rewritedUA))
|
||||
req.Header.Set("User-Agent", rewritedUA)
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
pass = true
|
||||
return
|
||||
}
|
||||
|
||||
statistics.AddRewriteRecord(&statistics.RewriteRecord{
|
||||
Host: destAddr,
|
||||
OriginalUA: originalUA,
|
||||
MockedUA: rewritedUA,
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ProxyHTTPOrRaw reads traffic from src and writes to dst.
|
||||
// - If target in LRU cache: pass-through (raw).
|
||||
// - Else if HTTP: rewrite UA (unless whitelisted or pattern not matched).
|
||||
@ -114,11 +182,12 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddr string, s
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
var pass bool
|
||||
|
||||
// HTTP request loop (handles keep-alive)
|
||||
for {
|
||||
isHTTP, err = r.isHTTP(reader)
|
||||
if err != nil {
|
||||
|
||||
if isHTTP, err = r.isHTTP(reader); err != nil {
|
||||
err = fmt.Errorf("isHTTP: %w", err)
|
||||
return
|
||||
}
|
||||
@ -132,67 +201,13 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddr string, s
|
||||
}
|
||||
return
|
||||
}
|
||||
req, err = http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
if req, err = http.ReadRequest(reader); err != nil {
|
||||
err = fmt.Errorf("http.ReadRequest: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
originalUA := req.Header.Get("User-Agent")
|
||||
|
||||
// No UA header: pass-through after writing this first request
|
||||
if originalUA == "" {
|
||||
r.Cache.Add(destAddr, destAddr)
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, "Not found User-Agent, Add LRU Relay Cache")
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
}
|
||||
if pass, err = r.RewriteAndForward(dst, req, destAddr, srcAddr); pass {
|
||||
return
|
||||
}
|
||||
|
||||
isWhitelist := r.inWhitelist(originalUA)
|
||||
matches := true
|
||||
if r.pattern != "" {
|
||||
matches, err = r.uaRegex.MatchString(originalUA)
|
||||
if err != nil {
|
||||
log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Pattern Match Error: %s", err.Error()))
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
|
||||
// If UA is whitelisted or does not match target pattern, write once then pass-through.
|
||||
if isWhitelist || !matches {
|
||||
if !matches {
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Not Hit User-Agent Regex: %s", originalUA))
|
||||
}
|
||||
if isWhitelist {
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent Whitelist: %s", originalUA))
|
||||
r.Cache.Add(destAddr, destAddr)
|
||||
}
|
||||
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||
Host: destAddr,
|
||||
UA: originalUA,
|
||||
})
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite UA and forward the request (including body)
|
||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent: %s", originalUA))
|
||||
mockedUA := r.buildNewUA(originalUA)
|
||||
req.Header.Set("User-Agent", mockedUA)
|
||||
if err = req.Write(dst); err != nil {
|
||||
err = fmt.Errorf("req.Write: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
statistics.AddRewriteRecord(&statistics.RewriteRecord{
|
||||
Host: destAddr,
|
||||
OriginalUA: originalUA,
|
||||
MockedUA: mockedUA,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
116
src/internal/server/http/http.go
Normal file
116
src/internal/server/http/http.go
Normal file
@ -0,0 +1,116 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sunbk201/ua3f/internal/config"
|
||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||
"github.com/sunbk201/ua3f/internal/server/utils"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
rw *rewrite.Rewriter
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
rw: rw,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
server := &http.Server{
|
||||
Addr: s.cfg.ListenAddr,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == http.MethodConnect {
|
||||
s.handleTunneling(w, req)
|
||||
} else {
|
||||
s.handleHTTP(w, req)
|
||||
}
|
||||
}),
|
||||
}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
req.RequestURI = ""
|
||||
req.URL.Scheme = "http"
|
||||
req.URL.Host = req.Host
|
||||
destPort := req.URL.Port()
|
||||
if destPort == "" {
|
||||
destPort = "80"
|
||||
}
|
||||
destAddr := fmt.Sprintf("%s:%s", req.URL.Hostname(), destPort)
|
||||
|
||||
logrus.Infof("HTTP request for %s", destAddr)
|
||||
|
||||
target, err := utils.Connect(destAddr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
_, err = s.rw.RewriteAndForward(target, req, req.Host, req.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
resp, err := http.ReadResponse(bufio.NewReader(target), req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
for k, v := range resp.Header {
|
||||
for _, vv := range v {
|
||||
w.Header().Add(k, vv)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) {
|
||||
logrus.Infof("HTTP CONNECT request for %s", req.Host)
|
||||
destAddr := req.Host
|
||||
dest, err := utils.Connect(destAddr)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
client, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
||||
s.ForwardTCP(client, dest, destAddr)
|
||||
}
|
||||
|
||||
func (s *Server) HandleClient(client net.Conn) {
|
||||
}
|
||||
|
||||
// ForwardTCP proxies traffic in both directions.
|
||||
// target->client uses raw copy.
|
||||
// client->target is processed by the rewriter (or raw if cached).
|
||||
func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
||||
// Server -> Client (raw)
|
||||
go utils.CopyHalf(client, target)
|
||||
|
||||
// Client -> Server (rewriter)
|
||||
go utils.ProxyHalf(target, client, s.rw, destAddr)
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/sunbk201/ua3f/internal/config"
|
||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||
"github.com/sunbk201/ua3f/internal/server/http"
|
||||
"github.com/sunbk201/ua3f/internal/server/redirect"
|
||||
"github.com/sunbk201/ua3f/internal/server/socks5"
|
||||
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
||||
@ -19,6 +20,8 @@ type Server interface {
|
||||
|
||||
func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
|
||||
switch cfg.ServerMode {
|
||||
case config.ServerModeHTTP:
|
||||
return http.New(cfg, rw), nil
|
||||
case config.ServerModeSocks5:
|
||||
return socks5.New(cfg, rw), nil
|
||||
case config.ServerModeTProxy:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user