feat: scan host LAN web devices via proxy
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user