feat: move native communication to FFI (#1457)

This commit is contained in:
Levi authored and GitHub committed 2026-09-05 20:18:42 +08:00
1 parent 177c225f16
commit 39cd9034cd
86 files changed
+4558 -886

No files matched your search

+95 -96
View File
@@ -643,7 +643,7 @@ func TestWebFsEnhance(t *testing.T) {
func TestDoProxy(t *testing.T) {
doTest(func() {
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
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 {
@@ -657,7 +657,7 @@ func TestDoProxy(t *testing.T) {
})
doTest(func() {
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
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 {
@@ -802,6 +802,28 @@ func TestApiToken(t *testing.T) {
}
func TestBuildServerRejectsWebAPITokenWithoutWebAuth(t *testing.T) {
cfg := (&model.StartConfig{
Storage: model.StorageMem,
WebEnable: true,
ApiToken: "123456",
}).Init()
server, listener, err := BuildServer(cfg)
if err == nil {
if listener != nil {
listener.Close()
}
if server != nil {
server.Close()
}
t.Fatal("BuildServer() should reject an API token without web authentication")
}
if server != nil || listener != nil {
t.Fatal("invalid web authentication config must fail before opening a listener")
}
}
func TestAuthorization(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
@@ -827,118 +849,74 @@ func TestAuthorization(t *testing.T) {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, loginHeaders, loginBody := doHttpRequest1(http.MethodPost, "/api/web/login", nil, cfg.WebAuth)
if status != http.StatusOK {
t.Fatalf("login status = %d, want %d", status, http.StatusOK)
}
var loginResult model.Result[any]
if err := json.Unmarshal(loginBody, &loginResult); err != nil {
t.Fatal(err)
}
if loginResult.Data != nil {
t.Fatalf("login response exposed session data: %#v", loginResult.Data)
}
loginResponse := &http.Response{Header: http.Header{"Set-Cookie": []string{loginHeaders["Set-Cookie"]}}}
cookies := loginResponse.Cookies()
if len(cookies) != 1 {
t.Fatalf("login cookies = %d, want 1", len(cookies))
}
authCookie := cookies[0]
if authCookie.Name != webAuthCookieName || authCookie.Value == "" || authCookie.Path != "/" ||
!authCookie.HttpOnly || authCookie.SameSite != http.SameSiteLaxMode || authCookie.MaxAge <= 0 {
t.Fatalf("unexpected login cookie: %#v", authCookie)
}
bearerHeaders := map[string]string{
"Authorization": "Bearer " + authCookie.Value,
}
authCookieHeader := map[string]string{
"Cookie": fmt.Sprintf("%s=%s", authCookie.Name, authCookie.Value),
}
for _, filePath := range []string{
"/fs/extensions/not_exist/icon.png",
"/fs/tasks/not_exist/file.bin",
} {
status, _ = doHttpRequest0(http.MethodGet, filePath, nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("unauthenticated %s status = %d, want %d", filePath, status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, filePath, bearerHeaders, nil)
if status != http.StatusUnauthorized {
t.Errorf("Bearer-only %s status = %d, want %d", filePath, status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, filePath, authCookieHeader, nil)
if status != http.StatusNotFound {
t.Errorf("cookie-authenticated %s status = %d, want %d", filePath, status, http.StatusNotFound)
}
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/future-resource", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("unauthenticated future /fs route status = %d, want %d", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", map[string]string{
"Cookie": webAuthCookieName + "=invalid",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("invalid-cookie file status = %d, want %d", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", bearerHeaders, nil)
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": "Bearer invalid-session",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("Bearer-authenticated API status = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": "xxx",
"Cookie": webAuthCookieName + "=invalid-session",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, loginHeaders, loginBody := doHttpRequest1(http.MethodPost, "/api/web/login", nil, cfg.WebAuth)
if status != http.StatusOK {
t.Fatalf("cookie login got = %v, want %v", status, http.StatusOK)
}
setCookie := loginHeaders["Set-Cookie"]
if !strings.Contains(setCookie, webAuthCookieName+"=") ||
!strings.Contains(setCookie, "HttpOnly") ||
!strings.Contains(setCookie, "SameSite=Lax") {
t.Fatalf("unexpected auth cookie: %q", setCookie)
}
cookieHeader := strings.SplitN(setCookie, ";", 2)[0]
sessionID := strings.TrimPrefix(cookieHeader, webAuthCookieName+"=")
if sessionID == "" || strings.Contains(string(loginBody), sessionID) {
t.Fatalf("session id must only be returned in the HttpOnly cookie")
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": "xxx",
"Cookie": cookieHeader,
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
if status != http.StatusOK {
t.Errorf("cookie authorization got = %v, want %v", status, http.StatusOK)
}
buildToken := func(username, password string, ts int64) string {
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
return token
}
fakeToken := buildToken("fake", "fake", time.Now().Unix())
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Cookie": fmt.Sprintf("%s=%s", webAuthCookieName, fakeToken),
}, nil)
status, _ = doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Cookie": fmt.Sprintf("%s=%s", webAuthCookieName, expireToken),
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
t.Errorf("unauthenticated fs request got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", map[string]string{
"Cookie": fmt.Sprintf("%s=%s", webAuthCookieName, expireToken),
"Cookie": cookieHeader,
}, nil)
if status != http.StatusNotFound {
t.Errorf("authenticated fs request got = %v, want %v", status, http.StatusNotFound)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/tasks/not_exist/file.png", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("unauthenticated task fs request got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/tasks/not_exist/file.png", map[string]string{
"Cookie": cookieHeader,
}, nil)
if status != http.StatusNotFound {
t.Errorf("authenticated task fs request got = %v, want %v", status, http.StatusNotFound)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/future-resource", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("unauthenticated future /fs route got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/fs/future-resource", map[string]string{
"Authorization": "Bearer " + sessionID,
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("expired-cookie file status = %d, want %d", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authCookieHeader, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
t.Errorf("Bearer-only future /fs route got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
@@ -947,17 +925,16 @@ func TestAuthorization(t *testing.T) {
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Cookie": fmt.Sprintf("%s=%s", authCookie.Name, authCookie.Value),
"Cookie": cookieHeader,
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
t.Errorf("cookie and API token authorization got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Cookie": fmt.Sprintf("%s=%s", authCookie.Name, authCookie.Value),
"Cookie": cookieHeader,
"X-Api-Token": "",
}, nil)
if status != http.StatusUnauthorized {
@@ -965,6 +942,28 @@ func TestAuthorization(t *testing.T) {
}
}
func TestWebSessionStoreExpiry(t *testing.T) {
now := time.Date(2026, time.August, 30, 12, 0, 0, 0, time.UTC)
store := newWebSessionStore()
store.now = func() time.Time { return now }
sessionID, expiresAt, err := store.create()
if err != nil {
t.Fatal(err)
}
if sessionID == "" || expiresAt != now.Add(webAuthSessionTTL) {
t.Fatalf("unexpected session: id=%q expiresAt=%v", sessionID, expiresAt)
}
if !store.valid(sessionID) {
t.Fatal("new session should be valid")
}
now = expiresAt
if store.valid(sessionID) {
t.Fatal("expired session should be invalid")
}
}
func TestBuildServerPropagatesWebViewProvider(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {