refactor: remove proxy API (#1596)

This commit is contained in:
Levi authored and GitHub committed 2026-10-21 22:48:17 +08:00
1 parent 47cafb7575
commit 4214765d39
98 files changed
+5958 -1225

No files matched your search

-39
View File
@@ -1,9 +1,7 @@
package rest
import (
"io"
"net/http"
"net/url"
"runtime"
"github.com/GopeedLab/gopeed/pkg/base"
@@ -337,43 +335,6 @@ func UpdateExtension(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewNilResult())
}
func DoProxy(w http.ResponseWriter, r *http.Request) {
target := r.Header.Get("X-Target-Uri")
if target == "" {
writeError(w, "param invalid: X-Target-Uri")
return
}
targetUrl, err := url.Parse(target)
if err != nil {
writeError(w, err.Error())
return
}
r.RequestURI = ""
r.URL = targetUrl
r.Host = targetUrl.Host
r.Header.Del("Cookie")
r.Header.Del("X-Api-Token")
r.Header.Del("X-Target-Uri")
resp, err := http.DefaultClient.Do(r)
if err != nil {
writeError(w, err.Error())
return
}
defer resp.Body.Close()
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Set(k, v)
}
}
w.WriteHeader(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
writeError(w, err.Error())
return
}
w.Write(buf)
}
func GetStats(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
+9
View File
@@ -0,0 +1,9 @@
package model
type MirrorCandidatesReq struct {
URL string `json:"url"`
}
type MirrorCandidatesResp struct {
URLs []string `json:"urls"`
}
+31 -28
View File
@@ -216,7 +216,6 @@ func buildServer(startCfg *model.StartConfig, continueOnStartup bool) (*http.Ser
WriteStatusJson(w, response.StatusCode, response.Body)
})
}
r.Path("/api/web/proxy").HandlerFunc(DoProxy)
if startCfg.MCPEnable {
r.Path("/mcp").Handler(mcpserver.NewHandler(Downloader))
} else {
@@ -231,35 +230,39 @@ func buildServer(startCfg *model.StartConfig, continueOnStartup bool) (*http.Ser
if enableWebAuth {
webSessions = newWebSessionStore()
}
if startCfg.WebEnable {
if enableWebAuth {
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var loginReq model.WebAuth
if ReadJson(r, w, &loginReq) {
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
sessionID, expiresAt, err := webSessions.create()
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
http.SetCookie(w, &http.Cookie{
Name: webAuthCookieName,
Value: sessionID,
Path: "/",
MaxAge: int(webAuthSessionTTL.Seconds()),
Expires: expiresAt,
HttpOnly: true,
Secure: requestUsesHTTPS(r),
SameSite: http.SameSiteLaxMode,
})
WriteJson(w, model.NewNilResult())
if enableWebAuth {
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var loginReq model.WebAuth
if ReadJson(r, w, &loginReq) {
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
sessionID, expiresAt, err := webSessions.create()
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
http.SetCookie(w, &http.Cookie{
Name: webAuthCookieName,
Value: sessionID,
Path: "/",
MaxAge: int(webAuthSessionTTL.Seconds()),
Expires: expiresAt,
HttpOnly: true,
Secure: requestUsesHTTPS(r),
SameSite: http.SameSiteLaxMode,
})
WriteJson(w, model.NewNilResult())
return
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
}
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
}
// Reserve the API namespace before the web file handler, including unknown
// endpoints and unsupported methods on otherwise known API paths.
r.Path("/api").Handler(http.NotFoundHandler())
r.PathPrefix("/api/").Handler(http.NotFoundHandler())
if startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
@@ -325,7 +328,7 @@ func buildServer(startCfg *model.StartConfig, continueOnStartup bool) (*http.Ser
})
server := &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Accept", "Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri", "Mcp-Method", "Mcp-Name", "Mcp-Protocol-Version", "Mcp-Session-Id", "Last-Event-ID"}),
handlers.AllowedHeaders([]string{"Accept", "Content-Type", "Authorization", "X-Api-Token", "Mcp-Method", "Mcp-Name", "Mcp-Protocol-Version", "Mcp-Session-Id", "Last-Event-ID"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
handlers.ExposedHeaders([]string{"Mcp-Session-Id"}),
+44 -24
View File
@@ -2,7 +2,6 @@ package rest
import (
"bytes"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
@@ -15,6 +14,7 @@ import (
"strings"
"sync"
"testing"
"testing/fstest"
"time"
"github.com/GopeedLab/gopeed/internal/test"
@@ -669,29 +669,49 @@ func TestWebFsEnhance(t *testing.T) {
})
}
func TestDoProxy(t *testing.T) {
doTest(func() {
code, respBody := doHttpRequest0(http.MethodGet, "/api/web/proxy", map[string]string{
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
}, nil)
if code != http.StatusOK {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK)
}
want := "4ee193b676f1ebb2ad810e016350d52a"
got := fmt.Sprintf("%x", md5.Sum(respBody))
if got != want {
t.Errorf("DoProxy() got = %v, want %v", got, want)
}
})
doTest(func() {
code, _ := doHttpRequest0(http.MethodGet, "/api/web/proxy", map[string]string{
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/NOT_FOUND",
}, nil)
if code != http.StatusNotFound {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusNotFound)
}
})
func TestUnknownAPIPathsReturn404(t *testing.T) {
for _, webEnabled := range []bool{false, true} {
t.Run(fmt.Sprintf("web=%t", webEnabled), func(t *testing.T) {
doTest0(func(cfg *model.StartConfig) {
cfg.WebEnable = webEnabled
cfg.WebFS = fstest.MapFS{
"index.html": &fstest.MapFile{Data: []byte("<html>app</html>")},
"api/static.txt": &fstest.MapFile{Data: []byte("must not serve API paths as files")},
"apiary": &fstest.MapFile{Data: []byte("namespace boundary")},
}
}, func() {
paths := []string{"/api", "/api/", "/api/missing", "/api/v1/missing", "/api/web/missing", "/api/web/login", "/api/static.txt"}
for _, path := range paths {
for _, method := range []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead} {
code, body := doHttpRequest0(method, path, nil, nil)
if code != http.StatusNotFound {
t.Fatalf("%s %s status %d, want 404: %s", method, path, code, body)
}
if strings.Contains(string(body), "<html>app</html>") {
t.Fatalf("%s %s fell through to web files", method, path)
}
}
}
// Known API routes still run, while unsupported methods use the API fallback.
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/info", nil, nil)
if code != http.StatusOK {
t.Fatalf("known API status %d, want 200", code)
}
code, _ = doHttpRequest0(http.MethodPost, "/api/v1/info", nil, nil)
if code != http.StatusNotFound {
t.Fatalf("unsupported API method status %d, want 404", code)
}
if webEnabled {
for path, want := range map[string]string{"/": "<html>app</html>", "/apiary": "namespace boundary"} {
code, body := doHttpRequest0(http.MethodGet, path, map[string]string{"Accept-Encoding": "identity"}, nil)
if code != http.StatusOK || !strings.Contains(string(body), want) {
t.Fatalf("web file %s status %d body %s", path, code, body)
}
}
}
})
})
}
}
func TestTestWebhook(t *testing.T) {