diff --git a/README.md b/README.md index 0c8d810..e0bd472 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ All API responses are returned in JSON format: ``` **Important Notes:** -- For **POST** requests: All data is sent in the request body as JSON +- For all requests: Authentication token and timestamp are sent in HTTP headers (prefixed with `X-`) +- For **POST** requests: Other data is sent in the request body as JSON - For **GET** requests: All data is sent in HTTP headers (prefixed with `X-`) - All requests require authentication (except `/register` and `/login`) - Timestamps are in milliseconds (Unix timestamp) @@ -82,21 +83,28 @@ All API responses are returned in JSON format: **Method:** POST **Content-Type:** application/json -**Request:** +**Request Headers:** +``` +X-Timestamp: 1704067200000 +``` + +**Request Body:** ```json { "username": "your_username", "passwd": "YourPass123!", - "timeStamp": 1704067200000, "type": "admin" } ``` +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + | Field | Type | Required | Description | |-------|------|----------|-------------| | username | string | Yes | Username (no special characters) | | passwd | string | Yes | Password (must contain uppercase, lowercase, digit, and special character, min 8 chars) | -| timeStamp | int64 | Yes | Client timestamp in milliseconds | | type | string | No | User type: superuser, admin, visitor (default: visitor) | **Response:** @@ -120,20 +128,27 @@ All API responses are returned in JSON format: **Method:** POST **Content-Type:** application/json -**Request:** +**Request Headers:** +``` +X-Timestamp: 1704067200000 +``` + +**Request Body:** ```json { "username": "your_username", - "passwd": "YourPass123!", - "timeStamp": 1704067200000 + "passwd": "YourPass123!" } ``` +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + | Field | Type | Required | Description | |-------|------|----------|-------------| | username | string | Yes | Username | | passwd | string | Yes | Password | -| timeStamp | int64 | Yes | Client timestamp in milliseconds | **Response:** ```json @@ -158,11 +173,15 @@ All API responses are returned in JSON format: **Content-Type:** application/json **Auth Required:** Yes (token) -**Request:** +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Request Body:** ```json { - "token": "your_token", - "timeStamp": 1704067200000, "instanceInfo": { "name": "my_frpc", "serverAddr": "127.0.0.1", @@ -177,10 +196,13 @@ All API responses are returned in JSON format: } ``` +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Token | string | Yes | Authentication token | +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + | Field | Type | Required | Description | |-------|------|----------|-------------| -| token | string | Yes | Authentication token | -| timeStamp | int64 | Yes | Client timestamp in milliseconds | | instanceInfo.name | string | Yes | Instance name | | instanceInfo.serverAddr | string | Yes | frps server address | | instanceInfo.serverPort | string | Yes | frps server port | @@ -211,12 +233,16 @@ All API responses are returned in JSON format: **Content-Type:** application/json **Auth Required:** Yes (token) -**Request:** +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Request Body:** ```json { - "instanceName": "my_frpc", - "token": "your_token", - "timeStamp": 1704067200000 + "instanceName": "my_frpc" } ``` @@ -242,12 +268,16 @@ All API responses are returned in JSON format: You can modify multiple fields at once: -**Request:** +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Request Body:** ```json { "instanceName": "my_frpc", - "token": "your_token", - "timeStamp": 1704067200000, "name": "new_name", "serverAddr": "192.168.1.1", "serverPort": "7000", diff --git a/auth.go b/auth.go index cc50489..ec03a1a 100644 --- a/auth.go +++ b/auth.go @@ -6,6 +6,8 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "net/http" + "strconv" "strings" "super-frpc/postLog" "sync" @@ -175,13 +177,24 @@ func isValidPassword(password string) bool { // Validate password complexity and return hasUpper && hasLower && hasDigit && hasSpecial } -func ValidateTimeStamp(timeStamp int64) error { - if globalConfig.Debug { - return nil +func ValidateTimeStamp(header http.Header) bool { + timeStampStr := header.Get("X-Timestamp") + if timeStampStr == "" { + return false } + + timeStamp, err := strconv.ParseInt(timeStampStr, 10, 64) + if err != nil { + return false + } + + if globalConfig.Debug { + return true + } + currentTime := time.Now().UnixMilli() if currentTime-timeStamp > 3000 || timeStamp-currentTime > 3000 { - return fmt.Errorf("Timestamp out of valid range: %d", timeStamp) + return false } - return nil + return true } diff --git a/database.go b/database.go index 46e334d..e3340db 100644 --- a/database.go +++ b/database.go @@ -42,7 +42,7 @@ func isValidInput(input string) bool { return true } -func AddUser(username, passwd, userType string) (int, error) { +func AddUser(username, passwd string) (int, error) { // New user registration with default type "visitor" if !isValidInput(username) || !isValidInput(passwd) { return 0, errors.New("invalid input: contains illegal characters") } @@ -57,7 +57,7 @@ func AddUser(username, passwd, userType string) (int, error) { } result, err := db.Exec("INSERT INTO userLogin (username, passwd, type) VALUES (?, ?, ?)", - username, hashedPasswd, userType) + username, hashedPasswd, "visitor") if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { return 0, errors.New("username already exists") diff --git a/frpc.go b/frpc.go index 921e342..433cd8b 100644 --- a/frpc.go +++ b/frpc.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "super-frpc/postLog" "time" @@ -27,8 +26,6 @@ type InstanceInfo struct { } type CreateInstanceRequest struct { - Token string `json:"token"` - TimeStamp int64 `json:"timeStamp"` InstanceInfo InstanceInfo `json:"instanceInfo"` BootAtStart bool `json:"bootAtStart"` RunUser string `json:"runUser"` @@ -80,21 +77,6 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - // 处理timeStamp字段 - timeStamp := int64(0) - if ts, ok := reqMap["timeStamp"]; ok { - switch v := ts.(type) { - case float64: - timeStamp = int64(v) - case string: - if v != "" { - if parsed, err := strconv.ParseInt(v, 10, 64); err == nil { - timeStamp = parsed - } - } - } - } - // 处理bootAtStart字段 bootAtStart := false if bas, ok := reqMap["bootAtStart"]; ok { @@ -128,31 +110,22 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) { instanceInfo.Additional = additional } + // 从Header中验证token和timeStamp + userID, _, err := ValidateRequestWithHeader(w, r) + if err != nil { + postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request header: %v", err)) + SendErrorResponse(w, http.StatusUnauthorized, err.Error()) + return + } + // 构建请求结构体 req := CreateInstanceRequest{ - Token: getStringFromMap(reqMap, "token"), - TimeStamp: timeStamp, InstanceInfo: instanceInfo, BootAtStart: bootAtStart, RunUser: instanceInfo.RunUser, Additional: instanceInfo.Additional, } - // 重新序列化为JSON,用于ValidateRequestWithBody - reqBody, err := json.Marshal(req) - if err != nil { - postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to marshal request body: %v", err)) - SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") - return - } - - userID, _, err := ValidateRequestWithBody(w, r, reqBody) - if err != nil { - postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request body: %v", err)) - SendErrorResponse(w, http.StatusUnauthorized, err.Error()) - return - } - if err := CheckPermission(userID, "superuser", "admin"); err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to check permission: %v", err)) SendErrorResponse(w, http.StatusForbidden, err.Error()) @@ -251,9 +224,10 @@ func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - userID, _, err := ValidateRequestWithBody(w, r, body) + // 从Header中验证token和timeStamp + userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { - postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request body: %v", err)) + postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusUnauthorized, err.Error()) return } @@ -345,9 +319,10 @@ func ModifyInstanceHandler(w http.ResponseWriter, r *http.Request, field string) return } - userID, _, err := ValidateRequestWithBody(w, r, body) + // 从Header中验证token和timeStamp + userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { - postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request body: %v", err)) + postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusUnauthorized, err.Error()) return } diff --git a/handlers.go b/handlers.go index 01cdea4..3315acb 100644 --- a/handlers.go +++ b/handlers.go @@ -11,16 +11,13 @@ import ( ) type RegisterRequest struct { - Username string `json:"username"` - Passwd string `json:"passwd"` - TimeStamp int64 `json:"timeStamp"` - Type string `json:"type"` + Username string `json:"username"` + Passwd string `json:"passwd"` } type LoginRequest struct { - Username string `json:"username"` - Passwd string `json:"passwd"` - TimeStamp int64 `json:"timeStamp"` + Username string `json:"username"` + Passwd string `json:"passwd"` } type Response struct { @@ -36,6 +33,11 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } + if !ValidateTimeStamp(r.Header) { + SendErrorResponse(w, http.StatusBadRequest, "Invalid or missing X-Timestamp in header") + return + } + body, err := io.ReadAll(r.Body) if err != nil { SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") @@ -57,11 +59,6 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } - if err := ValidateTimeStamp(req.TimeStamp); err != nil { - SendErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - if !isValidInput(req.Username) || !isValidInput(req.Passwd) { SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters") postLog.Debug(fmt.Sprintf("[RegisterHandler] New user registration failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd)) @@ -74,23 +71,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) { return } - userType := req.Type - if userType == "" { - userType = "visitor" - } - - validTypes := map[string]bool{ - "superuser": true, - "admin": true, - "visitor": true, - } - if !validTypes[userType] { - SendErrorResponse(w, http.StatusBadRequest, "Invalid user type") - postLog.Warning(fmt.Sprintf("[RegisterHandler] New user registration failed: invalid user type \"%s\"", userType)) - return - } - - userID, err := AddUser(req.Username, req.Passwd, userType) + userID, err := AddUser(req.Username, req.Passwd) if err != nil { SendErrorResponse(w, http.StatusInternalServerError, err.Error()) postLog.Error(fmt.Sprintf("[RegisterHandler] Failed to register user \"%s\": %v", req.Username, err)) @@ -118,6 +99,11 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } + if !ValidateTimeStamp(r.Header) { + SendErrorResponse(w, http.StatusBadRequest, "Invalid or missing X-Timestamp in header") + return + } + body, err := io.ReadAll(r.Body) if err != nil { SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") @@ -139,12 +125,6 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { return } - if err := ValidateTimeStamp(req.TimeStamp); err != nil { - SendErrorResponse(w, http.StatusBadRequest, err.Error()) - postLog.Warning(fmt.Sprintf("[LoginHandler] User \"%s\" Login failed: invalid timestamp \"%d\"", req.Username, req.TimeStamp)) - return - } - if !isValidInput(req.Username) || !isValidInput(req.Passwd) { SendErrorResponse(w, http.StatusBadRequest, "Invalid input: contains illegal characters") postLog.Debug(fmt.Sprintf("[LoginHandler] Login failed: username or password contains illegal characters \"%s\":\"%s\"", req.Username, req.Passwd)) @@ -218,7 +198,7 @@ func SendSuccessResponse(w http.ResponseWriter, message string, data interface{} w.Write(jsonResp) } -func ValidateRequest(w http.ResponseWriter, r *http.Request, requiredFields ...string) (int, string, error) { +func ValidateRequest(w http.ResponseWriter, r *http.Request, requiredFields ...string) (int, string, error) { // ValidateRequest validates the request body and header body, err := io.ReadAll(r.Body) if err != nil { return 0, "", fmt.Errorf("Failed to read request body: %w", err) @@ -234,20 +214,13 @@ func ValidateRequestWithBody(w http.ResponseWriter, r *http.Request, body []byte return 0, "", fmt.Errorf("Invalid request format: %w", err) } - token, ok := reqMap["token"].(string) - if !ok || token == "" { - return 0, "", fmt.Errorf("Token is required: %s", token) + token := r.Header.Get("X-Token") + if token == "" { + return 0, "", fmt.Errorf("Token is required in header: %s", token) } - timeStamp := int64(0) - if ts, ok := reqMap["timeStamp"].(float64); ok { - timeStamp = int64(ts) - } else if !globalConfig.Debug { - return 0, "", fmt.Errorf("Timestamp is required: %d", timeStamp) - } - - if err := ValidateTimeStamp(timeStamp); err != nil { - return 0, "", fmt.Errorf("Invalid timestamp: %w", err) + if !ValidateTimeStamp(r.Header) { + return 0, "", fmt.Errorf("Invalid or missing X-Timestamp in header") } userID, err := extractUserIDFromToken(token) @@ -274,20 +247,8 @@ func ValidateRequestWithHeader(w http.ResponseWriter, r *http.Request, requiredF return 0, "", fmt.Errorf("Token is required in header: %s", token) } - timeStampStr := r.Header.Get("X-Timestamp") - timeStamp := int64(0) - if timeStampStr != "" { - var err error - timeStamp, err = strconv.ParseInt(timeStampStr, 10, 64) - if err != nil { - return 0, "", fmt.Errorf("Invalid timestamp format in header: %w", err) - } - } else if !globalConfig.Debug { - return 0, "", fmt.Errorf("Timestamp is required in header: %s", timeStampStr) - } - - if err := ValidateTimeStamp(timeStamp); err != nil { - return 0, "", err + if !ValidateTimeStamp(r.Header) { + return 0, "", fmt.Errorf("Invalid or missing X-Timestamp in header") } userID, err := extractUserIDFromToken(token) diff --git a/router.go b/router.go index 1111ac3..be18fac 100644 --- a/router.go +++ b/router.go @@ -16,27 +16,27 @@ func setupRoutes() { http.HandleFunc("/frpcAct/instanceMgr/", func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path - if len(path) < len("/frpcAct/instanceMgr/") { + if len(path) < len("/frpcAct/instanceMgr/") { // Check if path is at least as long as the base path SendErrorResponse(w, http.StatusNotFound, "invalid path") return } - remainingPath := path[len("/frpcAct/instanceMgr/"):] + remainingPath := path[len("/frpcAct/instanceMgr/"):] // Get the remaining path after the base path - if r.Method == http.MethodGet { + if r.Method == http.MethodGet { // Handle `/list` and `/list/` by GET request if remainingPath == "list" { ListInstancesHandler(w, r) return } - instanceName := strings.Trim(remainingPath, "/") + instanceName := strings.Trim(remainingPath, "/") // Get the instance name from the remaining path if instanceName != "" { ListInstancesHandler(w, r) return } } - if r.Method == http.MethodPost { + if r.Method == http.MethodPost { // Handle `/create`, `/delete`, and `/modify/` by POST request if remainingPath == "create" { CreateInstanceHandler(w, r) return @@ -57,6 +57,6 @@ func setupRoutes() { } } - SendErrorResponse(w, http.StatusNotFound, "endpoint not found") + SendErrorResponse(w, http.StatusNotFound, "endpoint not found") // Send error response if no endpoint is found }) }