Skip to content

Commit

Permalink
🥧 api: rewrite with net/http
Browse files Browse the repository at this point in the history
  • Loading branch information
database64128 committed Dec 19, 2024
1 parent ff36250 commit 84f485c
Show file tree
Hide file tree
Showing 9 changed files with 444 additions and 251 deletions.
353 changes: 253 additions & 100 deletions api/api.go

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package api

import "testing"

func TestJoinPatternPath(t *testing.T) {
for _, c := range []struct {
elem []string
want string
}{
{[]string{}, ""},
{[]string{""}, ""},
{[]string{"a"}, "a"},
{[]string{"/"}, "/"},
{[]string{"/a"}, "/a"},
{[]string{"a/"}, "a/"},
{[]string{"/a/"}, "/a/"},
{[]string{"", "b"}, "b"},
{[]string{"", "/b"}, "/b"},
{[]string{"", "b/"}, "b/"},
{[]string{"", "/b/"}, "/b/"},
{[]string{"a", "b"}, "a/b"},
{[]string{"a", "/b"}, "a/b"},
{[]string{"a", "b/"}, "a/b/"},
{[]string{"a", "/b/"}, "a/b/"},
{[]string{"/", "b"}, "/b"},
{[]string{"/", "/b"}, "/b"},
{[]string{"/", "b/"}, "/b/"},
{[]string{"/", "/b/"}, "/b/"},
{[]string{"/a", "b"}, "/a/b"},
{[]string{"/a", "/b"}, "/a/b"},
{[]string{"/a", "b/"}, "/a/b/"},
{[]string{"/a", "/b/"}, "/a/b/"},
{[]string{"a/", "b"}, "a/b"},
{[]string{"a/", "/b"}, "a/b"},
{[]string{"a/", "b/"}, "a/b/"},
{[]string{"a/", "/b/"}, "a/b/"},
{[]string{"/a/", "b"}, "/a/b"},
{[]string{"/a/", "/b"}, "/a/b"},
{[]string{"/a/", "b/"}, "/a/b/"},
{[]string{"/a/", "/b/"}, "/a/b/"},
} {
if got := joinPatternPath(c.elem...); got != c.want {
t.Errorf("joinPatternPath(%#v) = %q; want %q", c.elem, got, c.want)
}
}
}
28 changes: 28 additions & 0 deletions api/internal/restapi/restapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package restapi

import (
"encoding/json"
"net/http"
)

// HandlerFunc is like [http.HandlerFunc], but returns a status code and an error.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) (status int, err error)

// EncodeResponse sets the Content-Type header field to application/json, and writes
// to the response writer with the given status code and data encoded as JSON.
//
// If data is nil, the status code is written and no data is encoded.
func EncodeResponse(w http.ResponseWriter, status int, data any) (int, error) {
if data == nil {
w.WriteHeader(status)
return status, nil
}
w.Header()["Content-Type"] = []string{"application/json"}
w.WriteHeader(status)
return status, json.NewEncoder(w).Encode(data)
}

// DecodeRequest decodes the request body as JSON into the provided value.
func DecodeRequest(r *http.Request, v any) error {
return json.NewDecoder(r.Body).Decode(v)
}
179 changes: 79 additions & 100 deletions api/ssm/ssm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,19 @@ package ssm

import (
"errors"
"net/http"

"github.com/database64128/shadowsocks-go"
"github.com/database64128/shadowsocks-go/api/internal/restapi"
"github.com/database64128/shadowsocks-go/cred"
"github.com/database64128/shadowsocks-go/stats"
"github.com/gofiber/fiber/v2"
)

// StandardError is the standard error response.
type StandardError struct {
Message string `json:"error"`
}

// ServerInfo contains information about the API server.
type ServerInfo struct {
Name string `json:"server"`
APIVersion string `json:"apiVersion"`
}

var serverInfo = ServerInfo{
Name: "shadowsocks-go " + shadowsocks.Version,
APIVersion: "v1",
}

// GetServerInfo returns information about the API server.
func GetServerInfo(c *fiber.Ctx) error {
return c.JSON(&serverInfo)
}

