Skip to content

Commit 84f485c

Browse files
committed
🥧 api: rewrite with net/http
1 parent ff36250 commit 84f485c

File tree

9 files changed

+444
-251
lines changed

9 files changed

+444
-251
lines changed

api/api.go

Lines changed: 253 additions & 100 deletions
Large diffs are not rendered by default.

api/api_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package api
2+
3+
import "testing"
4+
5+
func TestJoinPatternPath(t *testing.T) {
6+
for _, c := range []struct {
7+
elem []string
8+
want string
9+
}{
10+
{[]string{}, ""},
11+
{[]string{""}, ""},
12+
{[]string{"a"}, "a"},
13+
{[]string{"/"}, "/"},
14+
{[]string{"/a"}, "/a"},
15+
{[]string{"a/"}, "a/"},
16+
{[]string{"/a/"}, "/a/"},
17+
{[]string{"", "b"}, "b"},
18+
{[]string{"", "/b"}, "/b"},
19+
{[]string{"", "b/"}, "b/"},
20+
{[]string{"", "/b/"}, "/b/"},
21+
{[]string{"a", "b"}, "a/b"},
22+
{[]string{"a", "/b"}, "a/b"},
23+
{[]string{"a", "b/"}, "a/b/"},
24+
{[]string{"a", "/b/"}, "a/b/"},
25+
{[]string{"/", "b"}, "/b"},
26+
{[]string{"/", "/b"}, "/b"},
27+
{[]string{"/", "b/"}, "/b/"},
28+
{[]string{"/", "/b/"}, "/b/"},
29+
{[]string{"/a", "b"}, "/a/b"},
30+
{[]string{"/a", "/b"}, "/a/b"},
31+
{[]string{"/a", "b/"}, "/a/b/"},
32+
{[]string{"/a", "/b/"}, "/a/b/"},
33+
{[]string{"a/", "b"}, "a/b"},
34+
{[]string{"a/", "/b"}, "a/b"},
35+
{[]string{"a/", "b/"}, "a/b/"},
36+
{[]string{"a/", "/b/"}, "a/b/"},
37+
{[]string{"/a/", "b"}, "/a/b"},
38+
{[]string{"/a/", "/b"}, "/a/b"},
39+
{[]string{"/a/", "b/"}, "/a/b/"},
40+
{[]string{"/a/", "/b/"}, "/a/b/"},
41+
} {
42+
if got := joinPatternPath(c.elem...); got != c.want {
43+
t.Errorf("joinPatternPath(%#v) = %q; want %q", c.elem, got, c.want)
44+
}
45+
}
46+
}

api/internal/restapi/restapi.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package restapi
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
)
7+
8+
// HandlerFunc is like [http.HandlerFunc], but returns a status code and an error.
9+
type HandlerFunc func(w http.ResponseWriter, r *http.Request) (status int, err error)
10+
11+
// EncodeResponse sets the Content-Type header field to application/json, and writes
12+
// to the response writer with the given status code and data encoded as JSON.
13+
//
14+
// If data is nil, the status code is written and no data is encoded.
15+
func EncodeResponse(w http.ResponseWriter, status int, data any) (int, error) {
16+
if data == nil {
17+
w.WriteHeader(status)
18+
return status, nil
19+
}
20+
w.Header()["Content-Type"] = []string{"application/json"}
21+
w.WriteHeader(status)
22+
return status, json.NewEncoder(w).Encode(data)
23+
}
24+
25+
// DecodeRequest decodes the request body as JSON into the provided value.
26+
func DecodeRequest(r *http.Request, v any) error {
27+
return json.NewDecoder(r.Body).Decode(v)
28+
}

api/ssm/ssm.go

Lines changed: 79 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,19 @@ package ssm
33

44
import (
55
"errors"
6+
"net/http"
67

78
"github.com/database64128/shadowsocks-go"
9+
"github.com/database64128/shadowsocks-go/api/internal/restapi"
810
"github.com/database64128/shadowsocks-go/cred"
911
"github.com/database64128/shadowsocks-go/stats"
10-
"github.com/gofiber/fiber/v2"
1112
)
1213

1314
// StandardError is the standard error response.
1415
type StandardError struct {
1516
Message string `json:"error"`
1617
}
1718

