mirror of
https://github.com/v2rayA/v2rayA.git
synced 2026-10-29 05:26:43 +08:00
A custom latency test URL that http.NewRequest rejects made a spawned goroutine dereference nil and exit the process; the URL is validated before any goroutine starts and the request error is handled. A Shadowsocks link whose plugin field has no ";" panicked the parser, and from the automatic subscription update that goroutine was unrecovered; the parser accepts it and each updater recovers and logs. The DNS module took the last non-bootstrap upstream as the fallback while the template emitted upstreams in rule order with deduplication, so a catch-all rule anywhere but last, or one whose upstream was deduplicated, was not the fallback. The template names the fallback in a new default_upstream field and the module honours it. PUT /routingA stored the rules without regenerating the running core. It goes through ApplyCoreConfig like the other config writers, with restore on failure. UpdateSubscription logged a failed core reload and answered success; it returns the error. PATCH /subscription joins the mutation turn. Subscription fetching retried through a direct connection when the selected proxy failed, leaking the subscription URL; it returns the error. Subscription documents are capped at 32 MiB and downloaded assets at 256 MiB. PUT /gfwList validates the downloaded file as a geosite list before replacing the working one. The login handler took its one-at-a-time semaphore before reading the body, so a client sending headers and no body blocked every login; the body is read first and the HTTP server has header and idle timeouts. Share links: a VLESS or VMess link without a numeric port is rejected at parse time instead of producing port 0; an SSR link with a password that is not base64 is rejected instead of importing empty; the SIP003 path gets its leading slash prepended, not a trailing one appended. The macOS system proxy quoted shell arguments with strconv.Quote, which leaves $(...) live; values are single-quoted. The Windows system proxy snapshot includes AutoConfigURL, which teardown deleted and never restored. Latency test responses with a non-2xx status are closed. The version response carries the docker flag the GUI's port hints read.
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/v2rayA/v2rayA/common"
|
|
)
|
|
|
|
func codeOf(t *testing.T, recorder *httptest.ResponseRecorder) (common.Code, string) {
|
|
t.Helper()
|
|
var response struct {
|
|
Code common.Code `json:"code"`
|
|
ErrorCode string `json:"errorCode"`
|
|
}
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("%v: %s", err, recorder.Body.String())
|
|
}
|
|
return response.Code, response.ErrorCode
|
|
}
|
|
|
|
func TestTouchAnswersDuringAMutation(t *testing.T) {
|
|
release, ok := beginMutation(testContext(t))
|
|
if !ok {
|
|
t.Fatal("free turn refused")
|
|
}
|
|
defer release()
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/touch", nil)
|
|
GetTouch(ctx)
|
|
if code, _ := codeOf(t, recorder); code != common.SUCCESS {
|
|
t.Fatalf("touch during a mutation answered %s", recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestMutationWaitsForTheTurn(t *testing.T) {
|
|
previous := mutationWait
|
|
mutationWait = 200 * time.Millisecond
|
|
t.Cleanup(func() { mutationWait = previous })
|
|
|
|
release, ok := beginMutation(testContext(t))
|
|
if !ok {
|
|
t.Fatal("free turn refused")
|
|
}
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
release()
|
|
}()
|
|
second, ok := beginMutation(testContext(t))
|
|
if !ok {
|
|
t.Fatal("a turn released within the wait was refused")
|
|
}
|
|
// Holding the turn past the wait answers REQUEST_IN_PROGRESS.
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/connection", nil)
|
|
if _, ok := beginMutation(ctx); ok {
|
|
t.Fatal("a held turn was granted twice")
|
|
}
|
|
if _, errorCode := codeOf(t, recorder); errorCode != "REQUEST_IN_PROGRESS" {
|
|
t.Fatalf("held turn answered %s", recorder.Body.String())
|
|
}
|
|
second()
|
|
}
|
|
|
|
func TestCustomInboundMutationsWaitForTheTurn(t *testing.T) {
|
|
previous := mutationWait
|
|
mutationWait = 10 * time.Millisecond
|
|
t.Cleanup(func() { mutationWait = previous })
|
|
|
|
release, ok := beginMutation(testContext(t))
|
|
if !ok {
|
|
t.Fatal("free turn refused")
|
|
}
|
|
defer release()
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
handler func(*gin.Context)
|
|
}{
|
|
{name: "post", method: http.MethodPost, handler: PostCustomInbound},
|
|
{name: "delete", method: http.MethodDelete, handler: DeleteCustomInbound},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(tt.method, "/customInbound", nil)
|
|
tt.handler(ctx)
|
|
if _, errorCode := codeOf(t, recorder); errorCode != "REQUEST_IN_PROGRESS" {
|
|
t.Fatalf("held turn answered %s", recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchSubscriptionWaitsForTheMutationTurn(t *testing.T) {
|
|
previous := mutationWait
|
|
mutationWait = 10 * time.Millisecond
|
|
t.Cleanup(func() { mutationWait = previous })
|
|
release, ok := beginMutation(testContext(t))
|
|
if !ok {
|
|
t.Fatal("free turn refused")
|
|
}
|
|
defer release()
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPatch, "/subscription", nil)
|
|
PatchSubscription(ctx)
|
|
if _, errorCode := codeOf(t, recorder); errorCode != "REQUEST_IN_PROGRESS" {
|
|
t.Fatalf("held turn answered %s", recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func testContext(t *testing.T) *gin.Context {
|
|
t.Helper()
|
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/connection", nil)
|
|
return ctx
|
|
}
|