type managedServer struct {
cms *cred.ManagedServer
sc stats.Collector
Expand Down Expand Up @@ -58,131 +43,125 @@ func (sm *ServerManager) AddServer(name string, cms *cred.ManagedServer, sc stat
sm.managedServerNames = append(sm.managedServerNames, name)
}

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

server := v1.Group("/servers/:server", sm.ContextManagedServer)
server.Get("", GetServerInfo)
server.Get("/stats", sm.GetStats)
register(http.MethodGet, "/servers/{server}", sm.requireServerStats(handleGetServerInfo))
register(http.MethodGet, "/servers/{server}/stats", sm.requireServerStats(handleGetStats))

users := server.Group("/users", sm.CheckMultiUserSupport)
users.Get("", sm.ListUsers)
users.Post("", sm.AddUser)
users.Get("/:username", sm.GetUser)
users.Patch("/:username", sm.UpdateUser)
users.Delete("/:username", sm.DeleteUser)
register(http.MethodGet, "/servers/{server}/users", sm.requireServerUsers(handleListUsers))
register(http.MethodPost, "/servers/{server}/users", sm.requireServerUsers(handleAddUser))
register(http.MethodGet, "/servers/{server}/users/{username}", sm.requireServerUsers(handleGetUser))
register(http.MethodPatch, "/servers/{server}/users/{username}", sm.requireServerUsers(handleUpdateUser))
register(http.MethodDelete, "/servers/{server}/users/{username}", sm.requireServerUsers(handleDeleteUser))
}

// ListServers lists all managed servers.
func (sm *ServerManager) ListServers(c *fiber.Ctx) error {
return c.JSON(&sm.managedServerNames)
func (sm *ServerManager) handleListServers(w http.ResponseWriter, _ *http.Request) (int, error) {
return restapi.EncodeResponse(w, http.StatusOK, &sm.managedServerNames)
}

// ContextManagedServer is a middleware for the servers group.
// It adds the server with the given name to the request context.
func (sm *ServerManager) ContextManagedServer(c *fiber.Ctx) error {
name := c.Params("server")
ms := sm.managedServers[name]
if ms == nil {
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: "server not found"})
func (sm *ServerManager) requireServerStats(h func(http.ResponseWriter, *http.Request, stats.Collector) (int, error)) restapi.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) (int, error) {
name := r.PathValue("server")
ms := sm.managedServers[name]
if ms == nil {
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "server not found"})
}
return h(w, r, ms.sc)
}
c.Locals(0, ms)
return c.Next()
}

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

// GetStats returns server traffic statistics.
func (sm *ServerManager) GetStats(c *fiber.Ctx) error {
ms := managedServerFromContext(c)
if c.QueryBool("clear") {
return c.JSON(ms.sc.SnapshotAndReset())
}
return c.JSON(ms.sc.Snapshot())
func handleGetServerInfo(w http.ResponseWriter, _ *http.Request, _ stats.Collector) (int, error) {
w.Header()["Content-Type"] = []string{"application/json"}
_, err := w.Write(serverInfoJSON)
return http.StatusOK, err
}

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

// UserList contains a list of user credentials.
type UserList struct {
Users []cred.UserCredential `json:"users"`
func (sm *ServerManager) requireServerUsers(h func(http.ResponseWriter, *http.Request, *managedServer) (int, error)) func(http.ResponseWriter, *http.Request) (int, error) {
return func(w http.ResponseWriter, r *http.Request) (int, error) {
name := r.PathValue("server")
ms := sm.managedServers[name]
if ms == nil {
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "server not found"})
}
if ms.cms == nil {
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "The server does not support user management."})
}
return h(w, r, ms)
}
}

// ListUsers lists server users.
func (sm *ServerManager) ListUsers(c *fiber.Ctx) error {
ms := managedServerFromContext(c)
return c.JSON(&UserList{Users: ms.cms.Credentials()})
func handleListUsers(w http.ResponseWriter, _ *http.Request, ms *managedServer) (int, error) {
type response struct {
Users []cred.UserCredential `json:"users"`
}
return restapi.EncodeResponse(w, http.StatusOK, response{Users: ms.cms.Credentials()})
}

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

ms := managedServerFromContext(c)
if err := ms.cms.AddCredential(uc.Name, uc.UPSK); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
}
return c.JSON(&uc)
}

