mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package githubrelease
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"Wavelet/pkg/httppool"
|
||||
)
|
||||
|
||||
const (
|
||||
clientTimeout = 10 * time.Minute
|
||||
dialTimeout = 30 * time.Second
|
||||
dialKeepAlive = 30 * time.Second
|
||||
responseHeaderTimeout = 30 * time.Second
|
||||
maxRedirects = 5
|
||||
maxRetryAfterSeconds = math.MaxInt64 / int64(time.Second)
|
||||
)
|
||||
|
||||
var (
|
||||
errBlockedTarget = errors.New("GitHub Release 请求目标不是公网地址")
|
||||
errResolveTarget = errors.New("GitHub Release 请求目标解析失败")
|
||||
errRedirectLimit = errors.New("GitHub Release asset 重定向次数过多")
|
||||
|
||||
publicIPv6Prefix = netip.MustParsePrefix("2000::/3")
|
||||
nonPublicPrefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/8"),
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
netip.MustParsePrefix("100.64.0.0/10"),
|
||||
netip.MustParsePrefix("127.0.0.0/8"),
|
||||
netip.MustParsePrefix("169.254.0.0/16"),
|
||||
netip.MustParsePrefix("172.16.0.0/12"),
|
||||
netip.MustParsePrefix("192.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.0.2.0/24"),
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
netip.MustParsePrefix("198.18.0.0/15"),
|
||||
netip.MustParsePrefix("198.51.100.0/24"),
|
||||
netip.MustParsePrefix("203.0.113.0/24"),
|
||||
netip.MustParsePrefix("224.0.0.0/4"),
|
||||
netip.MustParsePrefix("240.0.0.0/4"),
|
||||
netip.MustParsePrefix("::/128"),
|
||||
netip.MustParsePrefix("::1/128"),
|
||||
netip.MustParsePrefix("::ffff:0:0/96"),
|
||||
netip.MustParsePrefix("64:ff9b::/96"),
|
||||
netip.MustParsePrefix("100::/64"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
netip.MustParsePrefix("fc00::/7"),
|
||||
netip.MustParsePrefix("fe80::/10"),
|
||||
netip.MustParsePrefix("ff00::/8"),
|
||||
}
|
||||
)
|
||||
|
||||
type resolver interface {
|
||||
LookupNetIP(context.Context, string, string) ([]netip.Addr, error)
|
||||
}
|
||||
|
||||
type clientOptions struct {
|
||||
baseURL string
|
||||
resolver resolver
|
||||
dialContext func(context.Context, string, string) (net.Conn, error)
|
||||
tlsConfig *tls.Config
|
||||
allowHTTP bool
|
||||
createTemp func(string, string) (*os.File, error)
|
||||
now func() time.Time
|
||||
clientTimeout time.Duration
|
||||
}
|
||||
|
||||
func defaultClientOptions() clientOptions {
|
||||
dialer := &net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}
|
||||
return clientOptions{
|
||||
baseURL: defaultAPIBaseURL,
|
||||
resolver: net.DefaultResolver,
|
||||
dialContext: dialer.DialContext,
|
||||
tlsConfig: &tls.Config{MinVersion: tls.VersionTLS12},
|
||||
createTemp: os.CreateTemp,
|
||||
now: time.Now,
|
||||
clientTimeout: clientTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func newClient(options clientOptions) *Client {
|
||||
if options.baseURL == "" {
|
||||
options.baseURL = defaultAPIBaseURL
|
||||
}
|
||||
if options.resolver == nil {
|
||||
options.resolver = net.DefaultResolver
|
||||
}
|
||||
if options.dialContext == nil {
|
||||
dialer := &net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}
|
||||
options.dialContext = dialer.DialContext
|
||||
}
|
||||
if options.tlsConfig == nil {
|
||||
options.tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
}
|
||||
if options.createTemp == nil {
|
||||
options.createTemp = os.CreateTemp
|
||||
}
|
||||
if options.now == nil {
|
||||
options.now = time.Now
|
||||
}
|
||||
if options.clientTimeout <= 0 {
|
||||
options.clientTimeout = clientTimeout
|
||||
}
|
||||
|
||||
secureDial := publicDialer(options.resolver, options.dialContext)
|
||||
transport := httppool.NewTransport(httppool.TransportOptions{
|
||||
Proxy: nil,
|
||||
DialContext: secureDial,
|
||||
TLSClientConfig: options.tlsConfig,
|
||||
ResponseHeaderTimeout: responseHeaderTimeout,
|
||||
TraceFilter: func(request *http.Request) bool {
|
||||
return request.URL == nil || request.URL.RawQuery == ""
|
||||
},
|
||||
})
|
||||
httpClient := &http.Client{Timeout: options.clientTimeout, Transport: transport}
|
||||
httpClient.CheckRedirect = func(next *http.Request, previous []*http.Request) error {
|
||||
if len(previous) > maxRedirects {
|
||||
return errRedirectLimit
|
||||
}
|
||||
if err := validateTarget(next.Context(), next.URL, options.resolver, options.allowHTTP); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(previous) > 0 && !sameHost(previous[len(previous)-1].URL, next.URL) {
|
||||
stripCrossHostHeaders(next)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return &Client{
|
||||
httpClient: httpClient,
|
||||
baseURL: strings.TrimRight(options.baseURL, "/"),
|
||||
createTemp: options.createTemp,
|
||||
now: options.now,
|
||||
}
|
||||
}
|
||||
|
||||
func applyMetadataHeaders(request *http.Request, etag string) {
|
||||
request.Header.Set("Accept", metadataAccept)
|
||||
request.Header.Set("User-Agent", defaultUserAgent)
|
||||
request.Header.Set("X-Github-Api-Version", APIVersion)
|
||||
if etag = safeETag(etag); etag != "" {
|
||||
request.Header.Set("If-None-Match", etag)
|
||||
}
|
||||
}
|
||||
|
||||
func applyAssetHeaders(request *http.Request) {
|
||||
request.Header.Set("Accept", assetAccept)
|
||||
request.Header.Set("Accept-Encoding", "identity")
|
||||
request.Header.Set("User-Agent", defaultUserAgent)
|
||||
request.Header.Set("X-Github-Api-Version", APIVersion)
|
||||
}
|
||||
|
||||
func stripCrossHostHeaders(request *http.Request) {
|
||||
for _, header := range []string{
|
||||
"Authorization",
|
||||
"Cookie",
|
||||
"Proxy-Authorization",
|
||||
"Referer",
|
||||
"If-None-Match",
|
||||
"If-Modified-Since",
|
||||
"X-GitHub-Api-Version",
|
||||
} {
|
||||
request.Header.Del(header)
|
||||
}
|
||||
}
|
||||
|
||||
func sameHost(left *url.URL, right *url.URL) bool {
|
||||
if left == nil || right == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(left.Hostname(), right.Hostname()) && effectivePort(left) == effectivePort(right)
|
||||
}
|
||||
|
||||
func effectivePort(target *url.URL) string {
|
||||
if port := target.Port(); port != "" {
|
||||
return port
|
||||
}
|
||||
if strings.EqualFold(target.Scheme, "https") {
|
||||
return "443"
|
||||
}
|
||||
return "80"
|
||||
}
|
||||
|
||||
func validateTarget(ctx context.Context, target *url.URL, targetResolver resolver, allowHTTP bool) error {
|
||||
if target == nil || target.User != nil || target.Fragment != "" || target.Opaque != "" || target.Hostname() == "" {
|
||||
return errBlockedTarget
|
||||
}
|
||||
isHTTPS := strings.EqualFold(target.Scheme, "https")
|
||||
isAllowedHTTP := allowHTTP && strings.EqualFold(target.Scheme, "http")
|
||||
if !isHTTPS && !isAllowedHTTP {
|
||||
return errBlockedTarget
|
||||
}
|
||||
_, err := resolvePublicIPs(ctx, targetResolver, target.Hostname())
|
||||
return err
|
||||
}
|
||||
|
||||
func publicDialer(
|
||||
targetResolver resolver,
|
||||
directDial func(context.Context, string, string) (net.Conn, error),
|
||||
) func(context.Context, string, string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, errResolveTarget
|
||||
}
|
||||
addresses, err := resolvePublicIPs(ctx, targetResolver, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, resolved := range addresses {
|
||||
if !ipMatchesNetwork(resolved, network) {
|
||||
continue
|
||||
}
|
||||
connection, dialErr := directDial(ctx, network, net.JoinHostPort(resolved.String(), port))
|
||||
if dialErr == nil {
|
||||
return connection, nil
|
||||
}
|
||||
}
|
||||
return nil, errDownload
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePublicIPs(ctx context.Context, targetResolver resolver, host string) ([]netip.Addr, error) {
|
||||
if strings.Contains(host, "%") {
|
||||
return nil, errBlockedTarget
|
||||
}
|
||||
if literal, err := netip.ParseAddr(host); err == nil {
|
||||
if !isPublicIP(literal) {
|
||||
return nil, errBlockedTarget
|
||||
}
|
||||
return []netip.Addr{literal}, nil
|
||||
}
|
||||
if targetResolver == nil {
|
||||
return nil, errResolveTarget
|
||||
}
|
||||
addresses, err := targetResolver.LookupNetIP(ctx, "ip", host)
|
||||
if err != nil || len(addresses) == 0 {
|
||||
return nil, errResolveTarget
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if !isPublicIP(address) {
|
||||
return nil, errBlockedTarget
|
||||
}
|
||||
}
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
func isPublicIP(address netip.Addr) bool {
|
||||
if !address.IsValid() || address.Zone() != "" {
|
||||
return false
|
||||
}
|
||||
address = address.Unmap()
|
||||
if !address.IsGlobalUnicast() {
|
||||
return false
|
||||
}
|
||||
if address.Is6() && !publicIPv6Prefix.Contains(address) {
|
||||
return false
|
||||
}
|
||||
for _, prefix := range nonPublicPrefixes {
|
||||
if prefix.Contains(address) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ipMatchesNetwork(address netip.Addr, network string) bool {
|
||||
switch network {
|
||||
case "tcp4":
|
||||
return address.Unmap().Is4()
|
||||
case "tcp6":
|
||||
return address.Unmap().Is6()
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func responseRetryAt(response *http.Response, now time.Time) *time.Time {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
if retryAfter := strings.TrimSpace(response.Header.Get("Retry-After")); retryAfter != "" {
|
||||
if seconds, err := strconv.ParseInt(retryAfter, 10, 64); err == nil && seconds >= 0 && seconds <= maxRetryAfterSeconds {
|
||||
retryAt := now.Add(time.Duration(seconds) * time.Second)
|
||||
return &retryAt
|
||||
}
|
||||
if retryAt, err := http.ParseTime(retryAfter); err == nil {
|
||||
retryAt = retryAt.UTC()
|
||||
return &retryAt
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(response.Header.Get("X-Ratelimit-Remaining")) != "0" {
|
||||
return nil
|
||||
}
|
||||
reset, err := strconv.ParseInt(strings.TrimSpace(response.Header.Get("X-Ratelimit-Reset")), 10, 64)
|
||||
if err != nil || reset <= 0 {
|
||||
return nil
|
||||
}
|
||||
retryAt := time.Unix(reset, 0).UTC()
|
||||
return &retryAt
|
||||
}
|
||||
Reference in New Issue
Block a user