|
@@ -0,0 +1,111 @@
|
|
|
|
|
+package network
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "go/ast"
|
|
|
|
|
+ "go/parser"
|
|
|
|
|
+ "go/token"
|
|
|
|
|
+ "net"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "runtime"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type failingListener struct{ err error }
|
|
|
|
|
+
|
|
|
|
|
+func (l failingListener) Accept() (net.Conn, error) { return nil, l.err }
|
|
|
|
|
+func (failingListener) Close() error { return nil }
|
|
|
|
|
+func (failingListener) Addr() net.Addr { return testAddr("failing") }
|
|
|
|
|
+
|
|
|
|
|
+type testAddr string
|
|
|
|
|
+
|
|
|
|
|
+func (a testAddr) Network() string { return string(a) }
|
|
|
|
|
+func (a testAddr) String() string { return string(a) }
|
|
|
|
|
+
|
|
|
|
|
+func TestServeHTTPLogsUnexpectedListenerFailure(t *testing.T) {
|
|
|
|
|
+ errInjected := errors.New("injected listener failure")
|
|
|
|
|
+ ServeHTTP(&http.Server{}, failingListener{err: errInjected}, "Test server")
|
|
|
|
|
+
|
|
|
|
|
+ for _, line := range logger.GetLogs(100, "error") {
|
|
|
|
|
+ if strings.Contains(line, errInjected.Error()) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Fatal("unexpected listener failure was not recorded in the panel log")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestServeHTTPSuppressesNormalServerClose(t *testing.T) {
|
|
|
|
|
+ const marker = "normal-close-must-stay-silent"
|
|
|
|
|
+ ServeHTTP(&http.Server{}, failingListener{err: http.ErrServerClosed}, marker)
|
|
|
|
|
+
|
|
|
|
|
+ for _, line := range logger.GetLogs(100, "error") {
|
|
|
|
|
+ if strings.Contains(line, marker) {
|
|
|
|
|
+ t.Fatalf("normal http.ErrServerClosed was recorded as an error: %s", line)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestProductionHTTPServersUseServeHTTPWrapper(t *testing.T) {
|
|
|
|
|
+ _, currentFile, _, ok := runtime.Caller(0)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ t.Fatal("locate test source")
|
|
|
|
|
+ }
|
|
|
|
|
+ repoRoot := filepath.Clean(filepath.Join(filepath.Dir(currentFile), "../../.."))
|
|
|
|
|
+ fset := token.NewFileSet()
|
|
|
|
|
+
|
|
|
|
|
+ err := filepath.WalkDir(repoRoot, func(path string, entry os.DirEntry, walkErr error) error {
|
|
|
|
|
+ if walkErr != nil {
|
|
|
|
|
+ return walkErr
|
|
|
|
|
+ }
|
|
|
|
|
+ if entry.IsDir() {
|
|
|
|
|
+ if entry.Name() == ".git" || entry.Name() == "vendor" || entry.Name() == "node_modules" {
|
|
|
|
|
+ return filepath.SkipDir
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") || path == currentFile || path == filepath.Join(filepath.Dir(currentFile), "serve.go") {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ parsed, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ usesHTTP := false
|
|
|
|
|
+ for _, imp := range parsed.Imports {
|
|
|
|
|
+ if imp.Path.Value == `"net/http"` {
|
|
|
|
|
+ usesHTTP = true
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if !usesHTTP {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ parsed, err = parser.ParseFile(fset, path, nil, 0)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ ast.Inspect(parsed, func(node ast.Node) bool {
|
|
|
|
|
+ call, ok := node.(*ast.CallExpr)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
|
|
|
+ if ok && selector.Sel.Name == "Serve" {
|
|
|
|
|
+ position := fset.Position(call.Pos())
|
|
|
|
|
+ t.Errorf("direct Serve call at %s; production HTTP servers must use network.ServeHTTP", position)
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ })
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("scan production Go files: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|