mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 17:40:28 +00:00
refactor: remove proxy API (#1596)
This commit is contained in:
98 files changed
+5958
-1225
No files matched your search
+6
-1
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
pathpkg "path"
|
||||
"strings"
|
||||
)
|
||||
@@ -75,7 +76,11 @@ func (r RouteSpec) Match(rawPath string) (map[string]string, bool) {
|
||||
params := make(map[string]string)
|
||||
for index, part := range r.parts {
|
||||
if part.param != "" {
|
||||
params[part.param] = segments[index]
|
||||
value, err := url.PathUnescape(segments[index])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
params[part.param] = value
|
||||
continue
|
||||
}
|
||||
if part.literal != segments[index] {
|
||||
|
||||
@@ -2,7 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -62,6 +64,8 @@ func NewService(downloader *download.Downloader) (*Service, error) {
|
||||
newRoute(http.MethodGet, "/api/v1/tasks/{id}/status", service.getTaskStatus),
|
||||
newRoute(http.MethodGet, "/api/v1/tasks", service.getTasks),
|
||||
newRoute(http.MethodGet, "/api/v1/tasks/{id}/stats", service.getStats),
|
||||
newRoute(http.MethodPost, "/api/v1/protocols/{protocol}/jobs/{name}/trigger", service.triggerProtocolJob),
|
||||
newRoute(http.MethodPost, "/api/v1/mirrors/{provider}/candidates", service.mirrorCandidates),
|
||||
newRoute(http.MethodGet, "/api/v1/config", service.getConfig),
|
||||
newRoute(http.MethodPut, "/api/v1/config", service.putConfig),
|
||||
newRoute(http.MethodPost, "/api/v1/extensions", service.installExtension),
|
||||
@@ -294,6 +298,36 @@ func (s *Service) getStats(ctx *Context) *Response {
|
||||
return newOKResponse(stats)
|
||||
}
|
||||
|
||||
func (s *Service) triggerProtocolJob(ctx *Context) *Response {
|
||||
if err := s.Downloader.TriggerProtocolJob(context.Background(), ctx.PathParam("protocol"), ctx.PathParam("name")); err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, download.ErrUnSupportedProtocol) || errors.Is(err, download.ErrProtocolJobNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
return newStatusResponse(status, restmodel.NewErrorResult(err.Error()))
|
||||
}
|
||||
return newNilResponse()
|
||||
}
|
||||
|
||||
func (s *Service) mirrorCandidates(ctx *Context) *Response {
|
||||
var request restmodel.MirrorCandidatesReq
|
||||
if response := decodeRequest(ctx.Body, &request); response != nil {
|
||||
return response
|
||||
}
|
||||
urls, err := s.Downloader.GetMirrorCandidates(ctx.PathParam("provider"), request.URL)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, download.ErrMirrorProviderNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, download.ErrInvalidMirrorURL):
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
return newStatusResponse(status, restmodel.NewErrorResult(err.Error()))
|
||||
}
|
||||
return newOKResponse(&restmodel.MirrorCandidatesResp{URLs: urls})
|
||||
}
|
||||
|
||||
func (s *Service) getConfig(_ *Context) *Response {
|
||||
config, err := s.Downloader.GetConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -8,7 +9,10 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
httpProtocol "github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
restmodel "github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
@@ -80,3 +84,186 @@ func serviceTestRequest[T any](t *testing.T, service *Service, method, path stri
|
||||
}
|
||||
return result.Data
|
||||
}
|
||||
|
||||
func TestFullConfigDispatch(t *testing.T) {
|
||||
d := download.NewDownloader(&download.DownloaderConfig{StorageDir: t.TempDir(), TempDir: t.TempDir(), FetchManagers: []fetcher.FetcherManager{&httpProtocol.FetcherManager{}}})
|
||||
if err := d.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
service, err := NewService(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := service.Dispatch(http.MethodPut, "/api/v1/config", nil, []byte(`{"protocolConfig":{"http":{"connections":3,"useServerCtime":false}}}`), nil)
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("%+v", response)
|
||||
}
|
||||
status, body := service.JSON(http.MethodGet, "/api/v1/config", nil, nil, nil)
|
||||
var result struct {
|
||||
Data struct {
|
||||
ProtocolConfig map[string]struct {
|
||||
Connections int `json:"connections"`
|
||||
} `json:"protocolConfig"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != http.StatusOK || result.Data.ProtocolConfig["http"].Connections != 3 {
|
||||
t.Fatalf("%d %s", status, body)
|
||||
}
|
||||
if response := service.Dispatch(http.MethodPost, "/api/v1/protocols/http/jobs/missing/trigger", nil, nil, nil); response.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("%+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativePathParametersAreDecodedOnce(t *testing.T) {
|
||||
route := newRoute(http.MethodGet, "/api/v1/tasks/{id}/status", func(c *Context) *Response { return newOKResponse(c.PathParam("id")) })
|
||||
for _, test := range []struct{ path, want string }{
|
||||
{"/api/v1/tasks/%E5%BC%80%E5%8F%91%E8%80%85%40name/status", "开发者@name"},
|
||||
{"/api/v1/tasks/literal%252Fname/status", "literal%2Fname"},
|
||||
} {
|
||||
response := (&Service{routes: []RouteSpec{route}}).Dispatch(http.MethodGet, test.path, nil, nil, nil)
|
||||
raw, _ := json.Marshal(response.Body)
|
||||
var body struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &body)
|
||||
if body.Data != test.want {
|
||||
t.Fatalf("%s => %q", test.path, body.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorCandidates(t *testing.T) {
|
||||
d := download.NewDownloader(&download.DownloaderConfig{StorageDir: t.TempDir(), TempDir: t.TempDir(), FetchManagers: []fetcher.FetcherManager{&httpProtocol.FetcherManager{}}})
|
||||
if err := d.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
cfg, _ := d.GetConfig()
|
||||
cfg.GithubMirror = &base.GithubMirrorConfig{Enabled: true, Mirrors: []base.GithubMirror{
|
||||
{Type: "jsdelivr", URL: "https://cdn.example/gh"},
|
||||
{Type: "ghProxy", URL: "https://proxy.example"},
|
||||
{Type: "ghProxy", URL: "https://proxy.example"},
|
||||
}}
|
||||
if err := d.PutConfig(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service, err := NewService(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, official := range []string{
|
||||
"https://github.com/GopeedLab/gopeed/releases/download/v2.0.0/package.zip",
|
||||
"https://raw.githubusercontent.com/owner/repo/main/trackers.txt",
|
||||
} {
|
||||
request, _ := json.Marshal(map[string]string{"url": official})
|
||||
status, raw := service.JSON(http.MethodPost, "/api/v1/mirrors/github/candidates", nil, request, nil)
|
||||
var body struct {
|
||||
Data struct {
|
||||
URLs []string `json:"urls"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := 2
|
||||
if official == "https://raw.githubusercontent.com/owner/repo/main/trackers.txt" {
|
||||
want = 3
|
||||
}
|
||||
urls := body.Data.URLs
|
||||
if status != http.StatusOK || len(urls) != want || urls[0] != official || urls[len(urls)-1] != "https://proxy.example/"+official {
|
||||
t.Fatalf("%d %s", status, raw)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{"https://api.github.com/repos/GopeedLab/gopeed/releases?per_page=10", "", "https://example.com/x", "http://github.com/x", "https://github.com.evil.test/x", "https://[email protected]/x", "https://github.com:443/x", "https://github.com:/x", "https://github.com/x#fragment", "https://github.com/x#", "/relative", "https://github.com%2fevil.test/x"} {
|
||||
request, _ := json.Marshal(map[string]string{"url": invalid})
|
||||
if response := service.Dispatch(http.MethodPost, "/api/v1/mirrors/github/candidates", nil, request, nil); response.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("accepted %s: %+v", invalid, response)
|
||||
}
|
||||
}
|
||||
cfg.GithubMirror.Enabled = false
|
||||
if err := d.PutConfig(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, raw := service.JSON(http.MethodPost, "/api/v1/mirrors/github/candidates", nil, []byte(`{"url":"https://github.com/owner/repo"}`), nil)
|
||||
var body struct {
|
||||
Data struct {
|
||||
URLs []string `json:"urls"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &body); err != nil || status != http.StatusOK || len(body.Data.URLs) != 1 || body.Data.URLs[0] != "https://github.com/owner/repo" {
|
||||
t.Fatalf("%d %s", status, raw)
|
||||
}
|
||||
|
||||
for _, provider := range []string{"unknown", "GitHub"} {
|
||||
if response := service.Dispatch(http.MethodPost, "/api/v1/mirrors/"+provider+"/candidates", nil, []byte(`{"url":"https://github.com/owner/repo"}`), nil); response.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("unknown provider accepted: %+v", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type apiJobState struct {
|
||||
Runs int `json:"runs"`
|
||||
}
|
||||
type apiJobConfig struct {
|
||||
fetcher.ConfigState[apiJobState]
|
||||
Writable int `json:"writable"`
|
||||
}
|
||||
type apiStateJobManager struct{ httpProtocol.FetcherManager }
|
||||
|
||||
func (*apiStateJobManager) Name() string { return "state-api" }
|
||||
func (*apiStateJobManager) DefaultConfig() any { return &apiJobConfig{Writable: 7} }
|
||||
func (*apiStateJobManager) CronJobs(host fetcher.ProtocolHost) []fetcher.CronJob {
|
||||
return []fetcher.CronJob{{Name: "refresh", Every: time.Hour, Timeout: time.Second, Run: func(_ context.Context, source fetcher.TriggerSource) error {
|
||||
if source == fetcher.TriggerScheduled {
|
||||
return fetcher.ErrJobSkipped
|
||||
}
|
||||
return fetcher.UpdateState[apiJobConfig, apiJobState](host, func(cfg *apiJobConfig, state *apiJobState) error { cfg.Writable = 999; state.Runs++; return nil })
|
||||
}}}
|
||||
}
|
||||
|
||||
func TestFullConfigAndJobTriggerShareProtectedProtocolState(t *testing.T) {
|
||||
d := download.NewDownloader(&download.DownloaderConfig{StorageDir: t.TempDir(), TempDir: t.TempDir(), FetchManagers: []fetcher.FetcherManager{&apiStateJobManager{}}})
|
||||
if err := d.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
service, err := NewService(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
put := []byte(`{"protocolConfig":{"state-api":{"writable":9,"state":"ignore forged state"}}}`)
|
||||
if response := service.Dispatch(http.MethodPut, "/api/v1/config", nil, put, nil); response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("%+v", response)
|
||||
}
|
||||
if response := service.Dispatch(http.MethodPost, "/api/v1/protocols/state-api/jobs/refresh/trigger", nil, nil, nil); response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("%+v", response)
|
||||
}
|
||||
status, raw := service.JSON(http.MethodGet, "/api/v1/config", nil, nil, nil)
|
||||
var response struct {
|
||||
Data struct {
|
||||
ProtocolConfig map[string]apiJobConfig `json:"protocolConfig"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := response.Data.ProtocolConfig["state-api"]
|
||||
if status != http.StatusOK || cfg.Writable != 9 || cfg.State.Runs != 1 {
|
||||
t.Fatalf("%d %s", status, raw)
|
||||
}
|
||||
// Replaying an old full PUT must not undo the manual job's state change.
|
||||
if response := service.Dispatch(http.MethodPut, "/api/v1/config", nil, put, nil); response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("%+v", response)
|
||||
}
|
||||
_, raw = service.JSON(http.MethodGet, "/api/v1/config", nil, nil, nil)
|
||||
if err := json.Unmarshal(raw, &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Data.ProtocolConfig["state-api"].State.Runs != 1 {
|
||||
t.Fatalf("job state overwritten: %s", raw)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user