From fa57555ec7917ac7aab5bd2309bcfec656d540d4 Mon Sep 17 00:00:00 2001 From: Ibrahim Ansari Date: Tue, 5 Dec 2023 21:06:46 +0530 Subject: [PATCH] Tackle large functions and split them up --- connector.go | 402 ++--------------- endpoints_auth.go | 443 ++++++++++--------- endpoints_files.go | 1035 ++++++++++++++++++++++---------------------- endpoints_misc.go | 382 ++++++++++++++++ main.go | 74 ++-- system/tar.go | 4 + 6 files changed, 1191 insertions(+), 1149 deletions(-) create mode 100644 endpoints_misc.go diff --git a/connector.go b/connector.go index 3bd9999..b8ff2a3 100644 --- a/connector.go +++ b/connector.go @@ -2,23 +2,19 @@ package main import ( "bufio" - "bytes" "encoding/json" "fmt" "io" "log" "net/http" - "os" "path/filepath" "strings" "sync" - "time" "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/puzpuzpuz/xsync/v2" "github.com/retrixe/octyne/auth" - "github.com/retrixe/octyne/system" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" @@ -93,6 +89,14 @@ func GetIP(r *http.Request) string { return r.RemoteAddr[:index] } +// WrapEndpointWithCtx provides Connector instances to HTTP endpoint handler functions. +func WrapEndpointWithCtx( + connector *Connector, + endpoint func(connector *Connector, w http.ResponseWriter, r *http.Request), +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { endpoint(connector, w, r) } +} + // InitializeConnector initializes a connector to create an HTTP API for interaction. func InitializeConnector(config *Config) *Connector { // Create an authenticator. @@ -144,9 +148,26 @@ func InitializeConnector(config *Config) *Connector { POST /server/{id}/compress?path=path&compress=true/false (compress is optional, default: true) POST /server/{id}/decompress?path=path */ - connector.registerMiscRoutes() - connector.registerAuthRoutes() - connector.registerFileRoutes() + + connector.Handle("/login", WrapEndpointWithCtx(connector, loginEndpoint)) + connector.Handle("/logout", WrapEndpointWithCtx(connector, logoutEndpoint)) + connector.Handle("/ott", WrapEndpointWithCtx(connector, ottEndpoint)) + connector.Handle("/accounts", WrapEndpointWithCtx(connector, accountsEndpoint)) + + connector.HandleFunc("/", rootEndpoint) + connector.Handle("/config", WrapEndpointWithCtx(connector, configEndpoint)) + connector.Handle("/config/reload", WrapEndpointWithCtx(connector, configReloadEndpoint)) + connector.Handle("/servers", WrapEndpointWithCtx(connector, serversEndpoint)) + connector.Handle("/server/{id}", WrapEndpointWithCtx(connector, serverEndpoint)) + connector.Upgrader.CheckOrigin = func(r *http.Request) bool { return true } + connector.Handle("/server/{id}/console", WrapEndpointWithCtx(connector, consoleEndpoint)) + + connector.Handle("/server/{id}/files", WrapEndpointWithCtx(connector, filesEndpoint)) + connector.Handle("/server/{id}/file", WrapEndpointWithCtx(connector, fileEndpoint)) + connector.Handle("/server/{id}/folder", WrapEndpointWithCtx(connector, folderEndpoint)) + connector.Handle("/server/{id}/compress", WrapEndpointWithCtx(connector, compressionEndpoint)) + connector.Handle("/server/{id}/compress/v2", WrapEndpointWithCtx(connector, compressionEndpoint)) + connector.Handle("/server/{id}/decompress", WrapEndpointWithCtx(connector, decompressionEndpoint)) return connector } @@ -211,373 +232,6 @@ func writeJsonStructRes(w http.ResponseWriter, resp interface{}) error { return json.NewEncoder(w).Encode(resp) } -// skipcq GO-R1005 -func (connector *Connector) registerMiscRoutes() { - // GET / - connector.Router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - writeJsonStringRes(w, "{\"version\": \""+OctyneVersion+"\"}") - }) - - // GET /config - // PATCH /config - connector.Router.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { - user := connector.Validate(w, r) - if user == "" { - return - } else if r.Method != "GET" && r.Method != "PATCH" { - httpError(w, "Only GET and PATCH are allowed!", http.StatusMethodNotAllowed) - return - } - if r.Method == "GET" { - contents, err := os.ReadFile(ConfigJsonPath) - if err != nil { - log.Println("Error reading "+ConfigJsonPath+" when user accessed /config!", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - connector.Info("config.view", "ip", GetIP(r), "user", user) - writeJsonStringRes(w, string(contents)) - } else if r.Method == "PATCH" { - var buffer bytes.Buffer - _, err := buffer.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - var origJson = buffer.String() - var config Config - contents, err := StripLineCommentsFromJSON(buffer.Bytes()) - if err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) - return - } - err = json.Unmarshal(contents, &config) - if err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) - return - } - err = os.WriteFile(ConfigJsonPath+"~", []byte(strings.TrimRight(origJson, "\n")+"\n"), 0666) - if err != nil { - log.Println("Error writing to " + ConfigJsonPath + " when user modified config!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - err = os.Rename(ConfigJsonPath+"~", ConfigJsonPath) - if err != nil { - log.Println("Error writing to " + ConfigJsonPath + " when user modified config!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - connector.UpdateConfig(&config) - connector.Info("config.edit", "ip", GetIP(r), "user", user, "newConfig", config) - writeJsonStringRes(w, "{\"success\":true}") - info.Println("Config updated remotely by user over HTTP API (see action logs for info)!") - } - }) - - // GET /config/reload - connector.Router.HandleFunc("/config/reload", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - // Read the new config. - var config Config - contents, err := os.ReadFile(ConfigJsonPath) - if err != nil { - log.Println("An error occurred while attempting to read config! " + err.Error()) - httpError(w, "An error occurred while reading config!", http.StatusInternalServerError) - return - } - contents, err = StripLineCommentsFromJSON(contents) - if err != nil { - log.Println("An error occurred while attempting to read config! " + err.Error()) - httpError(w, "An error occurred while reading config!", http.StatusInternalServerError) - return - } - err = json.Unmarshal(contents, &config) - if err != nil { - log.Println("An error occurred while attempting to parse config! " + err.Error()) - httpError(w, "An error occurred while parsing config!", http.StatusInternalServerError) - return - } - // Reload the config. - connector.UpdateConfig(&config) - // Send the response. - connector.Info("config.reload", "ip", GetIP(r), "user", user) - writeJsonStringRes(w, "{\"success\":true}") - info.Println("Config reloaded successfully!") - }) - - // GET /servers - type serversResponse struct { - Servers map[string]interface{} `json:"servers"` - } - connector.Router.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - if connector.Validate(w, r) == "" { - return - } - // Get a map of processes and their online status. - processes := make(map[string]interface{}) - connector.Processes.Range(func(k string, v *managedProcess) bool { - if r.URL.Query().Get("extrainfo") == "true" { - processes[v.Name] = map[string]interface{}{ - "status": v.Online.Load(), - "toDelete": v.ToDelete.Load(), - } - } else { - processes[v.Name] = v.Online.Load() - } - return true - }) - // Send the list. - writeJsonStructRes(w, serversResponse{Servers: processes}) // skipcq GSC-G104 - }) - - // GET /server/{id} - // POST /server/{id} - type serverResponse struct { - Status int `json:"status"` - CPUUsage float64 `json:"cpuUsage"` - MemoryUsage float64 `json:"memoryUsage"` - TotalMemory int64 `json:"totalMemory"` - Uptime int64 `json:"uptime"` - ToDelete bool `json:"toDelete,omitempty"` - } - totalMemory := int64(system.GetTotalSystemMemory()) - connector.Router.HandleFunc("/server/{id}", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, err := connector.Processes.Load(id) - // In case the process doesn't exist. - if !err { - httpError(w, "This server does not exist!", http.StatusNotFound) - return - } - // POST /server/{id} - if r.Method == "POST" { - // Get the request body to check whether the operation is to START or STOP. - var body bytes.Buffer - _, err := body.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - operation := strings.ToUpper(body.String()) - // Check whether the operation is correct or not. - if operation == "START" { - // Start process if required. - if process.Online.Load() != 1 { - err = process.StartProcess(connector) - connector.Info("server.start", "ip", GetIP(r), "user", user, "server", id) - } - // Send a response. - res := make(map[string]bool) - res["success"] = err == nil - writeJsonStructRes(w, res) // skipcq GSC-G104 - } else if operation == "STOP" || operation == "KILL" || operation == "TERM" { - // Stop process if required. - if process.Online.Load() == 1 { - // Octyne 2.x should drop STOP or move it to SIGTERM. - if operation == "KILL" || operation == "STOP" { - process.KillProcess() - connector.Info("server.kill", "ip", GetIP(r), "user", user, "server", id) - } else { - process.StopProcess() - connector.Info("server.stop", "ip", GetIP(r), "user", user, "server", id) - } - } - // Send a response. - res := make(map[string]bool) - res["success"] = true - writeJsonStructRes(w, res) // skipcq GSC-G104 - } else { - httpError(w, "Invalid operation requested!", http.StatusBadRequest) - return - } - // GET /server/{id} - } else if r.Method == "GET" { - // Get the PID of the process. - var stat system.ProcessStats - process.CommandMutex.RLock() - defer process.CommandMutex.RUnlock() - if process.Command != nil && - process.Command.Process != nil && - // Command.ProcessState == nil && // ProcessState isn't mutexed, the next if should suffice - process.Online.Load() == 1 { - // Get CPU usage and memory usage of the process. - var err error - stat, err = system.GetProcessStats(process.Command.Process.Pid) - if err != nil { - log.Println("Failed to get server statistics for "+process.Name+"! Is ps available?", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - } - - // Send a response. - uptime := process.Uptime.Load() - if uptime > 0 { - uptime = time.Now().UnixNano() - uptime - } - res := serverResponse{ - Status: int(process.Online.Load()), - Uptime: uptime, - CPUUsage: stat.CPUUsage, - MemoryUsage: stat.RSSMemory, - TotalMemory: totalMemory, - ToDelete: process.ToDelete.Load(), - } - writeJsonStructRes(w, res) // skipcq GSC-G104 - } else { - httpError(w, "Only GET and POST is allowed!", http.StatusMethodNotAllowed) - } - }) - - // WS /server/{id}/console - connector.Upgrader.CheckOrigin = func(r *http.Request) bool { return true } - connector.Router.HandleFunc("/server/{id}/console", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - ticket, ticketExists := connector.Tickets.LoadAndDelete(r.URL.Query().Get("ticket")) - user := "" - if ticketExists && ticket.IPAddr == GetIP(r) { - user = ticket.User - } else if user = connector.Validate(w, r); user == "" { - return - } - // Retrieve the token. - token := auth.GetTokenFromRequest(r) - if ticketExists { - token = ticket.Token - } - // Get the server being accessed. - id := mux.Vars(r)["id"] - process, exists := connector.Processes.Load(id) - // In case the server doesn't exist. - if !exists { - httpError(w, "This server does not exist!", http.StatusNotFound) - return - } - // Upgrade WebSocket connection. - c, err := connector.Upgrade(w, r, nil) - v2 := c.Subprotocol() == "console-v2" - if err == nil { - connector.Info("server.console.access", "ip", GetIP(r), "user", user, "server", id) - defer c.Close() - // Setup WebSocket limits. - timeout := 30 * time.Second - c.SetReadLimit(1024 * 1024) // Limit WebSocket reads to 1 MB. - // If v2, send settings and set read deadline. - if v2 { - c.SetReadDeadline(time.Now().Add(timeout)) - c.WriteJSON(struct { // skipcq GSC-G104 - Type string `json:"type"` - }{"settings"}) - } - // Use a channel to synchronise all writes to the WebSocket. - writeChannel := make(chan interface{}, 8) - defer close(writeChannel) - go (func() { - for { - data, ok := <-writeChannel - if !ok { - break - } else if data == nil { - c.Close() - break - } else if _, ok := connector.Authenticator.GetUsers().Load(user); !ok && r.RemoteAddr != "@" { - c.Close() - break - } - c.SetWriteDeadline(time.Now().Add(timeout)) // Set write deadline esp for v1 connections. - str, ok := data.(string) - if ok && v2 { - json, err := json.Marshal(struct { - Type string `json:"type"` - Data string `json:"data"` - }{"output", str}) - if err != nil { - log.Println("Error in "+process.Name+" console!", err) - } else { - c.WriteMessage(websocket.TextMessage, json) // skipcq GSC-G104 - } - } else if ok { - c.WriteMessage(websocket.TextMessage, []byte(str)) // skipcq GSC-G104 - } else { - c.WriteMessage(websocket.TextMessage, data.([]byte)) // skipcq GSC-G104 - } - } - })() - // Add connection to the process after sending current console output. - (func() { - process.ConsoleLock.RLock() - defer process.ConsoleLock.RUnlock() - writeChannel <- process.Console - process.Clients.Store(token, writeChannel) - })() - // Read messages from the user and execute them. - for { - client, _ := process.Clients.Load(token) - // Another client has connected with the same token. Terminate existing connection. - if client != writeChannel { - break - } - // Read messages from the user. - _, message, err := c.ReadMessage() - if err != nil { - process.Clients.Delete(token) - break // The WebSocket connection has terminated. - } else if _, ok := connector.Authenticator.GetUsers().Load(user); !ok && r.RemoteAddr != "@" { - process.Clients.Delete(token) - c.Close() - break - } - if v2 { - c.SetReadDeadline(time.Now().Add(timeout)) // Update read deadline. - var data map[string]string - err := json.Unmarshal(message, &data) - if err == nil { - if data["type"] == "input" && data["data"] != "" { - connector.Info("server.console.input", "ip", GetIP(r), "user", user, "server", id, - "input", data["data"]) - process.SendCommand(data["data"]) - } else if data["type"] == "ping" { - json, _ := json.Marshal(struct { // skipcq GSC-G104 - Type string `json:"type"` - ID string `json:"id"` - }{"pong", data["id"]}) - writeChannel <- json - } else { - json, _ := json.Marshal(struct { // skipcq GSC-G104 - Type string `json:"type"` - Message string `json:"message"` - }{"error", "Invalid message type: " + data["type"]}) - writeChannel <- json - } - } else { - json, _ := json.Marshal(struct { // skipcq GSC-G104 - Type string `json:"type"` - Message string `json:"message"` - }{"error", "Invalid message format"}) - writeChannel <- json - } - } else { - connector.Info("server.console.input", "ip", GetIP(r), "user", user, "server", id, - "input", string(message)) - process.SendCommand(string(message)) - } - } - } - }) -} - // UpdateConfig updates the connector with the new Config passed in arguments. func (connector *Connector) UpdateConfig(config *Config) { // Update logged actions. diff --git a/endpoints_auth.go b/endpoints_auth.go index 936af66..10e52f3 100644 --- a/endpoints_auth.go +++ b/endpoints_auth.go @@ -15,256 +15,255 @@ import ( "github.com/retrixe/octyne/auth" ) -// skipcq GO-R1005 -func (connector *Connector) registerAuthRoutes() { - // GET /login - type loginResponse struct { - Token string `json:"token"` - } - connector.Router.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - if r.RemoteAddr == "@" { - httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) - return - } - // In case the username and password headers don't exist. - username := r.Header.Get("Username") - password := r.Header.Get("Password") - if username == "" || password == "" { - httpError(w, "Username or password not provided!", http.StatusBadRequest) - return - } - // Authorize the user. - token, err := connector.Login(username, password) - if err != nil { - log.Println("An error occurred when logging user in!", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } else if token == "" { - httpError(w, "Invalid username or password!", http.StatusUnauthorized) - return - } - connector.Info("auth.login", "ip", GetIP(r), "user", username) - // Set the authentication cookie, if requested. - if r.URL.Query().Get("cookie") == "true" { - http.SetCookie(w, &http.Cookie{ - Name: "X-Authentication", - Value: token, - MaxAge: 60 * 60 * 24 * 31 * 3, // 3 months - // Allows HTTP usage. Strict SameSite will block sending cookie over HTTP when using HTTPS: - // https://web.dev/same-site-same-origin/ - Secure: false, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - }) - writeJsonStringRes(w, "{\"success\":true}") - return - } - // Send the response. - writeJsonStructRes(w, loginResponse{Token: token}) // skipcq GSC-G104 - }) +// GET /login +type loginEndpointResponse struct { + Token string `json:"token"` +} - // GET /logout - connector.Router.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { - if r.RemoteAddr == "@" { - httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) - return - } - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - token := auth.GetTokenFromRequest(r) - // Authorize the user. - success, err := connector.Logout(token) - if err != nil { - log.Println("An error occurred when logging out user!", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } else if !success { - httpError(w, "Invalid token, failed to logout!", http.StatusUnauthorized) - return - } - // Unset the authentication cookie. +func loginEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + if r.RemoteAddr == "@" { + httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) + return + } + // In case the username and password headers don't exist. + username := r.Header.Get("Username") + password := r.Header.Get("Password") + if username == "" || password == "" { + httpError(w, "Username or password not provided!", http.StatusBadRequest) + return + } + // Authorize the user. + token, err := connector.Login(username, password) + if err != nil { + log.Println("An error occurred when logging user in!", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } else if token == "" { + httpError(w, "Invalid username or password!", http.StatusUnauthorized) + return + } + connector.Info("auth.login", "ip", GetIP(r), "user", username) + // Set the authentication cookie, if requested. + if r.URL.Query().Get("cookie") == "true" { http.SetCookie(w, &http.Cookie{ - Name: "X-Authentication", - Value: "", - MaxAge: -1, + Name: "X-Authentication", + Value: token, + MaxAge: 60 * 60 * 24 * 31 * 3, // 3 months + // Allows HTTP usage. Strict SameSite will block sending cookie over HTTP when using HTTPS: + // https://web.dev/same-site-same-origin/ Secure: false, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) - // Send the response. - connector.Info("auth.logout", "ip", GetIP(r), "user", user) writeJsonStringRes(w, "{\"success\":true}") + return + } + // Send the response. + writeJsonStructRes(w, loginEndpointResponse{Token: token}) // skipcq GSC-G104 +} + +// GET /logout +func logoutEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + if r.RemoteAddr == "@" { + httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) + return + } + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + token := auth.GetTokenFromRequest(r) + // Authorize the user. + success, err := connector.Logout(token) + if err != nil { + log.Println("An error occurred when logging out user!", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } else if !success { + httpError(w, "Invalid token, failed to logout!", http.StatusUnauthorized) + return + } + // Unset the authentication cookie. + http.SetCookie(w, &http.Cookie{ + Name: "X-Authentication", + Value: "", + MaxAge: -1, + Secure: false, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, }) + // Send the response. + connector.Info("auth.logout", "ip", GetIP(r), "user", user) + writeJsonStringRes(w, "{\"success\":true}") +} - // GET /ott - connector.Router.HandleFunc("/ott", func(w http.ResponseWriter, r *http.Request) { - if r.RemoteAddr == "@" { - httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) - return - } - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - token := auth.GetTokenFromRequest(r) - // Add a ticket. - ticket := make([]byte, 4) - rand.Read(ticket) // Tolerate errors here, an error here is incredibly unlikely: skipcq GSC-G104 - ticketString := base64.StdEncoding.EncodeToString(ticket) - connector.Tickets.Store(ticketString, Ticket{ - Time: time.Now().Unix(), - User: user, - Token: token, - IPAddr: GetIP(r), - }) - // Schedule deletion (cancellable). - go (func() { - <-time.After(2 * time.Minute) - connector.Tickets.Delete(ticketString) - })() - // Send the response. - writeJsonStringRes(w, "{\"ticket\": \""+ticketString+"\"}") +// GET /ott +func ottEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + if r.RemoteAddr == "@" { + httpError(w, "Auth endpoints cannot be called over Unix socket!", http.StatusBadRequest) + return + } + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + token := auth.GetTokenFromRequest(r) + // Add a ticket. + ticket := make([]byte, 4) + rand.Read(ticket) // Tolerate errors here, an error here is incredibly unlikely: skipcq GSC-G104 + ticketString := base64.StdEncoding.EncodeToString(ticket) + connector.Tickets.Store(ticketString, Ticket{ + Time: time.Now().Unix(), + User: user, + Token: token, + IPAddr: GetIP(r), }) + // Schedule deletion (cancellable). + go (func() { + <-time.After(2 * time.Minute) + connector.Tickets.Delete(ticketString) + })() + // Send the response. + writeJsonStringRes(w, "{\"ticket\": \""+ticketString+"\"}") +} + +// GET /accounts +// POST /accounts +// PATCH /accounts?username=username +// DELETE /accounts?username=username +type accountsRequestBody struct { + Username string `json:"username"` + Password string `json:"password"` +} - // GET /accounts - // POST /accounts - // PATCH /accounts?username=username - // DELETE /accounts?username=username - type accountsRequestBody struct { - Username string `json:"username"` - Password string `json:"password"` +func accountsEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + user := connector.Validate(w, r) + if user == "" { + return + } else if r.Method != "GET" && r.Method != "POST" && r.Method != "PATCH" && r.Method != "DELETE" { + httpError(w, "Only GET, POST, PATCH and DELETE are allowed!", http.StatusMethodNotAllowed) + return } - connector.Router.HandleFunc("/accounts", func(w http.ResponseWriter, r *http.Request) { - user := connector.Validate(w, r) - if user == "" { - return - } else if r.Method != "GET" && r.Method != "POST" && r.Method != "PATCH" && r.Method != "DELETE" { - httpError(w, "Only GET, POST, PATCH and DELETE are allowed!", http.StatusMethodNotAllowed) - return + var users map[string]string + contents, err := os.ReadFile(UsersJsonPath) + if err != nil { + log.Println("Error reading "+UsersJsonPath+" when modifying accounts!", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + err = json.Unmarshal(contents, &users) + if err != nil { + log.Println("Error parsing "+UsersJsonPath+" when modifying accounts!", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + if r.Method == "GET" { + var usernames []string + for username := range users { + usernames = append(usernames, username) } - var users map[string]string - contents, err := os.ReadFile(UsersJsonPath) + usernamesJson, err := json.Marshal(usernames) if err != nil { - log.Println("Error reading "+UsersJsonPath+" when modifying accounts!", err) + log.Println("Error serialising usernames when listing accounts!") httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - err = json.Unmarshal(contents, &users) + writeJsonStringRes(w, string(usernamesJson)) + return + } else if r.Method == "POST" { + var buffer bytes.Buffer + _, err := buffer.ReadFrom(r.Body) if err != nil { - log.Println("Error parsing "+UsersJsonPath+" when modifying accounts!", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) + httpError(w, "Failed to read body!", http.StatusBadRequest) return } - if r.Method == "GET" { - var usernames []string - for username := range users { - usernames = append(usernames, username) - } - usernamesJson, err := json.Marshal(usernames) - if err != nil { - log.Println("Error serialising usernames when listing accounts!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - writeJsonStringRes(w, string(usernamesJson)) + var body accountsRequestBody + err = json.Unmarshal(buffer.Bytes(), &body) + if err != nil { + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } else if body.Username == "" || body.Password == "" { + httpError(w, "Username or password not provided!", http.StatusBadRequest) + return + } else if users[body.Username] != "" { + httpError(w, "User already exists!", http.StatusConflict) return - } else if r.Method == "POST" { - var buffer bytes.Buffer - _, err := buffer.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - var body accountsRequestBody - err = json.Unmarshal(buffer.Bytes(), &body) - if err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) - return - } else if body.Username == "" || body.Password == "" { - httpError(w, "Username or password not provided!", http.StatusBadRequest) - return - } else if users[body.Username] != "" { - httpError(w, "User already exists!", http.StatusConflict) - return - } - sha256sum := fmt.Sprintf("%x", sha256.Sum256([]byte(body.Password))) - connector.Info("accounts.create", "ip", GetIP(r), "user", user, "newUser", body.Username) - users[body.Username] = sha256sum - } else if r.Method == "PATCH" { - username := r.URL.Query().Get("username") - var buffer bytes.Buffer - _, err := buffer.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - var body accountsRequestBody - err = json.Unmarshal(buffer.Bytes(), &body) - if username == "" { // Legacy compat with older API, assume body.Username, fix in 2.0 - username = body.Username - } - toUpdateUsername := body.Username != username && body.Username != "" - if err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) - return - } else if username == "" || (body.Password == "" && !toUpdateUsername) { - httpError(w, "Username or password not provided!", http.StatusBadRequest) - return - } else if users[username] == "" { - httpError(w, "User does not exist!", http.StatusNotFound) - return - } else if toUpdateUsername && users[body.Username] != "" { - httpError(w, "User already exists!", http.StatusConflict) - return - } - sha256sum := users[username] - if body.Password != "" { - sha256sum = fmt.Sprintf("%x", sha256.Sum256([]byte(body.Password))) - } - if toUpdateUsername { - connector.Info("accounts.update", "ip", GetIP(r), "user", user, - "updatedUser", body.Username, "oldUsername", username, "changedPassword", body.Password != "") - delete(users, username) - users[body.Username] = sha256sum - } else { - connector.Info("accounts.update", "ip", GetIP(r), "user", user, - "updatedUser", username, "changedPassword", true) - users[username] = sha256sum - } - } else if r.Method == "DELETE" { - username := r.URL.Query().Get("username") - if username == "" { - httpError(w, "Username not provided!", http.StatusBadRequest) - return - } else if users[username] == "" { - httpError(w, "User does not exist!", http.StatusNotFound) - return - } - connector.Info("accounts.delete", "ip", GetIP(r), "user", user, "deletedUser", username) - delete(users, username) } - usersJson, err := json.MarshalIndent(users, "", " ") + sha256sum := fmt.Sprintf("%x", sha256.Sum256([]byte(body.Password))) + connector.Info("accounts.create", "ip", GetIP(r), "user", user, "newUser", body.Username) + users[body.Username] = sha256sum + } else if r.Method == "PATCH" { + username := r.URL.Query().Get("username") + var buffer bytes.Buffer + _, err := buffer.ReadFrom(r.Body) if err != nil { - log.Println("Error serialising " + UsersJsonPath + " when modifying accounts!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) + httpError(w, "Failed to read body!", http.StatusBadRequest) return } - err = os.WriteFile(UsersJsonPath+"~", []byte(string(usersJson)+"\n"), 0666) + var body accountsRequestBody + err = json.Unmarshal(buffer.Bytes(), &body) + if username == "" { // Legacy compat with older API, assume body.Username, fix in 2.0 + username = body.Username + } + toUpdateUsername := body.Username != username && body.Username != "" if err != nil { - log.Println("Error writing to " + UsersJsonPath + " when modifying accounts!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } else if username == "" || (body.Password == "" && !toUpdateUsername) { + httpError(w, "Username or password not provided!", http.StatusBadRequest) + return + } else if users[username] == "" { + httpError(w, "User does not exist!", http.StatusNotFound) + return + } else if toUpdateUsername && users[body.Username] != "" { + httpError(w, "User already exists!", http.StatusConflict) return } - err = os.Rename(UsersJsonPath+"~", UsersJsonPath) - if err != nil { - log.Println("Error writing to " + UsersJsonPath + " when modifying accounts!") - httpError(w, "Internal Server Error!", http.StatusInternalServerError) + sha256sum := users[username] + if body.Password != "" { + sha256sum = fmt.Sprintf("%x", sha256.Sum256([]byte(body.Password))) + } + if toUpdateUsername { + connector.Info("accounts.update", "ip", GetIP(r), "user", user, + "updatedUser", body.Username, "oldUsername", username, "changedPassword", body.Password != "") + delete(users, username) + users[body.Username] = sha256sum + } else { + connector.Info("accounts.update", "ip", GetIP(r), "user", user, + "updatedUser", username, "changedPassword", true) + users[username] = sha256sum + } + } else if r.Method == "DELETE" { + username := r.URL.Query().Get("username") + if username == "" { + httpError(w, "Username not provided!", http.StatusBadRequest) + return + } else if users[username] == "" { + httpError(w, "User does not exist!", http.StatusNotFound) return } - writeJsonStringRes(w, "{\"success\":true}") - }) + connector.Info("accounts.delete", "ip", GetIP(r), "user", user, "deletedUser", username) + delete(users, username) + } + usersJson, err := json.MarshalIndent(users, "", " ") + if err != nil { + log.Println("Error serialising " + UsersJsonPath + " when modifying accounts!") + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + err = os.WriteFile(UsersJsonPath+"~", []byte(string(usersJson)+"\n"), 0666) + if err != nil { + log.Println("Error writing to " + UsersJsonPath + " when modifying accounts!") + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + err = os.Rename(UsersJsonPath+"~", UsersJsonPath) + if err != nil { + log.Println("Error writing to " + UsersJsonPath + " when modifying accounts!") + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + writeJsonStringRes(w, "{\"success\":true}") } diff --git a/endpoints_files.go b/endpoints_files.go index 271e0c4..e8899df 100644 --- a/endpoints_files.go +++ b/endpoints_files.go @@ -33,602 +33,599 @@ func clean(pathToClean string) string { return filepath.FromSlash(path.Clean(pathToClean)) } -// skipcq GO-R1005 -func (connector *Connector) registerFileRoutes() { - // GET /server/{id}/files?path=path - type serverFilesResponse struct { - Name string `json:"name"` - Size int64 `json:"size"` - MimeType string `json:"mimeType"` - Folder bool `json:"folder"` - LastModified int64 `json:"lastModified"` +// GET /server/{id}/files?path=path +type serverFilesResponse struct { + Name string `json:"name"` + Size int64 `json:"size"` + MimeType string `json:"mimeType"` + Folder bool `json:"folder"` + LastModified int64 `json:"lastModified"` +} + +func filesEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + if connector.Validate(w, r) == "" { + return } - connector.Router.HandleFunc("/server/{id}/files", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - if connector.Validate(w, r) == "" { - return + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, err := connector.Processes.Load(id) + // In case the process doesn't exist. + if !err { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + // Check if folder is in the process directory or not. + process.ServerConfigMutex.RLock() + defer process.ServerConfigMutex.RUnlock() + folderPath := joinPath(process.Directory, r.URL.Query().Get("path")) + if !strings.HasPrefix(folderPath, clean(process.Directory)) { + httpError(w, "The folder requested is outside the server!", http.StatusForbidden) + return + } + // Get list of files and folders in the directory. + folder, err1 := os.Open(folderPath) + if err1 != nil { + httpError(w, "This folder does not exist!", http.StatusNotFound) + return + } + defer folder.Close() + contents, err2 := folder.Readdir(-1) + if err2 != nil { + httpError(w, "This is not a folder!", http.StatusBadRequest) + return + } + // Send the response. + toSend := make(map[string]([]serverFilesResponse)) + toSend["contents"] = make([]serverFilesResponse, 0, len(contents)) + for _, value := range contents { + // Determine the MIME-Type of the file. + mimeType := "" + if value.Mode()&os.ModeSymlink != 0 { + mimeType = "inode/symlink" + } else if !value.IsDir() { + var length int64 = 512 + if value.Size() < 512 { + length = value.Size() + } + buffer := make([]byte, length) + path := joinPath(process.Directory, r.URL.Query().Get("path"), value.Name()) + file, err := os.Open(path) + if err == nil { + file.Read(buffer) // skipcq GSC-G104 + mimeType = http.DetectContentType(buffer) + file.Close() // skipcq GSC-G104 + } } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, err := connector.Processes.Load(id) - // In case the process doesn't exist. - if !err { - httpError(w, "This server does not exist!", http.StatusNotFound) + toSend["contents"] = append(toSend["contents"], serverFilesResponse{ + Folder: value.IsDir() || mimeType == "inode/symlink", + Name: value.Name(), + Size: value.Size(), + LastModified: value.ModTime().Unix(), + MimeType: mimeType, + }) + } + writeJsonStructRes(w, toSend) // skipcq GSC-G104 +} + +// GET /server/{id}/file?path=path&ticket=ticket +// POST /server/{id}/file?path=path +// DELETE /server/{id}/file?path=path +// PATCH /server/{id}/file?path=path +func fileEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + ticket, ticketExists := connector.Tickets.LoadAndDelete(r.URL.Query().Get("ticket")) + user := "" + if ticketExists && ticket.IPAddr == GetIP(r) && r.Method == "GET" { + user = ticket.User + } else if user = connector.Validate(w, r); user == "" { + return + } + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, err := connector.Processes.Load(id) + // In case the process doesn't exist. + if !err { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + // Check if path is in the process directory or not. + process.ServerConfigMutex.RLock() + defer process.ServerConfigMutex.RUnlock() + filePath := joinPath(process.Directory, r.URL.Query().Get("path")) + if (r.Method == "GET" || r.Method == "POST" || r.Method == "DELETE") && + !strings.HasPrefix(filePath, clean(process.Directory)) { + httpError(w, "The file requested is outside the server!", http.StatusForbidden) + return + } + if r.Method == "GET" { + // Get list of files and folders in the directory. + file, err := os.Open(filePath) + stat, err1 := file.Stat() + if err != nil || err1 != nil { + httpError(w, "This file does not exist!", http.StatusNotFound) + return + } else if !stat.Mode().IsRegular() { + httpError(w, "This is not a file!", http.StatusBadRequest) return } - // Check if folder is in the process directory or not. - process.ServerConfigMutex.RLock() - defer process.ServerConfigMutex.RUnlock() - folderPath := joinPath(process.Directory, r.URL.Query().Get("path")) - if !strings.HasPrefix(folderPath, clean(process.Directory)) { - httpError(w, "The folder requested is outside the server!", http.StatusForbidden) + // Send the response. + buffer := make([]byte, 512) + file.Read(buffer) // skipcq GSC-G104 + file.Close() // skipcq GSC-G104 + w.Header().Set("Content-Disposition", "attachment; filename="+stat.Name()) + w.Header().Set("Content-Type", http.DetectContentType(buffer)) + w.Header().Set("Content-Length", fmt.Sprint(stat.Size())) + file, _ = os.Open(filePath) + defer file.Close() + connector.Info("server.files.download", "ip", GetIP(r), "user", user, "server", id, + "path", clean(r.URL.Query().Get("path"))) + io.Copy(w, file) + } else if r.Method == "DELETE" { + // Check if the file exists. + if filePath == "/" { + httpError(w, "This operation is dangerous and has been forbidden!", http.StatusForbidden) return } - // Get list of files and folders in the directory. - folder, err1 := os.Open(folderPath) - if err1 != nil { - httpError(w, "This folder does not exist!", http.StatusNotFound) + _, err := os.Stat(filePath) + if err != nil && os.IsNotExist(err) { + httpError(w, "This file does not exist!", http.StatusNotFound) return } - defer folder.Close() - contents, err2 := folder.Readdir(-1) - if err2 != nil { - httpError(w, "This is not a folder!", http.StatusBadRequest) + err = os.RemoveAll(filePath) + if err != nil && err.(*os.PathError).Err != nil && err.(*os.PathError).Err.Error() == + "The process cannot access the file because it is being used by another process." { + httpError(w, err.(*os.PathError).Err.Error(), http.StatusConflict) + return + } else if err != nil { + log.Println("An error occurred when removing "+filePath, "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - // Send the response. - toSend := make(map[string]([]serverFilesResponse)) - toSend["contents"] = make([]serverFilesResponse, 0, len(contents)) - for _, value := range contents { - // Determine the MIME-Type of the file. - mimeType := "" - if value.Mode()&os.ModeSymlink != 0 { - mimeType = "inode/symlink" - } else if !value.IsDir() { - var length int64 = 512 - if value.Size() < 512 { - length = value.Size() - } - buffer := make([]byte, length) - path := joinPath(process.Directory, r.URL.Query().Get("path"), value.Name()) - file, err := os.Open(path) - if err == nil { - file.Read(buffer) // skipcq GSC-G104 - mimeType = http.DetectContentType(buffer) - file.Close() // skipcq GSC-G104 - } - } - toSend["contents"] = append(toSend["contents"], serverFilesResponse{ - Folder: value.IsDir() || mimeType == "inode/symlink", - Name: value.Name(), - Size: value.Size(), - LastModified: value.ModTime().Unix(), - MimeType: mimeType, - }) + connector.Info("server.files.delete", "ip", GetIP(r), "user", user, "server", id, + "path", clean(r.URL.Query().Get("path"))) + writeJsonStringRes(w, "{\"success\":true}") + } else if r.Method == "POST" { + // Parse our multipart form, 5120 << 20 specifies a maximum upload of a 5 GB file. + err := r.ParseMultipartForm(5120 << 20) + if err != nil { + httpError(w, "Invalid form sent!", http.StatusBadRequest) + return } - writeJsonStructRes(w, toSend) // skipcq GSC-G104 - }) - - // GET /server/{id}/file?path=path&ticket=ticket - // POST /server/{id}/file?path=path - // DELETE /server/{id}/file?path=path - // PATCH /server/{id}/file?path=path - connector.Router.HandleFunc("/server/{id}/file", func(w http.ResponseWriter, r *http.Request) { - ticket, ticketExists := connector.Tickets.LoadAndDelete(r.URL.Query().Get("ticket")) - user := "" - if ticketExists && ticket.IPAddr == GetIP(r) && r.Method == "GET" { - user = ticket.User - } else if user = connector.Validate(w, r); user == "" { + // FormFile returns the first file for the given key `upload` + file, meta, err := r.FormFile("upload") + if err != nil { + httpError(w, "Invalid form sent!", http.StatusBadRequest) return } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, err := connector.Processes.Load(id) - // In case the process doesn't exist. - if !err { - httpError(w, "This server does not exist!", http.StatusNotFound) + defer file.Close() + // read the file. + filePath = joinPath(process.Directory, r.URL.Query().Get("path"), meta.Filename) + toWrite, err := os.Create(filePath) + stat, statErr := os.Stat(filePath) + if statErr == nil && stat.IsDir() { + httpError(w, "This is a folder!", http.StatusBadRequest) + return + } else if err != nil { + log.Println("An error occurred when writing to "+filePath, "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - // Check if path is in the process directory or not. - process.ServerConfigMutex.RLock() - defer process.ServerConfigMutex.RUnlock() - filePath := joinPath(process.Directory, r.URL.Query().Get("path")) - if (r.Method == "GET" || r.Method == "POST" || r.Method == "DELETE") && - !strings.HasPrefix(filePath, clean(process.Directory)) { - httpError(w, "The file requested is outside the server!", http.StatusForbidden) + defer toWrite.Close() + // write this byte array to our file + connector.Info("server.files.upload", "ip", GetIP(r), "user", user, "server", id, + "path", clean(r.URL.Query().Get("path")), "filename", meta.Filename) + io.Copy(toWrite, file) + writeJsonStringRes(w, "{\"success\":true}") + } else if r.Method == "PATCH" { + // Get the request body to check the operation. + var body bytes.Buffer + _, err := body.ReadFrom(r.Body) + if err != nil { + httpError(w, "Failed to read body!", http.StatusBadRequest) return } - if r.Method == "GET" { - // Get list of files and folders in the directory. - file, err := os.Open(filePath) - stat, err1 := file.Stat() - if err != nil || err1 != nil { - httpError(w, "This file does not exist!", http.StatusNotFound) - return - } else if !stat.Mode().IsRegular() { - httpError(w, "This is not a file!", http.StatusBadRequest) + // If the body doesn't start with {, parse as a legacy request. Remove this in Octyne 2.0. + // Legacy requests will not support anything further than mv/cp operations. + var req struct { + Operation string `json:"operation"` + Src string `json:"src"` + Dest string `json:"dest"` + } + if strings.TrimSpace(body.String())[0] != '{' { + split := strings.Split(body.String(), "\n") + if len(split) != 3 { + if split[0] == "mv" || split[0] == "cp" { + httpError(w, split[0]+" operation requires two arguments!", http.StatusMethodNotAllowed) + } else { + httpError(w, "Invalid operation! Operations available: mv,cp", http.StatusMethodNotAllowed) + } return } - // Send the response. - buffer := make([]byte, 512) - file.Read(buffer) // skipcq GSC-G104 - file.Close() // skipcq GSC-G104 - w.Header().Set("Content-Disposition", "attachment; filename="+stat.Name()) - w.Header().Set("Content-Type", http.DetectContentType(buffer)) - w.Header().Set("Content-Length", fmt.Sprint(stat.Size())) - file, _ = os.Open(filePath) - defer file.Close() - connector.Info("server.files.download", "ip", GetIP(r), "user", user, "server", id, - "path", clean(r.URL.Query().Get("path"))) - io.Copy(w, file) - } else if r.Method == "DELETE" { - // Check if the file exists. - if filePath == "/" { - httpError(w, "This operation is dangerous and has been forbidden!", http.StatusForbidden) + req.Operation = split[0] + req.Src = split[1] + req.Dest = split[2] + } else if err = json.Unmarshal(body.Bytes(), &req); err != nil { + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } + // Possible operations: mv, cp + if req.Operation == "mv" || req.Operation == "cp" { + // Check if original file exists. + oldpath := joinPath(process.Directory, req.Src) + newpath := joinPath(process.Directory, req.Dest) + if !strings.HasPrefix(oldpath, clean(process.Directory)) || + !strings.HasPrefix(newpath, clean(process.Directory)) { + httpError(w, "The files requested are outside the server!", http.StatusForbidden) return } - _, err := os.Stat(filePath) - if err != nil && os.IsNotExist(err) { + stat, err := os.Stat(oldpath) + if os.IsNotExist(err) { httpError(w, "This file does not exist!", http.StatusNotFound) return - } - err = os.RemoveAll(filePath) - if err != nil && err.(*os.PathError).Err != nil && err.(*os.PathError).Err.Error() == - "The process cannot access the file because it is being used by another process." { - httpError(w, err.(*os.PathError).Err.Error(), http.StatusConflict) - return } else if err != nil { - log.Println("An error occurred when removing "+filePath, "("+process.Name+")", err) + log.Println("An error occurred in mv/cp API when checking for "+oldpath, "("+process.Name+")", err) httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - connector.Info("server.files.delete", "ip", GetIP(r), "user", user, "server", id, - "path", clean(r.URL.Query().Get("path"))) - writeJsonStringRes(w, "{\"success\":true}") - } else if r.Method == "POST" { - // Parse our multipart form, 5120 << 20 specifies a maximum upload of a 5 GB file. - err := r.ParseMultipartForm(5120 << 20) - if err != nil { - httpError(w, "Invalid form sent!", http.StatusBadRequest) - return - } - // FormFile returns the first file for the given key `upload` - file, meta, err := r.FormFile("upload") - if err != nil { - httpError(w, "Invalid form sent!", http.StatusBadRequest) - return - } - defer file.Close() - // read the file. - filePath = joinPath(process.Directory, r.URL.Query().Get("path"), meta.Filename) - toWrite, err := os.Create(filePath) - stat, statErr := os.Stat(filePath) - if statErr == nil && stat.IsDir() { - httpError(w, "This is a folder!", http.StatusBadRequest) + // Check if destination file exists. + if stat, err := os.Stat(newpath); err == nil && stat.IsDir() { + newpath = joinPath(newpath, path.Base(oldpath)) + } else if err == nil { + httpError(w, "This file already exists!", http.StatusMethodNotAllowed) return - } else if err != nil { - log.Println("An error occurred when writing to "+filePath, "("+process.Name+")", err) + } else if err != nil && !os.IsNotExist(err) { + log.Println("An error occurred in mv/cp API when checking for "+newpath, "("+process.Name+")", err) httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - defer toWrite.Close() - // write this byte array to our file - connector.Info("server.files.upload", "ip", GetIP(r), "user", user, "server", id, - "path", clean(r.URL.Query().Get("path")), "filename", meta.Filename) - io.Copy(toWrite, file) - writeJsonStringRes(w, "{\"success\":true}") - } else if r.Method == "PATCH" { - // Get the request body to check the operation. - var body bytes.Buffer - _, err := body.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - // If the body doesn't start with {, parse as a legacy request. Remove this in Octyne 2.0. - // Legacy requests will not support anything further than mv/cp operations. - var req struct { - Operation string `json:"operation"` - Src string `json:"src"` - Dest string `json:"dest"` - } - if strings.TrimSpace(body.String())[0] != '{' { - split := strings.Split(body.String(), "\n") - if len(split) != 3 { - if split[0] == "mv" || split[0] == "cp" { - httpError(w, split[0]+" operation requires two arguments!", http.StatusMethodNotAllowed) - } else { - httpError(w, "Invalid operation! Operations available: mv,cp", http.StatusMethodNotAllowed) - } - return - } - req.Operation = split[0] - req.Src = split[1] - req.Dest = split[2] - } else if err = json.Unmarshal(body.Bytes(), &req); err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) - return - } - // Possible operations: mv, cp - if req.Operation == "mv" || req.Operation == "cp" { - // Check if original file exists. - oldpath := joinPath(process.Directory, req.Src) - newpath := joinPath(process.Directory, req.Dest) - if !strings.HasPrefix(oldpath, clean(process.Directory)) || - !strings.HasPrefix(newpath, clean(process.Directory)) { - httpError(w, "The files requested are outside the server!", http.StatusForbidden) - return - } - stat, err := os.Stat(oldpath) - if os.IsNotExist(err) { - httpError(w, "This file does not exist!", http.StatusNotFound) + // Move file if operation is mv. + if req.Operation == "mv" { + err := os.Rename(oldpath, newpath) + if err != nil && err.(*os.LinkError).Err != nil && err.(*os.LinkError).Err.Error() == + "The process cannot access the file because it is being used by another process." { + httpError(w, err.(*os.LinkError).Err.Error(), http.StatusConflict) return } else if err != nil { - log.Println("An error occurred in mv/cp API when checking for "+oldpath, "("+process.Name+")", err) + log.Println("An error occurred when moving "+oldpath+" to "+newpath, "("+process.Name+")", err) httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - // Check if destination file exists. - if stat, err := os.Stat(newpath); err == nil && stat.IsDir() { - newpath = joinPath(newpath, path.Base(oldpath)) - } else if err == nil { - httpError(w, "This file already exists!", http.StatusMethodNotAllowed) - return - } else if err != nil && !os.IsNotExist(err) { - log.Println("An error occurred in mv/cp API when checking for "+newpath, "("+process.Name+")", err) + connector.Info("server.files.move", "ip", GetIP(r), "user", user, "server", id, + "src", clean(req.Src), "dest", clean(req.Dest)) + writeJsonStringRes(w, "{\"success\":true}") + } else { + err := system.Copy(stat.Mode(), oldpath, newpath) + if err != nil { + log.Println("An error occurred when copying "+oldpath+" to "+newpath, "("+process.Name+")", err) httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - // Move file if operation is mv. - if req.Operation == "mv" { - err := os.Rename(oldpath, newpath) - if err != nil && err.(*os.LinkError).Err != nil && err.(*os.LinkError).Err.Error() == - "The process cannot access the file because it is being used by another process." { - httpError(w, err.(*os.LinkError).Err.Error(), http.StatusConflict) - return - } else if err != nil { - log.Println("An error occurred when moving "+oldpath+" to "+newpath, "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - connector.Info("server.files.move", "ip", GetIP(r), "user", user, "server", id, - "src", clean(req.Src), "dest", clean(req.Dest)) - writeJsonStringRes(w, "{\"success\":true}") - } else { - err := system.Copy(stat.Mode(), oldpath, newpath) - if err != nil { - log.Println("An error occurred when copying "+oldpath+" to "+newpath, "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - connector.Info("server.files.copy", "ip", GetIP(r), "user", user, "server", id, - "src", clean(req.Src), "dest", clean(req.Dest)) - writeJsonStringRes(w, "{\"success\":true}") - } - } else { - httpError(w, "Invalid operation! Operations available: mv,cp", http.StatusMethodNotAllowed) - } - } else { - httpError(w, "Only GET, POST, PATCH and DELETE are allowed!", http.StatusMethodNotAllowed) - } - }) - - // POST /server/{id}/folder?path=path - connector.Router.HandleFunc("/server/{id}/folder", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, err := connector.Processes.Load(id) - // In case the process doesn't exist. - if !err { - httpError(w, "This server does not exist!", http.StatusNotFound) - return - } - if r.Method == "POST" { - // Check if the folder already exists. - process.ServerConfigMutex.RLock() - defer process.ServerConfigMutex.RUnlock() - file := joinPath(process.Directory, r.URL.Query().Get("path")) - // Check if folder is in the process directory or not. - if !strings.HasPrefix(file, clean(process.Directory)) { - httpError(w, "The folder requested is outside the server!", http.StatusForbidden) - return - } - _, err := os.Stat(file) - if !os.IsNotExist(err) { - httpError(w, "This folder already exists!", http.StatusBadRequest) - return - } - // Create the folder. - err = os.Mkdir(file, os.ModePerm) - if err != nil { - log.Println("An error occurred when creating folder "+file, "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return + connector.Info("server.files.copy", "ip", GetIP(r), "user", user, "server", id, + "src", clean(req.Src), "dest", clean(req.Dest)) + writeJsonStringRes(w, "{\"success\":true}") } - connector.Info("server.files.createFolder", "ip", GetIP(r), "user", user, "server", id, - "path", clean(r.URL.Query().Get("path"))) - writeJsonStringRes(w, "{\"success\":true}") } else { - httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed) + httpError(w, "Invalid operation! Operations available: mv,cp", http.StatusMethodNotAllowed) } - }) + } else { + httpError(w, "Only GET, POST, PATCH and DELETE are allowed!", http.StatusMethodNotAllowed) + } +} - compressionProgressMap := xsync.NewMapOf[string]() - // GET /server/{id}/compress?token=token - // POST /server/{id}/compress?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean - // POST /server/{id}/compress/v2?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean - compressionEndpoint := func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { - return - } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, exists := connector.Processes.Load(id) - // In case the process doesn't exist. - if !exists { - httpError(w, "This server does not exist!", http.StatusNotFound) - return - } else if r.Method == "GET" { - if r.URL.Query().Get("token") == "" { - httpError(w, "No token provided!", http.StatusBadRequest) - return - } - progress, valid := compressionProgressMap.Load(r.URL.Query().Get("token")) - if !valid { - httpError(w, "Invalid token!", http.StatusBadRequest) - } else if progress == "finished" { - writeJsonStringRes(w, "{\"finished\":true}") - } else if progress == "" { - writeJsonStringRes(w, "{\"finished\":false}") - } else { - httpError(w, progress, http.StatusInternalServerError) - } - return - } else if r.Method != "POST" { - httpError(w, "Only GET and POST are allowed!", http.StatusMethodNotAllowed) - return - } - // Decode parameters. - async := r.URL.Query().Get("async") == "true" - basePath := r.URL.Query().Get("basePath") - archiveType := "zip" - compression := "true" - if r.URL.Query().Get("archiveType") != "" { - archiveType = r.URL.Query().Get("archiveType") - } - if r.URL.Query().Get("compress") != "" { - compression = r.URL.Query().Get("compress") - } - if archiveType != "zip" && archiveType != "tar" { - httpError(w, "Invalid archive type!", http.StatusBadRequest) - return - } else if compression != "true" && compression != "false" && - compression != "zstd" && compression != "xz" && compression != "gzip" { - httpError(w, "Invalid compression type!", http.StatusBadRequest) +// POST /server/{id}/folder?path=path +func folderEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, err := connector.Processes.Load(id) + // In case the process doesn't exist. + if !err { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + if r.Method == "POST" { + // Check if the folder already exists. + process.ServerConfigMutex.RLock() + defer process.ServerConfigMutex.RUnlock() + file := joinPath(process.Directory, r.URL.Query().Get("path")) + // Check if folder is in the process directory or not. + if !strings.HasPrefix(file, clean(process.Directory)) { + httpError(w, "The folder requested is outside the server!", http.StatusForbidden) return } - // Get the body. - var buffer bytes.Buffer - _, err := buffer.ReadFrom(r.Body) - if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) + _, err := os.Stat(file) + if !os.IsNotExist(err) { + httpError(w, "This folder already exists!", http.StatusBadRequest) return } - // Decode the array body and send it to files. - var files []string - err = json.Unmarshal(buffer.Bytes(), &files) + // Create the folder. + err = os.Mkdir(file, os.ModePerm) if err != nil { - httpError(w, "Invalid JSON body!", http.StatusBadRequest) + log.Println("An error occurred when creating folder "+file, "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) return } - // Validate every path. - process.ServerConfigMutex.RLock() - defer process.ServerConfigMutex.RUnlock() - if !strings.HasPrefix(joinPath(process.Directory, basePath), clean(process.Directory)) { - httpError(w, "The base path is outside the server directory!", http.StatusForbidden) + connector.Info("server.files.createFolder", "ip", GetIP(r), "user", user, "server", id, + "path", clean(r.URL.Query().Get("path"))) + writeJsonStringRes(w, "{\"success\":true}") + } else { + httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed) + } +} + +// GET /server/{id}/compress?token=token +// POST /server/{id}/compress?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean +// POST /server/{id}/compress/v2?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean +var compressionProgressMap = xsync.NewMapOf[string]() + +func compressionEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, exists := connector.Processes.Load(id) + // In case the process doesn't exist. + if !exists { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } else if r.Method == "GET" { + if r.URL.Query().Get("token") == "" { + httpError(w, "No token provided!", http.StatusBadRequest) return } - for _, file := range files { - filepath := joinPath(process.Directory, basePath, file) - if !strings.HasPrefix(filepath, clean(process.Directory)) { - httpError(w, "One of the paths provided is outside the server directory!", http.StatusForbidden) - return - } else if _, err := os.Stat(filepath); err != nil { - if os.IsNotExist(err) { - httpError(w, "The file "+file+" does not exist!", http.StatusBadRequest) - } else { - log.Println("An error occurred when checking "+filepath+" exists for compression", "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - } - return - } + progress, valid := compressionProgressMap.Load(r.URL.Query().Get("token")) + if !valid { + httpError(w, "Invalid token!", http.StatusBadRequest) + } else if progress == "finished" { + writeJsonStringRes(w, "{\"finished\":true}") + } else if progress == "" { + writeJsonStringRes(w, "{\"finished\":false}") + } else { + httpError(w, progress, http.StatusInternalServerError) } - // Check if a file exists at the location of the archive. - archivePath := joinPath(process.Directory, r.URL.Query().Get("path")) - if !strings.HasPrefix(archivePath, clean(process.Directory)) { - httpError(w, "The requested archive is outside the server directory!", http.StatusForbidden) + return + } else if r.Method != "POST" { + httpError(w, "Only GET and POST are allowed!", http.StatusMethodNotAllowed) + return + } + // Decode parameters. + async := r.URL.Query().Get("async") == "true" + basePath := r.URL.Query().Get("basePath") + archiveType := "zip" + compression := "true" + if r.URL.Query().Get("archiveType") != "" { + archiveType = r.URL.Query().Get("archiveType") + } + if r.URL.Query().Get("compress") != "" { + compression = r.URL.Query().Get("compress") + } + if archiveType != "zip" && archiveType != "tar" { + httpError(w, "Invalid archive type!", http.StatusBadRequest) + return + } else if compression != "true" && compression != "false" && + compression != "zstd" && compression != "xz" && compression != "gzip" { + httpError(w, "Invalid compression type!", http.StatusBadRequest) + return + } + // Get the body. + var buffer bytes.Buffer + _, err := buffer.ReadFrom(r.Body) + if err != nil { + httpError(w, "Failed to read body!", http.StatusBadRequest) + return + } + // Decode the array body and send it to files. + var files []string + err = json.Unmarshal(buffer.Bytes(), &files) + if err != nil { + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } + // Validate every path. + process.ServerConfigMutex.RLock() + defer process.ServerConfigMutex.RUnlock() + if !strings.HasPrefix(joinPath(process.Directory, basePath), clean(process.Directory)) { + httpError(w, "The base path is outside the server directory!", http.StatusForbidden) + return + } + for _, file := range files { + filepath := joinPath(process.Directory, basePath, file) + if !strings.HasPrefix(filepath, clean(process.Directory)) { + httpError(w, "One of the paths provided is outside the server directory!", http.StatusForbidden) return - } - _, err = os.Stat(archivePath) - if !os.IsNotExist(err) { - httpError(w, "A file/folder already exists at the path of requested archive!", http.StatusBadRequest) + } else if _, err := os.Stat(filepath); err != nil { + if os.IsNotExist(err) { + httpError(w, "The file "+file+" does not exist!", http.StatusBadRequest) + } else { + log.Println("An error occurred when checking "+filepath+" exists for compression", "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + } return } + } + // Check if a file exists at the location of the archive. + archivePath := joinPath(process.Directory, r.URL.Query().Get("path")) + if !strings.HasPrefix(archivePath, clean(process.Directory)) { + httpError(w, "The requested archive is outside the server directory!", http.StatusForbidden) + return + } + _, err = os.Stat(archivePath) + if !os.IsNotExist(err) { + httpError(w, "A file/folder already exists at the path of requested archive!", http.StatusBadRequest) + return + } - // Begin compressing the archive. - archiveFile, err := os.Create(archivePath) - if err != nil { - log.Println("An error occurred when creating "+archivePath+" for compression", "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - tokenBytes := make([]byte, 16) - rand.Read(tokenBytes) // Tolerate errors here, an error here is incredibly unlikely: skipcq GSC-G104 - token := hex.EncodeToString(tokenBytes) - completionFunc := func() { - defer archiveFile.Close() - if archiveType == "zip" { - archive := zip.NewWriter(archiveFile) - defer archive.Close() - // Archive stuff inside. - for _, file := range files { - err := system.AddFileToZip(archive, joinPath(process.Directory, basePath), file, compression != "false") - if err != nil { - log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err) - if !async { - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - } else { - compressionProgressMap.Store(token, "Internal Server Error!") - } - return + // Begin compressing the archive. + archiveFile, err := os.Create(archivePath) + if err != nil { + log.Println("An error occurred when creating "+archivePath+" for compression", "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + tokenBytes := make([]byte, 16) + rand.Read(tokenBytes) // Tolerate errors here, an error here is incredibly unlikely: skipcq GSC-G104 + token := hex.EncodeToString(tokenBytes) + completionFunc := func() { + defer archiveFile.Close() + if archiveType == "zip" { + archive := zip.NewWriter(archiveFile) + defer archive.Close() + // Archive stuff inside. + for _, file := range files { + err := system.AddFileToZip(archive, joinPath(process.Directory, basePath), file, compression != "false") + if err != nil { + log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err) + if !async { + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + } else { + compressionProgressMap.Store(token, "Internal Server Error!") } + return } + } + } else { + var archive *tar.Writer + if compression == "true" || compression == "gzip" || compression == "" { + compressionWriter := gzip.NewWriter(archiveFile) + defer compressionWriter.Close() + archive = tar.NewWriter(compressionWriter) + } else if compression == "xz" || compression == "zstd" { + compressionWriter := system.NativeCompressionWriter(archiveFile, compression) + defer compressionWriter.Close() + archive = tar.NewWriter(compressionWriter) } else { - var archive *tar.Writer - if compression == "true" || compression == "gzip" || compression == "" { - compressionWriter := gzip.NewWriter(archiveFile) - defer compressionWriter.Close() - archive = tar.NewWriter(compressionWriter) - } else if compression == "xz" || compression == "zstd" { - compressionWriter := system.NativeCompressionWriter(archiveFile, compression) - defer compressionWriter.Close() - archive = tar.NewWriter(compressionWriter) - } else { - archive = tar.NewWriter(archiveFile) - } - defer archive.Close() - for _, file := range files { - err := system.AddFileToTar(archive, joinPath(process.Directory, basePath), file) - if err != nil { - log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err) - if !async { - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - } else { - compressionProgressMap.Store(token, "Internal Server Error!") - } - return + archive = tar.NewWriter(archiveFile) + } + defer archive.Close() + for _, file := range files { + err := system.AddFileToTar(archive, joinPath(process.Directory, basePath), file) + if err != nil { + log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err) + if !async { + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + } else { + compressionProgressMap.Store(token, "Internal Server Error!") } + return } } - connector.Info("server.files.compress", "ip", GetIP(r), "user", user, "server", id, - "archive", clean(r.URL.Query().Get("path")), "archiveType", archiveType, - "compression", compression, "basePath", basePath, "files", files) - if async { - compressionProgressMap.Store(token, "finished") - go func() { // We want our previous Close() defers to call *now*, so we do this in goroutine - <-time.After(10 * time.Second) - compressionProgressMap.Delete(token) - }() - } else { - writeJsonStringRes(w, "{\"success\":true}") - } } + connector.Info("server.files.compress", "ip", GetIP(r), "user", user, "server", id, + "archive", clean(r.URL.Query().Get("path")), "archiveType", archiveType, + "compression", compression, "basePath", basePath, "files", files) if async { - compressionProgressMap.Store(token, "") - writeJsonStringRes(w, "{\"token\":\""+token+"\"}") - go completionFunc() + compressionProgressMap.Store(token, "finished") + go func() { // We want our previous Close() defers to call *now*, so we do this in goroutine + <-time.After(10 * time.Second) + compressionProgressMap.Delete(token) + }() } else { - completionFunc() + writeJsonStringRes(w, "{\"success\":true}") } } - connector.Router.HandleFunc("/server/{id}/compress", compressionEndpoint) - connector.Router.HandleFunc("/server/{id}/compress/v2", compressionEndpoint) + if async { + compressionProgressMap.Store(token, "") + writeJsonStringRes(w, "{\"token\":\""+token+"\"}") + go completionFunc() + } else { + completionFunc() + } +} - // POST /server/{id}/decompress?path=path - connector.Router.HandleFunc("/server/{id}/decompress", func(w http.ResponseWriter, r *http.Request) { - // Check with authenticator. - user := connector.Validate(w, r) - if user == "" { +// POST /server/{id}/decompress?path=path +func decompressionEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, err := connector.Processes.Load(id) + // In case the process doesn't exist. + if !err { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + process.ServerConfigMutex.RLock() + defer process.ServerConfigMutex.RUnlock() + directory := clean(process.Directory) + if r.Method == "POST" { + // Check if the archive exists. + archivePath := joinPath(directory, r.URL.Query().Get("path")) + if !strings.HasPrefix(archivePath, directory) { + httpError(w, "The archive is outside the server directory!", http.StatusForbidden) + return + } + archiveStat, exists := os.Stat(archivePath) + if os.IsNotExist(exists) { + httpError(w, "The requested archive does not exist!", http.StatusBadRequest) + return + } else if exists != nil { + log.Println("An error occurred when checking "+archivePath+" archive file exists", "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } else if archiveStat.IsDir() { + httpError(w, "The requested archive is a folder!", http.StatusBadRequest) return } - // Get the process being accessed. - id := mux.Vars(r)["id"] - process, err := connector.Processes.Load(id) - // In case the process doesn't exist. - if !err { - httpError(w, "This server does not exist!", http.StatusNotFound) + // Check if there is a file/folder at the destination. + var body bytes.Buffer + _, err := body.ReadFrom(r.Body) + if err != nil { + httpError(w, "Failed to read body!", http.StatusBadRequest) return } - process.ServerConfigMutex.RLock() - defer process.ServerConfigMutex.RUnlock() - directory := clean(process.Directory) - if r.Method == "POST" { - // Check if the archive exists. - archivePath := joinPath(directory, r.URL.Query().Get("path")) - if !strings.HasPrefix(archivePath, directory) { - httpError(w, "The archive is outside the server directory!", http.StatusForbidden) - return - } - archiveStat, exists := os.Stat(archivePath) - if os.IsNotExist(exists) { - httpError(w, "The requested archive does not exist!", http.StatusBadRequest) - return - } else if exists != nil { - log.Println("An error occurred when checking "+archivePath+" archive file exists", "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } else if archiveStat.IsDir() { - httpError(w, "The requested archive is a folder!", http.StatusBadRequest) - return - } - // Check if there is a file/folder at the destination. - var body bytes.Buffer - _, err := body.ReadFrom(r.Body) + unpackPath := joinPath(directory, body.String()) + if !strings.HasPrefix(unpackPath, directory) { + httpError(w, "The archive file is outside the server directory!", http.StatusForbidden) + return + } + stat, err := os.Stat(unpackPath) + if os.IsNotExist(err) { + err = os.Mkdir(unpackPath, os.ModePerm) if err != nil { - httpError(w, "Failed to read body!", http.StatusBadRequest) - return - } - unpackPath := joinPath(directory, body.String()) - if !strings.HasPrefix(unpackPath, directory) { - httpError(w, "The archive file is outside the server directory!", http.StatusForbidden) - return - } - stat, err := os.Stat(unpackPath) - if os.IsNotExist(err) { - err = os.Mkdir(unpackPath, os.ModePerm) - if err != nil { - log.Println("An error occurred when creating "+unpackPath+" to unpack archive", "("+process.Name+")", err) - httpError(w, "Internal Server Error!", http.StatusInternalServerError) - return - } - } else if err != nil { - log.Println("An error occurred when checking "+unpackPath+" exists to unpack archive to", "("+process.Name+")", err) + log.Println("An error occurred when creating "+unpackPath+" to unpack archive", "("+process.Name+")", err) httpError(w, "Internal Server Error!", http.StatusInternalServerError) return - } else if !stat.IsDir() { - httpError(w, "There is a file at the requested unpack destination!", http.StatusBadRequest) - return - } - // Decompress the archive. - if strings.HasSuffix(archivePath, ".zip") { - err = system.UnzipFile(archivePath, unpackPath) - } else if strings.HasSuffix(archivePath, ".tar") || - strings.HasSuffix(archivePath, ".tar.gz") || strings.HasSuffix(archivePath, ".tgz") || - strings.HasSuffix(archivePath, ".tar.bz") || strings.HasSuffix(archivePath, ".tbz") || - strings.HasSuffix(archivePath, ".tar.bz2") || strings.HasSuffix(archivePath, ".tbz2") || - strings.HasSuffix(archivePath, ".tar.xz") || strings.HasSuffix(archivePath, ".txz") || - strings.HasSuffix(archivePath, ".tar.zst") || strings.HasSuffix(archivePath, ".tzst") { - err = system.ExtractTarFile(archivePath, unpackPath) - } else { - httpError(w, "Unsupported archive file!", http.StatusBadRequest) - return - } - if err != nil { - httpError(w, "An error occurred while decompressing archive!", http.StatusInternalServerError) - return } - connector.Info("server.files.decompress", "ip", GetIP(r), "user", user, "server", id, - "archive", clean(r.URL.Query().Get("path")), "destPath", body.String()) - writeJsonStringRes(w, "{\"success\":true}") + } else if err != nil { + log.Println("An error occurred when checking "+unpackPath+" exists to unpack archive to", "("+process.Name+")", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } else if !stat.IsDir() { + httpError(w, "There is a file at the requested unpack destination!", http.StatusBadRequest) + return + } + // Decompress the archive. + if strings.HasSuffix(archivePath, ".zip") { + err = system.UnzipFile(archivePath, unpackPath) + } else if strings.HasSuffix(archivePath, ".tar") || + strings.HasSuffix(archivePath, ".tar.gz") || strings.HasSuffix(archivePath, ".tgz") || + strings.HasSuffix(archivePath, ".tar.bz") || strings.HasSuffix(archivePath, ".tbz") || + strings.HasSuffix(archivePath, ".tar.bz2") || strings.HasSuffix(archivePath, ".tbz2") || + strings.HasSuffix(archivePath, ".tar.xz") || strings.HasSuffix(archivePath, ".txz") || + strings.HasSuffix(archivePath, ".tar.zst") || strings.HasSuffix(archivePath, ".tzst") { + err = system.ExtractTarFile(archivePath, unpackPath) } else { - httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed) + httpError(w, "Unsupported archive file!", http.StatusBadRequest) + return } - }) + if err != nil { + httpError(w, "An error occurred while decompressing archive!", http.StatusInternalServerError) + return + } + connector.Info("server.files.decompress", "ip", GetIP(r), "user", user, "server", id, + "archive", clean(r.URL.Query().Get("path")), "destPath", body.String()) + writeJsonStringRes(w, "{\"success\":true}") + } else { + httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed) + } } diff --git a/endpoints_misc.go b/endpoints_misc.go new file mode 100644 index 0000000..c9b7f67 --- /dev/null +++ b/endpoints_misc.go @@ -0,0 +1,382 @@ +package main + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/retrixe/octyne/auth" + "github.com/retrixe/octyne/system" +) + +// GET / +func rootEndpoint(w http.ResponseWriter, r *http.Request) { + writeJsonStringRes(w, "{\"version\": \""+OctyneVersion+"\"}") +} + +// GET /config +// PATCH /config +func configEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + user := connector.Validate(w, r) + if user == "" { + return + } else if r.Method != "GET" && r.Method != "PATCH" { + httpError(w, "Only GET and PATCH are allowed!", http.StatusMethodNotAllowed) + return + } + if r.Method == "GET" { + contents, err := os.ReadFile(ConfigJsonPath) + if err != nil { + log.Println("Error reading "+ConfigJsonPath+" when user accessed /config!", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + connector.Info("config.view", "ip", GetIP(r), "user", user) + writeJsonStringRes(w, string(contents)) + } else if r.Method == "PATCH" { + var buffer bytes.Buffer + _, err := buffer.ReadFrom(r.Body) + if err != nil { + httpError(w, "Failed to read body!", http.StatusBadRequest) + return + } + var origJson = buffer.String() + var config Config + contents, err := StripLineCommentsFromJSON(buffer.Bytes()) + if err != nil { + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } + err = json.Unmarshal(contents, &config) + if err != nil { + httpError(w, "Invalid JSON body!", http.StatusBadRequest) + return + } + err = os.WriteFile(ConfigJsonPath+"~", []byte(strings.TrimRight(origJson, "\n")+"\n"), 0666) + if err != nil { + log.Println("Error writing to " + ConfigJsonPath + " when user modified config!") + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + err = os.Rename(ConfigJsonPath+"~", ConfigJsonPath) + if err != nil { + log.Println("Error writing to " + ConfigJsonPath + " when user modified config!") + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + connector.UpdateConfig(&config) + connector.Info("config.edit", "ip", GetIP(r), "user", user, "newConfig", config) + writeJsonStringRes(w, "{\"success\":true}") + info.Println("Config updated remotely by user over HTTP API (see action logs for info)!") + } +} + +// GET /config/reload +func configReloadEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + // Read the new config. + var config Config + contents, err := os.ReadFile(ConfigJsonPath) + if err != nil { + log.Println("An error occurred while attempting to read config! " + err.Error()) + httpError(w, "An error occurred while reading config!", http.StatusInternalServerError) + return + } + contents, err = StripLineCommentsFromJSON(contents) + if err != nil { + log.Println("An error occurred while attempting to read config! " + err.Error()) + httpError(w, "An error occurred while reading config!", http.StatusInternalServerError) + return + } + err = json.Unmarshal(contents, &config) + if err != nil { + log.Println("An error occurred while attempting to parse config! " + err.Error()) + httpError(w, "An error occurred while parsing config!", http.StatusInternalServerError) + return + } + // Reload the config. + connector.UpdateConfig(&config) + // Send the response. + connector.Info("config.reload", "ip", GetIP(r), "user", user) + writeJsonStringRes(w, "{\"success\":true}") + info.Println("Config reloaded successfully!") +} + +// GET /servers +type serversResponse struct { + Servers map[string]interface{} `json:"servers"` +} + +func serversEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + if connector.Validate(w, r) == "" { + return + } + // Get a map of processes and their online status. + processes := make(map[string]interface{}) + connector.Processes.Range(func(k string, v *managedProcess) bool { + if r.URL.Query().Get("extrainfo") == "true" { + processes[v.Name] = map[string]interface{}{ + "status": v.Online.Load(), + "toDelete": v.ToDelete.Load(), + } + } else { + processes[v.Name] = v.Online.Load() + } + return true + }) + // Send the list. + writeJsonStructRes(w, serversResponse{Servers: processes}) // skipcq GSC-G104 +} + +// GET /server/{id} +// POST /server/{id} +type serverResponse struct { + Status int `json:"status"` + CPUUsage float64 `json:"cpuUsage"` + MemoryUsage float64 `json:"memoryUsage"` + TotalMemory int64 `json:"totalMemory"` + Uptime int64 `json:"uptime"` + ToDelete bool `json:"toDelete,omitempty"` +} + +var totalMemory = int64(system.GetTotalSystemMemory()) + +func serverEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + user := connector.Validate(w, r) + if user == "" { + return + } + // Get the process being accessed. + id := mux.Vars(r)["id"] + process, err := connector.Processes.Load(id) + // In case the process doesn't exist. + if !err { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + // POST /server/{id} + if r.Method == "POST" { + // Get the request body to check whether the operation is to START or STOP. + var body bytes.Buffer + _, err := body.ReadFrom(r.Body) + if err != nil { + httpError(w, "Failed to read body!", http.StatusBadRequest) + return + } + operation := strings.ToUpper(body.String()) + // Check whether the operation is correct or not. + if operation == "START" { + // Start process if required. + if process.Online.Load() != 1 { + err = process.StartProcess(connector) + connector.Info("server.start", "ip", GetIP(r), "user", user, "server", id) + } + // Send a response. + res := make(map[string]bool) + res["success"] = err == nil + writeJsonStructRes(w, res) // skipcq GSC-G104 + } else if operation == "STOP" || operation == "KILL" || operation == "TERM" { + // Stop process if required. + if process.Online.Load() == 1 { + // Octyne 2.x should drop STOP or move it to SIGTERM. + if operation == "KILL" || operation == "STOP" { + process.KillProcess() + connector.Info("server.kill", "ip", GetIP(r), "user", user, "server", id) + } else { + process.StopProcess() + connector.Info("server.stop", "ip", GetIP(r), "user", user, "server", id) + } + } + // Send a response. + res := make(map[string]bool) + res["success"] = true + writeJsonStructRes(w, res) // skipcq GSC-G104 + } else { + httpError(w, "Invalid operation requested!", http.StatusBadRequest) + return + } + // GET /server/{id} + } else if r.Method == "GET" { + // Get the PID of the process. + var stat system.ProcessStats + process.CommandMutex.RLock() + defer process.CommandMutex.RUnlock() + if process.Command != nil && + process.Command.Process != nil && + // Command.ProcessState == nil && // ProcessState isn't mutexed, the next if should suffice + process.Online.Load() == 1 { + // Get CPU usage and memory usage of the process. + var err error + stat, err = system.GetProcessStats(process.Command.Process.Pid) + if err != nil { + log.Println("Failed to get server statistics for "+process.Name+"! Is ps available?", err) + httpError(w, "Internal Server Error!", http.StatusInternalServerError) + return + } + } + + // Send a response. + uptime := process.Uptime.Load() + if uptime > 0 { + uptime = time.Now().UnixNano() - uptime + } + res := serverResponse{ + Status: int(process.Online.Load()), + Uptime: uptime, + CPUUsage: stat.CPUUsage, + MemoryUsage: stat.RSSMemory, + TotalMemory: totalMemory, + ToDelete: process.ToDelete.Load(), + } + writeJsonStructRes(w, res) // skipcq GSC-G104 + } else { + httpError(w, "Only GET and POST is allowed!", http.StatusMethodNotAllowed) + } +} + +// WS /server/{id}/console +func consoleEndpoint(connector *Connector, w http.ResponseWriter, r *http.Request) { + // Check with authenticator. + ticket, ticketExists := connector.Tickets.LoadAndDelete(r.URL.Query().Get("ticket")) + user := "" + if ticketExists && ticket.IPAddr == GetIP(r) { + user = ticket.User + } else if user = connector.Validate(w, r); user == "" { + return + } + // Retrieve the token. + token := auth.GetTokenFromRequest(r) + if ticketExists { + token = ticket.Token + } + // Get the server being accessed. + id := mux.Vars(r)["id"] + process, exists := connector.Processes.Load(id) + // In case the server doesn't exist. + if !exists { + httpError(w, "This server does not exist!", http.StatusNotFound) + return + } + // Upgrade WebSocket connection. + c, err := connector.Upgrade(w, r, nil) + v2 := c.Subprotocol() == "console-v2" + if err == nil { + connector.Info("server.console.access", "ip", GetIP(r), "user", user, "server", id) + defer c.Close() + // Setup WebSocket limits. + timeout := 30 * time.Second + c.SetReadLimit(1024 * 1024) // Limit WebSocket reads to 1 MB. + // If v2, send settings and set read deadline. + if v2 { + c.SetReadDeadline(time.Now().Add(timeout)) + c.WriteJSON(struct { // skipcq GSC-G104 + Type string `json:"type"` + }{"settings"}) + } + // Use a channel to synchronise all writes to the WebSocket. + writeChannel := make(chan interface{}, 8) + defer close(writeChannel) + go (func() { + for { + data, ok := <-writeChannel + if !ok { + break + } else if data == nil { + c.Close() + break + } else if _, ok := connector.Authenticator.GetUsers().Load(user); !ok && r.RemoteAddr != "@" { + c.Close() + break + } + c.SetWriteDeadline(time.Now().Add(timeout)) // Set write deadline esp for v1 connections. + str, ok := data.(string) + if ok && v2 { + json, err := json.Marshal(struct { + Type string `json:"type"` + Data string `json:"data"` + }{"output", str}) + if err != nil { + log.Println("Error in "+process.Name+" console!", err) + } else { + c.WriteMessage(websocket.TextMessage, json) // skipcq GSC-G104 + } + } else if ok { + c.WriteMessage(websocket.TextMessage, []byte(str)) // skipcq GSC-G104 + } else { + c.WriteMessage(websocket.TextMessage, data.([]byte)) // skipcq GSC-G104 + } + } + })() + // Add connection to the process after sending current console output. + (func() { + process.ConsoleLock.RLock() + defer process.ConsoleLock.RUnlock() + writeChannel <- process.Console + process.Clients.Store(token, writeChannel) + })() + // Read messages from the user and execute them. + for { + client, _ := process.Clients.Load(token) + // Another client has connected with the same token. Terminate existing connection. + if client != writeChannel { + break + } + // Read messages from the user. + _, message, err := c.ReadMessage() + if err != nil { + process.Clients.Delete(token) + break // The WebSocket connection has terminated. + } else if _, ok := connector.Authenticator.GetUsers().Load(user); !ok && r.RemoteAddr != "@" { + process.Clients.Delete(token) + c.Close() + break + } + if v2 { + c.SetReadDeadline(time.Now().Add(timeout)) // Update read deadline. + var data map[string]string + err := json.Unmarshal(message, &data) + if err == nil { + if data["type"] == "input" && data["data"] != "" { + connector.Info("server.console.input", "ip", GetIP(r), "user", user, "server", id, + "input", data["data"]) + process.SendCommand(data["data"]) + } else if data["type"] == "ping" { + json, _ := json.Marshal(struct { // skipcq GSC-G104 + Type string `json:"type"` + ID string `json:"id"` + }{"pong", data["id"]}) + writeChannel <- json + } else { + json, _ := json.Marshal(struct { // skipcq GSC-G104 + Type string `json:"type"` + Message string `json:"message"` + }{"error", "Invalid message type: " + data["type"]}) + writeChannel <- json + } + } else { + json, _ := json.Marshal(struct { // skipcq GSC-G104 + Type string `json:"type"` + Message string `json:"message"` + }{"error", "Invalid message format"}) + writeChannel <- json + } + } else { + connector.Info("server.console.input", "ip", GetIP(r), "user", user, "server", id, + "input", string(message)) + process.SendCommand(string(message)) + } + } + } +} diff --git a/main.go b/main.go index 4d7c9f1..07874b7 100644 --- a/main.go +++ b/main.go @@ -32,8 +32,6 @@ var info *log.Logger var ConfigJsonPath = "config.json" var UsersJsonPath = "users.json" -// TODO: This is a real problem, perform some code refactors! -// skipcq GO-R1005 func main() { for _, arg := range os.Args { if arg == "--help" || arg == "-h" { @@ -118,41 +116,10 @@ func main() { return } if config.UnixSocket.Enabled { - loc := filepath.Join(os.TempDir(), "octyne.sock."+port[1:]) - if config.UnixSocket.Location != "" { - loc = config.UnixSocket.Location - } - err = os.RemoveAll(loc) - if err != nil { - log.Println("Error when unlinking Unix socket at "+loc+"!", err) - return - } - unixListener, err := net.Listen("unix", loc) // This unlinks the socket when closed by Serve(). + unixListener, err := ListenOnUnixSocket(port, config) if err != nil { - log.Println("Error when listening on Unix socket at "+loc+"!", err) return } - if config.UnixSocket.Group != "" { - if runtime.GOOS == "windows" { - log.Println("Error: Assigning Unix sockets to groups is not supported on Windows!") - return - } - group, err := user.LookupGroup(config.UnixSocket.Group) - if err != nil { - log.Println("Error when looking up Unix socket group owner: "+config.UnixSocket.Group, err) - return - } - gid, err := strconv.Atoi(group.Gid) - if err != nil { - log.Println("Error when getting Unix socket group owner '"+config.UnixSocket.Group+"' GID!", err) - return - } - err = os.Chown(loc, -1, gid) - if err != nil { - log.Println("Error when changing Unix socket group ownership to '"+config.UnixSocket.Group+"'!", err) - return - } - } go (func() { defer server.Close() // Close the TCP server if the Unix socket server fails. err = server.Serve(unixListener) @@ -182,3 +149,42 @@ func main() { log.Println("Error when serving HTTP requests!", err) // skipcq: GO-S0904 } } + +func ListenOnUnixSocket(port string, config Config) (net.Listener, error) { + loc := filepath.Join(os.TempDir(), "octyne.sock."+port[1:]) + if config.UnixSocket.Location != "" { + loc = config.UnixSocket.Location + } + err := os.RemoveAll(loc) + if err != nil { + log.Println("Error when unlinking Unix socket at "+loc+"!", err) + return nil, err + } + unixListener, err := net.Listen("unix", loc) // This unlinks the socket when closed by Serve(). + if err != nil { + log.Println("Error when listening on Unix socket at "+loc+"!", err) + return nil, err + } + if config.UnixSocket.Group != "" { + if runtime.GOOS == "windows" { + log.Println("Error: Assigning Unix sockets to groups is not supported on Windows!") + return nil, err + } + group, err := user.LookupGroup(config.UnixSocket.Group) + if err != nil { + log.Println("Error when looking up Unix socket group owner: "+config.UnixSocket.Group, err) + return nil, err + } + gid, err := strconv.Atoi(group.Gid) + if err != nil { + log.Println("Error when getting Unix socket group owner '"+config.UnixSocket.Group+"' GID!", err) + return nil, err + } + err = os.Chown(loc, -1, gid) + if err != nil { + log.Println("Error when changing Unix socket group ownership to '"+config.UnixSocket.Group+"'!", err) + return nil, err + } + } + return unixListener, nil +} diff --git a/system/tar.go b/system/tar.go index 3a554a9..61751a3 100644 --- a/system/tar.go +++ b/system/tar.go @@ -20,6 +20,8 @@ func ExtractTarFile(tarFile string, location string) error { return err } var reader io.Reader = file + // Select an appropriate decompression stream if necessary. + // Currently supported: gzip, bzip2, xz, zstd if strings.HasSuffix(tarFile, ".gz") || strings.HasSuffix(tarFile, ".tgz") { reader, err = gzip.NewReader(file) if err != nil { @@ -33,6 +35,8 @@ func ExtractTarFile(tarFile string, location string) error { } else if strings.HasSuffix(tarFile, ".zst") || strings.HasSuffix(tarFile, ".tzst") { reader = NativeCompressionReader(file, "zstd") } + + // Extract archive. archive := tar.NewReader(reader) for { header, err := archive.Next()