You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/server/handlers/proxy/proxy.go

185 lines
5.0 KiB
Go

package proxy
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/internal/settings"
"github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
"github.com/zijiren233/go-uhc"
)
var (
defaultCache *MemoryCache
fileCacheOnce sync.Once
fileCache Cache
)
// MB GB KB
func parseProxyCacheSize(sizeStr string) (int64, error) {
if sizeStr == "" {
return 0, nil
}
sizeStr = strings.ToLower(sizeStr)
sizeStr = strings.TrimSpace(sizeStr)
var multiplier int64 = 1024 * 1024 // Default MB
if strings.HasSuffix(sizeStr, "gb") {
multiplier = 1024 * 1024 * 1024
sizeStr = strings.TrimSuffix(sizeStr, "gb")
} else if strings.HasSuffix(sizeStr, "mb") {
multiplier = 1024 * 1024
sizeStr = strings.TrimSuffix(sizeStr, "mb")
} else if strings.HasSuffix(sizeStr, "kb") {
multiplier = 1024
sizeStr = strings.TrimSuffix(sizeStr, "kb")
}
size, err := strconv.ParseInt(strings.TrimSpace(sizeStr), 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid size format: %w", err)
}
return size * multiplier, nil
}
func getCache() Cache {
fileCacheOnce.Do(func() {
size, err := parseProxyCacheSize(conf.Conf.Server.ProxyCacheSize)
if err != nil {
log.Fatalf("parse proxy cache size error: %v", err)
}
if size == 0 {
size = 1024 * 1024 * 1024
}
if conf.Conf.Server.ProxyCachePath == "" {
log.Infof("proxy cache path is empty, use memory cache, size: %d", size)
defaultCache = NewMemoryCache(0, WithMaxSizeBytes(size))
return
}
log.Infof("proxy cache path: %s, size: %d", conf.Conf.Server.ProxyCachePath, size)
fileCache = NewFileCache(conf.Conf.Server.ProxyCachePath, WithFileCacheMaxSizeBytes(size))
})
if fileCache != nil {
return fileCache
}
return defaultCache
}
func ProxyURL(ctx *gin.Context, u string, headers map[string]string, cache bool) error {
if !settings.AllowProxyToLocal.Get() {
if l, err := utils.ParseURLIsLocalIP(u); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest,
model.NewApiErrorStringResp(
fmt.Sprintf("check url is local ip error: %v", err),
),
)
return fmt.Errorf("check url is local ip error: %w", err)
} else if l {
ctx.AbortWithStatusJSON(http.StatusBadRequest,
model.NewApiErrorStringResp(
"not allow proxy to local",
),
)
return errors.New("not allow proxy to local")
}
}
if cache && settings.ProxyCacheEnable.Get() {
rsc := NewHttpReadSeekCloser(u,
WithHeadersMap(headers),
WithNotSupportRange(ctx.GetHeader("Range") == ""),
)
defer rsc.Close()
return NewSliceCacheProxy(u, 1024*512, rsc, getCache()).
Proxy(ctx.Writer, ctx.Request)
}
ctx2, cf := context.WithCancel(ctx)
defer cf()
req, err := http.NewRequestWithContext(ctx2, http.MethodGet, u, nil)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest,
model.NewApiErrorStringResp(
fmt.Sprintf("new request error: %v", err),
),
)
return fmt.Errorf("new request error: %w", err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
if r := ctx.GetHeader("Range"); r != "" {
req.Header.Set("Range", r)
}
if r := ctx.GetHeader("Accept-Encoding"); r != "" {
req.Header.Set("Accept-Encoding", r)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", utils.UA)
}
cli := http.Client{
Transport: uhc.DefaultTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
req.Header.Del("Referer")
for k, v := range headers {
req.Header.Set(k, v)
}
if r := ctx.GetHeader("Range"); r != "" {
req.Header.Set("Range", r)
}
if r := ctx.GetHeader("Accept-Encoding"); r != "" {
req.Header.Set("Accept-Encoding", r)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", utils.UA)
}
return nil
},
}
resp, err := cli.Do(req)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest,
model.NewApiErrorStringResp(
fmt.Sprintf("request url error: %v", err),
),
)
return fmt.Errorf("request url error: %w", err)
}
defer resp.Body.Close()
ctx.Status(resp.StatusCode)
ctx.Header("Accept-Ranges", resp.Header.Get("Accept-Ranges"))
ctx.Header("Cache-Control", resp.Header.Get("Cache-Control"))
ctx.Header("Content-Length", resp.Header.Get("Content-Length"))
ctx.Header("Content-Range", resp.Header.Get("Content-Range"))
ctx.Header("Content-Type", resp.Header.Get("Content-Type"))
_, err = copyBuffer(ctx.Writer, resp.Body)
if err != nil && err != io.EOF {
ctx.AbortWithStatusJSON(http.StatusBadRequest,
model.NewApiErrorStringResp(
fmt.Sprintf("copy response body error: %v", err),
),
)
return fmt.Errorf("copy response body error: %w", err)
}
return nil
}
func AutoProxyURL(ctx *gin.Context, u, t string, headers map[string]string, cache bool, token, roomId, movieId string) error {
if strings.HasPrefix(t, "m3u") || utils.IsM3u8Url(u) {
return ProxyM3u8(ctx, u, headers, cache, true, token, roomId, movieId)
}
return ProxyURL(ctx, u, headers, cache)
}