|
@@ -7,6 +7,7 @@ import (
|
|
|
"errors"
|
|
"errors"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"io"
|
|
"io"
|
|
|
|
|
+ "net"
|
|
|
"net/http"
|
|
"net/http"
|
|
|
"net/url"
|
|
"net/url"
|
|
|
"strconv"
|
|
"strconv"
|
|
@@ -16,6 +17,7 @@ import (
|
|
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
|
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/util/netsafe"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const remoteHTTPTimeout = 10 * time.Second
|
|
const remoteHTTPTimeout = 10 * time.Second
|
|
@@ -25,6 +27,7 @@ var remoteHTTPClient = &http.Client{
|
|
|
MaxIdleConns: 64,
|
|
MaxIdleConns: 64,
|
|
|
MaxIdleConnsPerHost: 4,
|
|
MaxIdleConnsPerHost: 4,
|
|
|
IdleConnTimeout: 60 * time.Second,
|
|
IdleConnTimeout: 60 * time.Second,
|
|
|
|
|
+ DialContext: netsafe.SSRFGuardedDialContext,
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -50,7 +53,18 @@ func NewRemote(n *model.Node) *Remote {
|
|
|
|
|
|
|
|
func (r *Remote) Name() string { return "node:" + r.node.Name }
|
|
func (r *Remote) Name() string { return "node:" + r.node.Name }
|
|
|
|
|
|
|
|
-func (r *Remote) baseURL() string {
|
|
|
|
|
|
|
+func (r *Remote) baseURL() (string, error) {
|
|
|
|
|
+ addr, err := netsafe.NormalizeHost(r.node.Address)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return "", err
|
|
|
|
|
+ }
|
|
|
|
|
+ scheme := r.node.Scheme
|
|
|
|
|
+ if scheme != "http" && scheme != "https" {
|
|
|
|
|
+ scheme = "https"
|
|
|
|
|
+ }
|
|
|
|
|
+ if r.node.Port <= 0 || r.node.Port > 65535 {
|
|
|
|
|
+ return "", fmt.Errorf("invalid node port %d", r.node.Port)
|
|
|
|
|
+ }
|
|
|
bp := r.node.BasePath
|
|
bp := r.node.BasePath
|
|
|
if bp == "" {
|
|
if bp == "" {
|
|
|
bp = "/"
|
|
bp = "/"
|
|
@@ -58,7 +72,12 @@ func (r *Remote) baseURL() string {
|
|
|
if !strings.HasSuffix(bp, "/") {
|
|
if !strings.HasSuffix(bp, "/") {
|
|
|
bp += "/"
|
|
bp += "/"
|
|
|
}
|
|
}
|
|
|
- return fmt.Sprintf("%s://%s:%d%s", r.node.Scheme, r.node.Address, r.node.Port, bp)
|
|
|
|
|
|
|
+ u := &url.URL{
|
|
|
|
|
+ Scheme: scheme,
|
|
|
|
|
+ Host: net.JoinHostPort(addr, strconv.Itoa(r.node.Port)),
|
|
|
|
|
+ Path: bp,
|
|
|
|
|
+ }
|
|
|
|
|
+ return u.String(), nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelope, error) {
|
|
func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelope, error) {
|
|
@@ -66,7 +85,11 @@ func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelo
|
|
|
return nil, errors.New("node has no API token configured")
|
|
return nil, errors.New("node has no API token configured")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- target := r.baseURL() + strings.TrimPrefix(path, "/")
|
|
|
|
|
|
|
+ base, err := r.baseURL()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ target := base + strings.TrimPrefix(path, "/")
|
|
|
|
|
|
|
|
var (
|
|
var (
|
|
|
reqBody io.Reader
|
|
reqBody io.Reader
|
|
@@ -78,15 +101,15 @@ func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelo
|
|
|
reqBody = strings.NewReader(b.Encode())
|
|
reqBody = strings.NewReader(b.Encode())
|
|
|
contentType = "application/x-www-form-urlencoded"
|
|
contentType = "application/x-www-form-urlencoded"
|
|
|
default:
|
|
default:
|
|
|
- buf, err := json.Marshal(b)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, fmt.Errorf("marshal body: %w", err)
|
|
|
|
|
|
|
+ buf, jerr := json.Marshal(b)
|
|
|
|
|
+ if jerr != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("marshal body: %w", jerr)
|
|
|
}
|
|
}
|
|
|
reqBody = bytes.NewReader(buf)
|
|
reqBody = bytes.NewReader(buf)
|
|
|
contentType = "application/json"
|
|
contentType = "application/json"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- cctx, cancel := context.WithTimeout(ctx, remoteHTTPTimeout)
|
|
|
|
|
|
|
+ cctx, cancel := context.WithTimeout(netsafe.ContextWithAllowPrivate(ctx, r.node.AllowPrivateAddress), remoteHTTPTimeout)
|
|
|
defer cancel()
|
|
defer cancel()
|
|
|
req, err := http.NewRequestWithContext(cctx, method, target, reqBody)
|
|
req, err := http.NewRequestWithContext(cctx, method, target, reqBody)
|
|
|
if err != nil {
|
|
if err != nil {
|