feat: scan host LAN web devices via proxy

This commit is contained in:
Yoilun
2026-05-15 01:17:44 +08:00
parent f8a6d9803d
commit 1114ee00c1
7 changed files with 421 additions and 22 deletions

View File

@@ -3,11 +3,13 @@ package webdevice
import (
"errors"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
)
var (
@@ -36,6 +38,14 @@ func (s *Service) ProxyHTTP(w http.ResponseWriter, r *http.Request, targetIP, pr
rawQuery := r.URL.RawQuery
proxy := &httputil.ReverseProxy{
Transport: retryTransport{
base: &http.Transport{
DisableKeepAlives: true,
ResponseHeaderTimeout: 5 * time.Second,
},
attempts: 3,
delay: 80 * time.Millisecond,
},
Director: func(req *http.Request) {
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
@@ -43,7 +53,8 @@ func (s *Service) ProxyHTTP(w http.ResponseWriter, r *http.Request, targetIP, pr
req.URL.RawPath = ""
req.URL.RawQuery = rawQuery
req.Host = targetURL.Host
req.Header.Del("Accept-Encoding")
req.Header = sanitizeProxyRequestHeader(req.Header, req.URL.Path)
req.Close = true
},
ModifyResponse: func(resp *http.Response) error {
proxyPrefix := "/proxy/web/" + targetIP
@@ -77,6 +88,7 @@ func (s *Service) ProxyHTTP(w http.ResponseWriter, r *http.Request, targetIP, pr
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("web device proxy failed target=%s path=%s error=%v", targetURL.String(), r.URL.RequestURI(), err)
http.Error(w, "代理访问失败: "+err.Error(), http.StatusBadGateway)
},
}
@@ -85,6 +97,92 @@ func (s *Service) ProxyHTTP(w http.ResponseWriter, r *http.Request, targetIP, pr
return nil
}
type retryTransport struct {
base http.RoundTripper
attempts int
delay time.Duration
}
func (t retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
base := t.base
if base == nil {
base = http.DefaultTransport
}
attempts := t.attempts
if attempts < 1 {
attempts = 1
}
var lastErr error
for attempt := 0; attempt < attempts; attempt++ {
nextReq := req
if attempt > 0 {
nextReq = req.Clone(req.Context())
nextReq.Header = req.Header.Clone()
nextReq.Close = true
}
resp, err := base.RoundTrip(nextReq)
if err == nil {
return resp, nil
}
lastErr = err
if !shouldRetryProxyRequest(req, err) || attempt == attempts-1 {
return nil, err
}
time.Sleep(t.delay)
}
return nil, lastErr
}
func shouldRetryProxyRequest(req *http.Request, err error) bool {
if req == nil || err == nil {
return false
}
switch req.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
default:
return false
}
message := strings.ToLower(err.Error())
return strings.Contains(message, "eof") ||
strings.Contains(message, "connection reset") ||
strings.Contains(message, "broken pipe")
}
func sanitizeProxyRequestHeader(source http.Header, upstreamPath string) http.Header {
header := make(http.Header)
copyHeaderValue(header, source, "Accept")
copyHeaderValue(header, source, "Content-Type")
copyHeaderValue(header, source, "Authorization")
userAgent := strings.TrimSpace(source.Get("User-Agent"))
if userAgent == "" {
userAgent = "Mozilla/5.0"
}
header.Set("User-Agent", userAgent)
header.Set("Connection", "close")
if !isLoginPagePath(upstreamPath) {
copyHeaderValue(header, source, "Cookie")
}
return header
}
func copyHeaderValue(target, source http.Header, key string) {
if value := source.Values(key); len(value) > 0 {
target.Del(key)
for _, item := range value {
target.Add(key, item)
}
}
}
func isLoginPagePath(path string) bool {
path = strings.ToLower(path)
return strings.HasSuffix(path, "/doc/page/login.asp") || path == "/doc/page/login.asp"
}
type closeNotifyWriter struct {
http.ResponseWriter
}