18-
// ServerInfo contains information about the API server.
19-
type ServerInfo struct {
20-
Name string `json:"server"`
21-
APIVersion string `json:"apiVersion"`
22-
}
23-
24-
var serverInfo = ServerInfo{
25-
Name: "shadowsocks-go " + shadowsocks.Version,
26-
APIVersion: "v1",
27-
}
28-
29-
// GetServerInfo returns information about the API server.
30-
func GetServerInfo(c *fiber.Ctx) error {
31-
return c.JSON(&serverInfo)
32-
}
33-
3419
type managedServer struct {
3520
cms *cred.ManagedServer
3621
sc stats.Collector
@@ -58,131 +43,125 @@ func (sm *ServerManager) AddServer(name string, cms *cred.ManagedServer, sc stat
5843
sm.managedServerNames = append(sm.managedServerNames, name)
5944
}
6045

61-
// RegisterRoutes sets up routes for the /servers endpoint.
62-
func (sm *ServerManager) RegisterRoutes(v1 fiber.Router) {
63-
v1.Get("/servers", sm.ListServers)
46+
// RegisterHandlers sets up handlers for the /servers endpoint.
47+
func (sm *ServerManager) RegisterHandlers(register func(method string, path string, handler restapi.HandlerFunc)) {
48+
register(http.MethodGet, "/servers", sm.handleListServers)
6449

65-
server := v1.Group("/servers/:server", sm.ContextManagedServer)
66-
server.Get("", GetServerInfo)
67-
server.Get("/stats", sm.GetStats)
50+
register(http.MethodGet, "/servers/{server}", sm.requireServerStats(handleGetServerInfo))
51+
register(http.MethodGet, "/servers/{server}/stats", sm.requireServerStats(handleGetStats))
6852

69-
users := server.Group("/users", sm.CheckMultiUserSupport)
70-
users.Get("", sm.ListUsers)
71-
users.Post("", sm.AddUser)
72-
users.Get("/:username", sm.GetUser)
73-
users.Patch("/:username", sm.UpdateUser)
74-
users.Delete("/:username", sm.DeleteUser)
53+
register(http.MethodGet, "/servers/{server}/users", sm.requireServerUsers(handleListUsers))
54+
register(http.MethodPost, "/servers/{server}/users", sm.requireServerUsers(handleAddUser))
55+
register(http.MethodGet, "/servers/{server}/users/{username}", sm.requireServerUsers(handleGetUser))
56+
register(http.MethodPatch, "/servers/{server}/users/{username}", sm.requireServerUsers(handleUpdateUser))
57+
register(http.MethodDelete, "/servers/{server}/users/{username}", sm.requireServerUsers(handleDeleteUser))
7558
}
7659

77-
// ListServers lists all managed servers.
78-
func (sm *ServerManager) ListServers(c *fiber.Ctx) error {
79-
return c.JSON(&sm.managedServerNames)
60+
func (sm *ServerManager) handleListServers(w http.ResponseWriter, _ *http.Request) (int, error) {
61+
return restapi.EncodeResponse(w, http.StatusOK, &sm.managedServerNames)
8062
}
8163

82-
// ContextManagedServer is a middleware for the servers group.
83-
// It adds the server with the given name to the request context.
84-
func (sm *ServerManager) ContextManagedServer(c *fiber.Ctx) error {
85-
name := c.Params("server")
86-
ms := sm.managedServers[name]
87-
if ms == nil {
88-
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: "server not found"})
64+
func (sm *ServerManager) requireServerStats(h func(http.ResponseWriter, *http.Request, stats.Collector) (int, error)) restapi.HandlerFunc {
65+
return func(w http.ResponseWriter, r *http.Request) (int, error) {
66+
name := r.PathValue("server")
67+
ms := sm.managedServers[name]
68+
if ms == nil {
69+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "server not found"})
70+
}
71+
return h(w, r, ms.sc)
8972
}
90-
c.Locals(0, ms)
91-
return c.Next()
9273
}
9374

94-
// managedServerFromContext returns the managed server from the request context.
95-
func managedServerFromContext(c *fiber.Ctx) *managedServer {
96-
return c.Locals(0).(*managedServer)
97-
}
75+
var serverInfoJSON = []byte(`{"server":"shadowsocks-go ` + shadowsocks.Version + `","apiVersion":"v1"}`)
9876

99-
// GetStats returns server traffic statistics.
100-
func (sm *ServerManager) GetStats(c *fiber.Ctx) error {
101-
ms := managedServerFromContext(c)
102-
if c.QueryBool("clear") {
103-
return c.JSON(ms.sc.SnapshotAndReset())
104-
}
105-
return c.JSON(ms.sc.Snapshot())
77+
func handleGetServerInfo(w http.ResponseWriter, _ *http.Request, _ stats.Collector) (int, error) {
78+
w.Header()["Content-Type"] = []string{"application/json"}
79+
_, err := w.Write(serverInfoJSON)
80+
return http.StatusOK, err
10681
}
10782

108-
// CheckMultiUserSupport is a middleware for the users group.
109-
// It checks whether the selected server supports user management.
110-
func (sm *ServerManager) CheckMultiUserSupport(c *fiber.Ctx) error {
111-
ms := managedServerFromContext(c)
112-
if ms.cms == nil {
113-
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: "The server does not support user management."})
83+
func handleGetStats(w http.ResponseWriter, r *http.Request, sc stats.Collector) (int, error) {
84+
var serverStats stats.Server
85+
if v := r.URL.Query()["clear"]; len(v) == 1 && (v[0] == "" || v[0] == "true") {
86+
serverStats = sc.SnapshotAndReset()
87+
} else {
88+
serverStats = sc.Snapshot()
11489
}
115-
return c.Next()
90+
return restapi.EncodeResponse(w, http.StatusOK, serverStats)
11691
}
11792

118-
// UserList contains a list of user credentials.
119-
type UserList struct {
120-
Users []cred.UserCredential `json:"users"`
93+
func (sm *ServerManager) requireServerUsers(h func(http.ResponseWriter, *http.Request, *managedServer) (int, error)) func(http.ResponseWriter, *http.Request) (int, error) {
94+
return func(w http.ResponseWriter, r *http.Request) (int, error) {
95+
name := r.PathValue("server")
96+
ms := sm.managedServers[name]
97+
if ms == nil {
98+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "server not found"})
99+
}
100+
if ms.cms == nil {
101+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "The server does not support user management."})
102+
}
103+
return h(w, r, ms)
104+
}
121105
}
122106

