179 lines
5.6 KiB
Go
179 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/creator-hub/internal/hub"
|
|
)
|
|
|
|
const networkExitObservationURL = "https://ipinfo.io/json"
|
|
|
|
type networkExitProbe interface {
|
|
Check(context.Context, hub.NetworkExitAccess) (hub.ExitObservation, string)
|
|
}
|
|
|
|
type httpNetworkExitProbe struct {
|
|
endpoint string
|
|
client *http.Client
|
|
resolve func(hub.NetworkExitAccess) (string, error)
|
|
}
|
|
|
|
func defaultNetworkExitProbe() networkExitProbe {
|
|
return httpNetworkExitProbe{endpoint: networkExitObservationURL, client: &http.Client{Timeout: 20 * time.Second}, resolve: resolveExitCredential}
|
|
}
|
|
|
|
func (probe httpNetworkExitProbe) Check(ctx context.Context, exit hub.NetworkExitAccess) (hub.ExitObservation, string) {
|
|
proxyURL := &url.URL{Scheme: exit.Protocol, Host: net.JoinHostPort(exit.Host, fmt.Sprint(exit.Port))}
|
|
proxyUsername := ""
|
|
if exit.CredentialReference != nil {
|
|
secret, err := probe.resolve(exit)
|
|
if err != nil {
|
|
return hub.ExitObservation{}, "credential_unavailable"
|
|
}
|
|
username, password, found := strings.Cut(secret, ":")
|
|
if !found || username == "" {
|
|
return hub.ExitObservation{}, "credential_invalid"
|
|
}
|
|
proxyUsername = username
|
|
proxyURL.User = url.UserPassword(username, password)
|
|
}
|
|
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
|
|
if exit.Protocol == "socks4" {
|
|
transport.Proxy = nil
|
|
transport.DialContext = socks4DialContext(proxyURL.Host, proxyUsername)
|
|
}
|
|
defer transport.CloseIdleConnections()
|
|
client := *probe.client
|
|
client.Transport = transport
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, probe.endpoint, nil)
|
|
if err != nil {
|
|
return hub.ExitObservation{}, "proxy_check_failed"
|
|
}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
if strings.Contains(strings.ToLower(err.Error()), "auth") {
|
|
return hub.ExitObservation{}, "proxy_auth_failed"
|
|
}
|
|
return hub.ExitObservation{}, "proxy_check_failed"
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode == http.StatusProxyAuthRequired {
|
|
return hub.ExitObservation{}, "proxy_auth_failed"
|
|
}
|
|
if response.StatusCode != http.StatusOK {
|
|
return hub.ExitObservation{}, "proxy_check_failed"
|
|
}
|
|
var observed struct {
|
|
IP string `json:"ip"`
|
|
Region string `json:"region"`
|
|
}
|
|
decoder := json.NewDecoder(io.LimitReader(response.Body, 64<<10))
|
|
if err := decoder.Decode(&observed); err != nil || net.ParseIP(observed.IP) == nil || len(observed.Region) > 64 {
|
|
return hub.ExitObservation{}, "exit_observation_invalid"
|
|
}
|
|
return hub.ExitObservation{PublicIP: observed.IP, Region: observed.Region}, ""
|
|
}
|
|
|
|
func socks4DialContext(proxyAddress, userID string) func(context.Context, string, string) (net.Conn, error) {
|
|
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
connection, err := (&net.Dialer{}).DialContext(ctx, network, proxyAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
failed := true
|
|
defer func() {
|
|
if failed {
|
|
_ = connection.Close()
|
|
}
|
|
}()
|
|
host, portText, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, errors.New("invalid SOCKS4 destination")
|
|
}
|
|
port, err := net.LookupPort("tcp", portText)
|
|
if err != nil {
|
|
return nil, errors.New("invalid SOCKS4 destination port")
|
|
}
|
|
request := []byte{4, 1, 0, 0, 0, 0, 0, 1}
|
|
binary.BigEndian.PutUint16(request[2:4], uint16(port))
|
|
if ip := net.ParseIP(host).To4(); ip != nil {
|
|
copy(request[4:8], ip)
|
|
}
|
|
request = append(request, userID...)
|
|
request = append(request, 0)
|
|
if net.ParseIP(host).To4() == nil {
|
|
request = append(request, host...)
|
|
request = append(request, 0)
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = connection.SetDeadline(deadline)
|
|
}
|
|
if _, err := connection.Write(request); err != nil {
|
|
return nil, err
|
|
}
|
|
response := make([]byte, 8)
|
|
if _, err := io.ReadFull(connection, response); err != nil || response[1] != 90 {
|
|
return nil, errors.New("SOCKS4 proxy rejected connection")
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
failed = false
|
|
return connection, nil
|
|
}
|
|
}
|
|
|
|
// Secret managers and keyring bridges inject the referenced value at process start.
|
|
// Only the resolved username:password value is kept in the request-local call stack.
|
|
func resolveExitCredential(exit hub.NetworkExitAccess) (string, error) {
|
|
if exit.CredentialReference == nil || exit.CredentialKey == "" {
|
|
return "", errors.New("credential reference unavailable")
|
|
}
|
|
value, ok := os.LookupEnv(credentialEnvironmentName(exit.CredentialKey))
|
|
if !ok || value == "" {
|
|
return "", errors.New("credential value unavailable")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func credentialEnvironmentName(key string) string {
|
|
digest := sha256.Sum256([]byte(key))
|
|
return "CREATORHUB_CREDENTIAL_" + strings.ToUpper(hex.EncodeToString(digest[:]))
|
|
}
|
|
|
|
type gatewayNetworkExit struct {
|
|
Protocol string `json:"protocol"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
Username string `json:"username,omitempty"`
|
|
Password string `json:"password,omitempty"`
|
|
}
|
|
|
|
func gatewayNetworkExitFor(exit hub.NetworkExitAccess, resolve func(hub.NetworkExitAccess) (string, error)) (gatewayNetworkExit, error) {
|
|
result := gatewayNetworkExit{Protocol: exit.Protocol, Host: exit.Host, Port: exit.Port}
|
|
if exit.CredentialReference == nil {
|
|
return result, nil
|
|
}
|
|
secret, err := resolve(exit)
|
|
if err != nil {
|
|
return gatewayNetworkExit{}, errors.New("credential unavailable")
|
|
}
|
|
username, password, found := strings.Cut(secret, ":")
|
|
if !found || username == "" {
|
|
return gatewayNetworkExit{}, errors.New("credential invalid")
|
|
}
|
|
result.Username, result.Password = username, password
|
|
return result, nil
|
|
}
|