// UserInfo contains information about a user.
type UserInfo struct {
cred.UserCredential
stats.Traffic
return restapi.EncodeResponse(w, http.StatusOK, &uc)
}

// GetUser returns information about a user.
func (sm *ServerManager) GetUser(c *fiber.Ctx) error {
ms := managedServerFromContext(c)
username := c.Params("username")
uc, ok := ms.cms.GetCredential(username)
func handleGetUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
type response struct {
cred.UserCredential
stats.Traffic
}

username := r.PathValue("username")
userCred, ok := ms.cms.GetCredential(username)
if !ok {
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: "user not found"})
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: "user not found"})
}
return c.JSON(&UserInfo{uc, ms.sc.Snapshot().Traffic})

return restapi.EncodeResponse(w, http.StatusOK, response{userCred, ms.sc.Snapshot().Traffic})
}

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

ms := managedServerFromContext(c)
username := c.Params("username")
username := r.PathValue("username")
if err := ms.cms.UpdateCredential(username, update.UPSK); err != nil {
if errors.Is(err, cred.ErrNonexistentUser) {
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: err.Error()})
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: err.Error()})
}
return c.Status(fiber.StatusBadRequest).JSON(&StandardError{Message: err.Error()})
return restapi.EncodeResponse(w, http.StatusBadRequest, StandardError{Message: err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)

return restapi.EncodeResponse(w, http.StatusNoContent, nil)
}

// DeleteUser deletes a user's credential.
func (sm *ServerManager) DeleteUser(c *fiber.Ctx) error {
ms := managedServerFromContext(c)
username := c.Params("username")
func handleDeleteUser(w http.ResponseWriter, r *http.Request, ms *managedServer) (int, error) {
username := r.PathValue("username")
if err := ms.cms.DeleteCredential(username); err != nil {
return c.Status(fiber.StatusNotFound).JSON(&StandardError{Message: err.Error()})
return restapi.EncodeResponse(w, http.StatusNotFound, StandardError{Message: err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
return restapi.EncodeResponse(w, http.StatusNoContent, nil)
}
8 changes: 8 additions & 0 deletions conn/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ type ListenConfig struct {
fns setFuncSlice
}

// Listen wraps [tfo.ListenConfig.Listen].
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (ln net.Listener, info SocketInfo, err error) {
tlc := lc.tlc
tlc.Control = lc.fns.controlFunc(&info)
ln, err = tlc.Listen(ctx, network, address)
return
}

// ListenTCP wraps [tfo.ListenConfig.Listen] and returns a [*net.TCPListener] directly.
func (lc *ListenConfig) ListenTCP(ctx context.Context, network, address string) (tln *net.TCPListener, info SocketInfo, err error) {
tlc := lc.tlc
Expand Down
27 changes: 21 additions & 6 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,27 @@
"enableTrustedProxyCheck": false,
"trustedProxies": [],
"proxyHeader": "X-Forwarded-For",
"listen": ":20221",
"certFile": "",
"keyFile": "",
"clientCertFile": "",
"secretPath": "/4paZvyoK3dCjyQXU33md5huJMMYVD9o8",
"fiberConfigPath": ""
"staticPath": "",
"secretPath": "4paZvyoK3dCjyQXU33md5huJMMYVD9o8",
"listeners": [
{
"network": "tcp",
"address": ":20221",
"fwmark": 52140,
"trafficClass": 0,
"reusePort": false,
"fastOpen": true,
"fastOpenBacklog": 0,
"fastOpenFallback": true,
"multipath": false,
"deferAcceptSecs": 0,
"userTimeoutMsecs": 0,
"certList": "example.com",
"clientCAs": "my-root-ca",
"enableTLS": false,
"requireAndVerifyClientCert": false
}
]
},
"certs": {
"certLists": [
Expand Down
12 changes: 0 additions & 12 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ go 1.23.0
require (
github.com/database64128/netx-go v0.0.0-20241205055133-3d4b4d263f10
github.com/database64128/tfo-go/v2 v2.2.2
github.com/gofiber/contrib/fiberzap/v2 v2.1.4
github.com/gofiber/fiber/v2 v2.52.5
github.com/oschwald/geoip2-golang v1.11.0
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
Expand All @@ -16,17 +14,7 @@ require (
)

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
)
Loading

0 comments on commit 84f485c

Please sign in to comment.