123-
// ListUsers lists server users.
124-
func (sm *ServerManager) ListUsers(c *fiber.Ctx) error {
125-
ms := managedServerFromContext(c)
126-
return c.JSON(&UserList{Users: ms.cms.Credentials()})
107+
func handleListUsers(w http.ResponseWriter, _ *http.Request, ms *managedServer) (int, error) {
108+
type response struct {
109+
Users []cred.UserCredential `json:"users"`
110+
}
111+
return restapi.EncodeResponse(w, http.StatusOK, response{Users: ms.cms.Credentials()})
127112
}
128113

129-
// AddUser adds a new user credential to the server.
130-
func (sm *ServerManager) AddUser(c *fiber.Ctx) error {
114+
func handleAddUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
131115
var uc cred.UserCredential
132-
if err := c.BodyParser(&uc); err != nil {
133-
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
116+
if err := restapi.DecodeRequest(r, &uc); err != nil {
117+
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
134118
}
135119

136-
ms := managedServerFromContext(c)
137120
if err := ms.cms.AddCredential(uc.Name, uc.UPSK); err != nil {
138-
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
121+
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
139122
}
140-
return c.JSON(&uc)
141-
}
142123

143-
// UserInfo contains information about a user.
144-
type UserInfo struct {
145-
cred.UserCredential
146-
stats.Traffic
124+
return restapi.EncodeResponse(w, http.StatusOK, &uc)
147125
}
148126

149-
// GetUser returns information about a user.
150-
func (sm *ServerManager) GetUser(c *fiber.Ctx) error {
151-
ms := managedServerFromContext(c)
152-
username := c.Params("username")
153-
uc, ok := ms.cms.GetCredential(username)
127+
func handleGetUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
128+
type response struct {
129+
cred.UserCredential
130+
stats.Traffic
131+
}
132+
133+
username := r.PathValue("username")
134+
userCred, ok := ms.cms.GetCredential(username)
154135
if !ok {
155-
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: "user not found"})
136+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "user not found"})
156137
}
157-
return c.JSON(&UserInfo{uc, ms.sc.Snapshot().Traffic})
138+
139+
return restapi.EncodeResponse(w, http.StatusOK, response{userCred, ms.sc.Snapshot().Traffic})
158140
}
159141

