wings/router/router_server.go

264 lines
7.5 KiB
Go
Raw Normal View History

2020-04-06 01:00:33 +00:00
package router
import (
"bytes"
"context"
"emperror.dev/errors"
2020-06-13 17:26:35 +00:00
"github.com/apex/log"
2020-04-06 01:00:33 +00:00
"github.com/gin-gonic/gin"
2020-11-04 05:01:50 +00:00
"github.com/pterodactyl/wings/router/tokens"
2020-04-06 01:00:33 +00:00
"github.com/pterodactyl/wings/server"
"net/http"
"os"
"strconv"
)
type serverProcData struct {
server.ResourceUsage
Suspended bool `json:"suspended"`
}
2020-04-06 01:00:33 +00:00
// Returns a single server from the collection of servers.
func getServer(c *gin.Context) {
s := GetServer(c.Param("server"))
c.JSON(http.StatusOK, serverProcData{
ResourceUsage: *s.Proc(),
Suspended: s.IsSuspended(),
})
2020-04-06 01:00:33 +00:00
}
// Returns the logs for a given server instance.
func getServerLogs(c *gin.Context) {
s := GetServer(c.Param("server"))
l, _ := strconv.Atoi(c.DefaultQuery("size", "100"))
2020-04-06 01:00:33 +00:00
if l <= 0 {
l = 100
} else if l > 100 {
l = 100
2020-04-06 01:00:33 +00:00
}
out, err := s.ReadLogfile(l)
if err != nil {
2020-12-16 05:08:00 +00:00
NewServerError(err, s).Abort(c)
2020-04-06 01:00:33 +00:00
return
}
c.JSON(http.StatusOK, gin.H{"data": out})
}
// Handles a request to control the power state of a server. If the action being passed
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
// and the actual power action is run asynchronously so that we don't have to block the
// request until a potentially slow operation completes.
//
// This is done because for the most part the Panel is using websockets to determine when
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
// just see over the socket if something isn't working correctly.
func postServerPower(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Action server.PowerAction `json:"action"`
}
2020-05-29 15:44:49 +00:00
if err := c.BindJSON(&data); err != nil {
return
}
2020-04-06 01:00:33 +00:00
if !data.Action.IsValid() {
2020-04-06 01:00:33 +00:00
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
})
return
}
// Because we route all of the actual bootup process to a separate thread we need to
// check the suspension status here, otherwise the user will hit the endpoint and then
// just sit there wondering why it returns a success but nothing actually happens.
//
// We don't really care about any of the other actions at this point, they'll all result
// in the process being stopped, which should have happened anyways if the server is suspended.
if (data.Action == server.PowerActionStart || data.Action == server.PowerActionRestart) && s.IsSuspended() {
2020-04-06 01:00:33 +00:00
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot start or restart a server that is suspended.",
})
return
}
// Pass the actual heavy processing off to a separate thread to handle so that
2020-04-06 01:00:33 +00:00
// we can immediately return a response from the server. Some of these actions
// can take quite some time, especially stopping or restarting.
go func(s *server.Server) {
if err := s.HandlePowerAction(data.Action, 30); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
s.Log().WithField("action", data.Action).
Warn("could not acquire a lock while attempting to perform a power action")
} else {
s.Log().WithFields(log.Fields{"action": data, "error": err}).
Error("encountered error processing a server power action in the background")
}
2020-04-06 01:00:33 +00:00
}
2020-06-13 17:26:35 +00:00
}(s)
2020-04-06 01:00:33 +00:00
c.Status(http.StatusAccepted)
}
// Sends an array of commands to a running server instance.
func postServerCommands(c *gin.Context) {
s := GetServer(c.Param("server"))
if running, err := s.Environment.IsRunning(); err != nil {
2020-12-16 05:08:00 +00:00
NewServerError(err, s).Abort(c)
2020-04-06 01:00:33 +00:00
return
} else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
"error": "Cannot send commands to a stopped server instance.",
})
return
}
2020-05-29 15:44:49 +00:00
var data struct {
Commands []string `json:"commands"`
}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
2020-04-06 01:00:33 +00:00
for _, command := range data.Commands {
if err := s.Environment.SendCommand(command); err != nil {
2020-06-13 17:26:35 +00:00
s.Log().WithFields(log.Fields{"command": command, "error": err}).Warn("failed to send command to server instance")
2020-04-06 01:00:33 +00:00
}
}
c.Status(http.StatusNoContent)
}
// Updates information about a server internally.
func patchServer(c *gin.Context) {
s := GetServer(c.Param("server"))
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
2020-12-16 05:08:00 +00:00
NewServerError(err, s).Abort(c)
2020-04-06 01:00:33 +00:00
return
}
s.SyncWithEnvironment()
2020-04-06 01:00:33 +00:00
c.Status(http.StatusNoContent)
}
// Performs a server installation in a background thread.
2020-04-06 01:00:33 +00:00
func postServerInstall(c *gin.Context) {
s := GetServer(c.Param("server"))
go func(serv *server.Server) {
if err := serv.Install(true); err != nil {
serv.Log().WithField("error", err).Error("failed to execute server installation process")
2020-04-06 01:00:33 +00:00
}
}(s)
c.Status(http.StatusAccepted)
}
// Reinstalls a server.
func postServerReinstall(c *gin.Context) {
s := GetServer(c.Param("server"))
if s.ExecutingPowerAction() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Cannot execute server reinstall event while another power action is running.",
})
return
}
go func(s *server.Server) {
if err := s.Reinstall(); err != nil {
s.Log().WithField("error", err).Error("failed to complete server re-install process")
2020-04-06 01:00:33 +00:00
}
}(s)
c.Status(http.StatusAccepted)
}
// Deletes a server from the wings daemon and dissociate it's objects.
2020-04-06 01:00:33 +00:00
func deleteServer(c *gin.Context) {
s := GetServer(c.Param("server"))
// Immediately suspend the server to prevent a user from attempting
// to start it while this process is running.
s.Config().SetSuspended(true)
2020-04-06 01:00:33 +00:00
// If the server is currently installing, abort it.
if s.IsInstalling() {
s.AbortInstallation()
}
2020-04-06 01:00:33 +00:00
// Delete the server's archive if it exists. We intentionally don't return
// here, if the archive fails to delete, the server can still be removed.
if err := s.Archiver.DeleteIfExists(); err != nil {
s.Log().WithField("error", err).Warn("failed to delete server archive during deletion process")
2020-04-06 01:00:33 +00:00
}
// Unsubscribe all of the event listeners.
s.Events().Destroy()
s.Throttler().StopTimer()
s.Websockets().CancelAll()
2020-04-06 01:00:33 +00:00
// Destroy the environment; in Docker this will handle a running container and
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
if err := s.Environment.Destroy(); err != nil {
2020-12-16 05:08:00 +00:00
NewServerError(err, s).Abort(c)
2020-04-06 01:00:33 +00:00
}
// Once the environment is terminated, remove the server files from the system. This is
// done in a separate process since failure is not the end of the world and can be
// manually cleaned up after the fact.
//
// In addition, servers with large amounts of files can take some time to finish deleting
// so we don't want to block the HTTP call while waiting on this.
go func(p string) {
if err := os.RemoveAll(p); err != nil {
log.WithFields(log.Fields{
"path": p,
"error": err,
}).Warn("failed to remove server files during deletion process")
2020-04-06 01:00:33 +00:00
}
}(s.Filesystem().Path())
2020-04-06 01:00:33 +00:00
2020-07-20 00:53:41 +00:00
var uuid = s.Id()
2020-04-06 01:00:33 +00:00
server.GetServers().Remove(func(s2 *server.Server) bool {
2020-07-20 00:53:41 +00:00
return s2.Id() == uuid
2020-04-06 01:00:33 +00:00
})
// Deallocate the reference to this server.
s = nil
c.Status(http.StatusNoContent)
}
2020-11-04 05:01:50 +00:00
// Adds any of the JTIs passed through in the body to the deny list for the websocket
// preventing any JWT generated before the current time from being used to connect to
// the socket or send along commands.
func postServerDenyWSTokens(c *gin.Context) {
var data struct {
JTIs []string `json:"jtis"`
}
2020-11-04 05:01:50 +00:00
if err := c.BindJSON(&data); err != nil {
return
}
for _, jti := range data.JTIs {
tokens.DenyJTI(jti)
}
c.Status(http.StatusNoContent)
}