160-
// UpdateUser updates a user's credential.
161-
func (sm *ServerManager) UpdateUser(c *fiber.Ctx) error {
142+
func handleUpdateUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
162143
var update struct {
163144
UPSK []byte `json:"uPSK"`
164145
}
165-
if err := c.BodyParser(&update); err != nil {
166-
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
146+
if err := restapi.DecodeRequest(r, &update); err != nil {
147+
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
167148
}
168149

169-
ms := managedServerFromContext(c)
170-
username := c.Params("username")
150+
username := r.PathValue("username")
171151
if err := ms.cms.UpdateCredential(username, update.UPSK); err != nil {
172152
if errors.Is(err, cred.ErrNonexistentUser) {
173-
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: err.Error()})
153+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: err.Error()})
174154
}
175-
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
155+
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
176156
}
177-
return c.SendStatus(fiber.StatusNoContent)
157+
158+
return restapi.EncodeResponse(w, http.StatusNoContent, nil)
178159
}
179160

180-
// DeleteUser deletes a user's credential.
181-
func (sm *ServerManager) DeleteUser(c *fiber.Ctx) error {
182-
ms := managedServerFromContext(c)
183-
username := c.Params("username")
161+
func handleDeleteUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
162+
username := r.PathValue("username")
184163
if err := ms.cms.DeleteCredential(username); err != nil {
185-
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: err.Error()})
164+
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: err.Error()})
186165
}
187-
return c.SendStatus(fiber.StatusNoContent)
166+
return restapi.EncodeResponse(w, http.StatusNoContent, nil)
188167
}

conn/conn.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ type ListenConfig struct {
7070
fns setFuncSlice
7171
}
7272

73+
// Listen wraps [tfo.ListenConfig.Listen].
74+
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (ln net.Listener, info SocketInfo, err error) {
75+
tlc := lc.tlc
76+
tlc.Control = lc.fns.controlFunc(&info)
77+
ln, err = tlc.Listen(ctx, network, address)
78+
return
79+
}
80+
7381
// ListenTCP wraps [tfo.ListenConfig.Listen] and returns a [*net.TCPListener] directly.
7482
func (lc *ListenConfig) ListenTCP(ctx context.Context, network, address string) (tln *net.TCPListener, info SocketInfo, err error) {
7583
tlc := lc.tlc

docs/config.json

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -523,12 +523,27 @@
523523
"enableTrustedProxyCheck": false,
524524
"trustedProxies": [],
525525
"proxyHeader": "X-Forwarded-For",
526-
"listen": ":20221",
527-
"certFile": "",
528-
"keyFile": "",
529-
"clientCertFile": "",
530-
"secretPath": "/4paZvyoK3dCjyQXU33md5huJMMYVD9o8",
531-
"fiberConfigPath": ""
526+
"staticPath": "",
527+
"secretPath": "4paZvyoK3dCjyQXU33md5huJMMYVD9o8",
528+
"listeners": [
529+
{
530+
"network": "tcp",
531+
"address": ":20221",
532+
"fwmark": 52140,
533+
"trafficClass": 0,
534+
"reusePort": false,
535+
"fastOpen": true,
536+
"fastOpenBacklog": 0,
537+
"fastOpenFallback": true,
538+
"multipath": false,
539+
"deferAcceptSecs": 0,
540+
"userTimeoutMsecs": 0,
541+
"certList": "example.com",
542+
"clientCAs": "my-root-ca",
543+
"enableTLS": false,
544+
"requireAndVerifyClientCert": false
545+
}
546+
]
532547
},
533548
"certs": {
534549
"certLists": [

go.mod

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ go 1.23.0
55
require (
66
github.com/database64128/netx-go v0.0.0-20241205055133-3d4b4d263f10
77
github.com/database64128/tfo-go/v2 v2.2.2
8-
github.com/gofiber/contrib/fiberzap/v2 v2.1.4
9-
github.com/gofiber/fiber/v2 v2.52.5
108
github.com/oschwald/geoip2-golang v1.11.0
119
go.uber.org/zap v1.27.0
1210
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
@@ -16,17 +14,7 @@ require (
1614
)
1715

1816
require (
19-
github.com/andybalholm/brotli v1.1.0 // indirect
20-
github.com/google/uuid v1.6.0 // indirect
21-
github.com/klauspost/compress v1.17.9 // indirect
2217
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
23-
github.com/mattn/go-colorable v0.1.13 // indirect
24-
github.com/mattn/go-isatty v0.0.20 // indirect
25-
github.com/mattn/go-runewidth v0.0.16 // indirect
2618
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
27-
github.com/rivo/uniseg v0.4.7 // indirect
28-
github.com/valyala/bytebufferpool v1.0.0 // indirect
29-
github.com/valyala/fasthttp v1.55.0 // indirect
30-
github.com/valyala/tcplisten v1.0.0 // indirect
3119
go.uber.org/multierr v1.11.0 // indirect
3220
)

0 commit comments

Comments
 (0)