Port most of the HTTP code over to gin

This commit is contained in:
Dane Everitt
2020-04-05 18:00:33 -07:00
parent 223b9e05a1
commit cf2ef1a173
22 changed files with 1233 additions and 968 deletions

93
router/error.go Normal file
View File

@@ -0,0 +1,93 @@
package router
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"net/http"
"os"
)
type RequestError struct {
Err error
Uuid string
Message string
server *server.Server
}
// Generates a new tracked error, which simply tracks the specific error that
// is being passed in, and also assigned a UUID to the error so that it can be
// cross referenced in the logs.
func TrackedError(err error) *RequestError {
return &RequestError{
Err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "",
}
}
// Same as TrackedError, except this will also attach the server instance that
// generated this server for the purposes of logging.
func TrackedServerError(err error, s *server.Server) *RequestError {
return &RequestError{
Err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "",
server: s,
}
}
// Sets the output message to display to the user in the error.
func (e *RequestError) SetMessage(msg string) *RequestError {
e.Message = msg
return e
}
// Aborts the request with the given status code, and responds with the error. This
// will also include the error UUID in the output so that the user can report that
// and link the response to a specific error in the logs.
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
// If this error is because the resource does not exist, we likely do not need to log
// the error anywhere, just return a 404 and move on with our lives.
if os.IsNotExist(e.Err) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
// Otherwise, log the error to zap, and then report the error back to the user.
if status >= 500 {
if e.server != nil {
zap.S().Errorw("encountered error while handling HTTP request", zap.String("server", e.server.Uuid), zap.String("error_id", e.Uuid), zap.Error(e.Err))
} else {
zap.S().Errorw("encountered error while handling HTTP request", zap.String("error_id", e.Uuid), zap.Error(e.Err))
}
c.Error(errors.WithStack(e))
}
msg := "An unexpected error was encountered while processing this request."
if e.Message != "" {
msg = e.Message
}
c.AbortWithStatusJSON(status, gin.H{
"error": msg,
"error_id": e.Uuid,
})
}
// Helper function to just abort with an internal server error. This is generally the response
// from most errors encountered by the API.
func (e *RequestError) AbortWithServerError(c *gin.Context) {
e.AbortWithStatus(http.StatusInternalServerError, c)
}
// Format the error to a string and include the UUID.
func (e *RequestError) Error() string {
return fmt.Sprintf("%v (uuid: %s)", e.Err, e.Uuid)
}

67
router/middleware.go Normal file
View File

@@ -0,0 +1,67 @@
package router
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"net/http"
"strings"
)
// Set the access request control headers on all of the requests.
func SetAccessControlHeaders(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation)
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
c.Next()
}
// Authenticates the request token against the given permission string, ensuring that
// if it is a server permission, the token has control over that server. If it is a global
// token, this will ensure that the request is using a properly signed global token.
func AuthorizationMiddleware(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "The required authorization heads were not present in the request.",
})
return
}
// Try to match the request against the global token for the Daemon, regardless
// of the permission type. If nothing is matched we will fall through to the Panel
// API to try and validate permissions for a server.
if auth[1] == config.Get().AuthenticationToken {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "You are not authorized to access this endpoint.",
})
}
// Helper function to fetch a server out of the servers collection stored in memory.
func GetServer (uuid string) *server.Server {
return server.GetServers().Find(func(s *server.Server) bool {
return uuid == s.Uuid
})
}
// Ensure that the requested server exists in this setup. Returns a 404 if we cannot
// locate it.
func ServerExists(c *gin.Context) {
u, err := uuid.Parse(c.Param("server"))
if err != nil || GetServer(u.String()) == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested server does not exist.",
})
return
}
c.Next()
}

50
router/router.go Normal file
View File

@@ -0,0 +1,50 @@
package router
import "github.com/gin-gonic/gin"
// Configures the routing infrastructure for this daemon instance.
func Configure() *gin.Engine {
router := gin.Default()
router.Use(SetAccessControlHeaders)
// This route is special is sits above all of the other requests because we are
// using a JWT to authorize access to it, therefore it needs to be publically
// accessible.
router.GET("/api/servers/:server/ws", getServerWebsocket)
// All of the routes beyond this mount will use an authorization middleware
// and will not be accessible without the correct Authorization header provided.
protected := router.Use(AuthorizationMiddleware)
protected.GET("/api/system", getSystemInformation)
protected.GET("/api/servers", getAllServers)
protected.POST("/api/servers", postCreateServer)
// These are server specific routes, and require that the request be authorized, and
// that the server exist on the Daemon.
server := router.Group("/api/servers/:server")
server.Use(AuthorizationMiddleware, ServerExists)
{
server.GET("", getServer)
server.PATCH("", patchServer)
server.DELETE("", deleteServer)
server.GET("/logs", getServerLogs)
server.POST("/power", postServerPower)
server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall)
server.POST("/reinstall", postServerReinstall)
files := server.Group("/files")
{
files.GET("/contents", getServerFileContents)
files.GET("/list-directory", getServerListDirectory)
files.PUT("/rename", putServerRenameFile)
files.POST("/copy", postServerCopyFile)
files.POST("/write", postServerWriteFile)
files.POST("/create-directory", postServerCreateDirectory)
files.POST("/delete", postServerDeleteFile)
}
}
return router
}

View File

@@ -0,0 +1 @@
package router

216
router/router_server.go Normal file
View File

@@ -0,0 +1,216 @@
package router
import (
"bytes"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"net/http"
"os"
"strconv"
)
// Returns a single server from the collection of servers.
func getServer(c *gin.Context) {
c.JSON(http.StatusOK, GetServer(c.Param("server")))
}
// Returns the logs for a given server instance.
func getServerLogs(c *gin.Context) {
s := GetServer(c.Param("server"))
l, _ := strconv.ParseInt(c.DefaultQuery("size", "8192"), 10, 64)
if l <= 0 {
l = 2048
}
out, err := s.ReadLogfile(l)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
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 server.PowerAction
c.BindJSON(&data)
if !data.IsValid() {
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 == "start" || data.Action == "restart") && s.Suspended {
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 seperate thread to handle so that
// we can immediately return a response from the server. Some of these actions
// can take quite some time, especially stopping or restarting.
go func() {
if err := s.HandlePowerAction(data); err != nil {
zap.S().Errorw(
"encountered an error processing a server power action",
zap.String("server", s.Uuid),
zap.Error(err),
)
}
}()
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 {
TrackedServerError(err, s).AbortWithServerError(c)
return
} else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
"error": "Cannot send commands to a stopped server instance.",
})
return
}
var data struct{ Commands []string `json:"commands"` }
c.BindJSON(&data)
for _, command := range data.Commands {
if err := s.Environment.SendCommand(command); err != nil {
zap.S().Warnw(
"failed to send command to server",
zap.String("server", s.Uuid),
zap.String("command", command),
zap.Error(err),
)
}
}
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(), true); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Performs a server installation in a backgrounded thread.
func postServerInstall(c *gin.Context) {
s := GetServer(c.Param("server"))
go func(serv *server.Server) {
if err := serv.Install(); err != nil {
zap.S().Errorw(
"failed to execute server installation process",
zap.String("server", serv.Uuid),
zap.Error(err),
)
}
}(s)
c.Status(http.StatusAccepted)
}
// Reinstalls a server.
func postServerReinstall(c *gin.Context) {
s := GetServer(c.Param("server"))
go func(serv *server.Server) {
if err := serv.Reinstall(); err != nil {
zap.S().Errorw(
"failed to complete server reinstall process",
zap.String("server", serv.Uuid),
zap.Error(err),
)
}
}(s)
c.Status(http.StatusAccepted)
}
// Deletes a server from the wings daemon and deassociates its objects.
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.Suspended = true
// 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 {
zap.S().Warnw("failed to delete server archive during deletion process", zap.String("server", s.Uuid), zap.Error(err))
}
// 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 {
TrackedServerError(err, s).AbortWithServerError(c)
}
// 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 {
zap.S().Warnw("failed to remove server files during deletion process", zap.String("path", p), zap.Error(errors.WithStack(err)))
}
}(s.Filesystem.Path())
var uuid = s.Uuid
server.GetServers().Remove(func(s2 *server.Server) bool {
return s2.Uuid == uuid
})
// Deallocate the reference to this server.
s = nil
// Remove the configuration file stored on the Daemon for this server.
go func(u string) {
if err := os.Remove("data/servers/" + u + ".yml"); err != nil {
zap.S().Warnw("failed to delete server configuration file while processing deletion request", zap.String("server", u), zap.Error(errors.WithStack(err)))
}
}(uuid)
c.Status(http.StatusNoContent)
}

View File

@@ -0,0 +1,155 @@
package router
import (
"bufio"
"github.com/gin-gonic/gin"
"net/http"
"os"
"strconv"
)
// Returns the contents of a file on the server.
func getServerFileContents(c *gin.Context) {
s := GetServer(c.Param("server"))
cleaned, err := s.Filesystem.SafePath(c.Query("file"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The file requested could not be found.",
})
return
}
st, err := s.Filesystem.Stat(cleaned)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
if st.Info.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
f, err := os.Open(cleaned)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
defer f.Close()
c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
// If a download parameter is included in the URL go ahead and attach the necessary headers
// so that the file can be downloaded.
if c.Query("download") != "" {
c.Header("Content-Disposition", "attachment; filename="+st.Info.Name())
c.Header("Content-Type", "application/octet-stream")
}
bufio.NewReader(f).WriteTo(c.Writer)
}
// Returns the contents of a directory for a server.
func getServerListDirectory(c *gin.Context) {
s := GetServer(c.Param("server"))
stats, err := s.Filesystem.ListDirectory(c.Query("directory"))
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.JSON(http.StatusOK, stats)
}
// Renames (or moves) a file for a server.
func putServerRenameFile(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct{
RenameFrom string `json:"rename_from"`
RenameTo string `json:"rename_to"`
}
c.BindJSON(&data)
if data.RenameFrom == "" || data.RenameTo == "" {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "Invalid paths were provided, did you forget to provide both a new and old path?",
})
return
}
if err := s.Filesystem.Rename(data.RenameFrom, data.RenameTo); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Copies a server file.
func postServerCopyFile(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Location string `json:"location"`
}
c.BindJSON(&data)
if err := s.Filesystem.Copy(data.Location); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Deletes a server file.
func postServerDeleteFile(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Location string `json:"location"`
}
c.BindJSON(&data)
if err := s.Filesystem.Delete(data.Location); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Writes the contents of the request to a file on a server.
func postServerWriteFile(c *gin.Context) {
s := GetServer(c.Param("server"))
if err := s.Filesystem.Writefile(c.Query("file"), c.Request.Body); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Create a directory on a server.
func postServerCreateDirectory(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Name string `json:"name"`
Path string `json:"path"`
}
c.BindJSON(&data)
if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}

View File

@@ -0,0 +1,60 @@
package router
import (
"context"
"encoding/json"
"github.com/gin-gonic/gin"
ws "github.com/gorilla/websocket"
"github.com/pterodactyl/wings/router/websocket"
"go.uber.org/zap"
)
// Upgrades a connection to a websocket and passes events along between.
func getServerWebsocket(c *gin.Context) {
s := GetServer(c.Param("server"))
handler, err := websocket.GetHandler(s, c.Writer, c.Request)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
defer handler.Connection.Close()
// Create a context that can be canceled when the user disconnects from this
// socket that will also cancel listeners running in seperate threads.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go handler.ListenForServerEvents(ctx)
go handler.ListenForExpiration(ctx)
for {
j := websocket.Message{}
_, p, err := handler.Connection.ReadMessage()
if err != nil {
if !ws.IsCloseError(
err,
ws.CloseNormalClosure,
ws.CloseGoingAway,
ws.CloseNoStatusReceived,
ws.CloseServiceRestart,
ws.CloseAbnormalClosure,
) {
zap.S().Warnw("error handling websocket message", zap.Error(err))
}
break
}
// Discard and JSON parse errors into the void and don't continue processing this
// specific socket request. If we did a break here the client would get disconnected
// from the socket, which is NOT what we want to do.
if err := json.Unmarshal(p, &j); err != nil {
continue
}
if err := handler.HandleInbound(j); err != nil {
handler.SendErrorJson(err)
}
}
}

64
router/router_system.go Normal file
View File

@@ -0,0 +1,64 @@
package router
import (
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"go.uber.org/zap"
"net/http"
)
// Returns information about the system that wings is running on.
func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation()
if err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
c.JSON(http.StatusOK, i)
}
// Returns all of the servers that are registered and configured correctly on
// this wings instance.
func getAllServers(c *gin.Context) {
c.JSON(http.StatusOK, server.GetServers().All())
}
// Creates a new server on the wings daemon and begins the installation process
// for it.
func postCreateServer(c *gin.Context) {
var data []byte
c.Bind(&data)
install, err := installer.New(data)
if err != nil {
TrackedError(err).
SetMessage("Failed to validate the data provided in the request.").
AbortWithStatus(http.StatusUnprocessableEntity, c)
return
}
// Plop that server instance onto the request so that it can be referenced in
// requests from here-on out.
server.GetServers().Add(install.Server())
// Begin the installation process in the background to not block the request
// cycle. If there are any errors they will be logged and communicated back
// to the Panel where a reinstall may take place.
go func(i *installer.Installer) {
i.Execute()
if err := i.Server().Install(); err != nil {
zap.S().Errorw(
"failed to run install process for server",
zap.String("server", i.Uuid()),
zap.Error(err),
)
}
}(install)
c.Status(http.StatusAccepted)
}

View File

@@ -0,0 +1,76 @@
package websocket
import (
"context"
"github.com/pterodactyl/wings/server"
"time"
)
// Checks the time to expiration on the JWT every 30 seconds until the token has
// expired. If we are within 3 minutes of the token expiring, send a notice over
// the socket that it is expiring soon. If it has expired, send that notice as well.
func (h *Handler) ListenForExpiration(ctx context.Context) {
// Make a ticker and completion channel that is used to continuously poll the
// JWT stored in the session to send events to the socket when it is expiring.
ticker := time.NewTicker(time.Second * 30)
done := make(chan bool)
// Whenever this function is complete, end the ticker, close out the channel,
// and then close the websocket connection.
defer func() {
ticker.Stop()
done <- true
}()
for {
select {
case <-ctx.Done():
case <-done:
return
case <-ticker.C:
{
if h.JWT != nil {
if h.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
h.SendJson(&Message{Event: TokenExpiredEvent})
} else if h.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 180 {
h.SendJson(&Message{Event: TokenExpiringEvent})
}
}
}
}
}
}
// Listens for different events happening on a server and sends them along
// to the connected websocket.
func (h *Handler) ListenForServerEvents(ctx context.Context) {
events := []string{
server.StatsEvent,
server.StatusEvent,
server.ConsoleOutputEvent,
server.InstallOutputEvent,
server.DaemonMessageEvent,
}
eventChannel := make(chan server.Event)
for _, event := range events {
h.server.Events().Subscribe(event, eventChannel)
}
select {
case <-ctx.Done():
for _, event := range events {
h.server.Events().Unsubscribe(event, eventChannel)
}
close(eventChannel)
default:
// Listen for different events emitted by the server and respond to them appropriately.
for d := range eventChannel {
h.SendJson(&Message{
Event: d.Topic,
Args: []string{d.Data},
})
}
}
}

View File

@@ -0,0 +1,26 @@
package websocket
const (
AuthenticationSuccessEvent = "auth success"
TokenExpiringEvent = "token expiring"
TokenExpiredEvent = "token expired"
AuthenticationEvent = "auth"
SetStateEvent = "set state"
SendServerLogsEvent = "send logs"
SendCommandEvent = "send command"
ErrorEvent = "daemon error"
)
type Message struct {
// The event to perform. Should be one of the following that are supported:
//
// - status : Returns the server's power state.
// - logs : Returns the server log data at the time of the request.
// - power : Performs a power action aganist the server based the data.
// - command : Performs a command on a server using the data field.
Event string `json:"event"`
// The data to pass along, only used by power/command currently. Other requests
// should either omit the field or pass an empty value as it is ignored.
Args []string `json:"args,omitempty"`
}

View File

@@ -0,0 +1,24 @@
package websocket
import (
"encoding/json"
"github.com/gbrlsnchs/jwt/v3"
)
type TokenPayload struct {
jwt.Payload
UserID json.Number `json:"user_id"`
ServerUUID string `json:"server_uuid"`
Permissions []string `json:"permissions"`
}
// Checks if the given token payload has a permission string.
func (p *TokenPayload) HasPermission(permission string) bool {
for _, k := range p.Permissions {
if k == permission {
return true
}
}
return false
}

View File

@@ -0,0 +1,277 @@
package websocket
import (
"fmt"
"github.com/gbrlsnchs/jwt/v3"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"net/http"
"os"
"strings"
"sync"
"time"
)
var alg *jwt.HMACSHA
const (
PermissionConnect = "connect"
PermissionSendCommand = "send-command"
PermissionSendPower = "send-power"
PermissionReceiveErrors = "receive-errors"
PermissionReceiveInstall = "receive-install"
)
type Handler struct {
Connection *websocket.Conn
JWT *TokenPayload `json:"-"`
server *server.Server
mutex sync.Mutex
}
// Returns a new websocket handler using the context provided.
func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) {
upgrader := websocket.Upgrader{
// Ensure that the websocket request is originating from the Panel itself,
// and not some other location.
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == config.Get().PanelLocation
},
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}
return &Handler{
Connection: conn,
JWT: nil,
server: s,
mutex: sync.Mutex{},
}, nil
}
// Validates the provided JWT against the known secret for the Daemon and returns the
// parsed data.
//
// This function DOES NOT validate that the token is valid for the connected server, nor
// does it ensure that the user providing the token is able to actually do things.
func ParseJWT(token []byte) (*TokenPayload, error) {
var payload TokenPayload
if alg == nil {
alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken))
}
now := time.Now()
verifyOptions := jwt.ValidatePayload(
&payload.Payload,
jwt.ExpirationTimeValidator(now),
)
_, err := jwt.Verify(token, alg, &payload, verifyOptions)
if err != nil {
return nil, err
}
if !payload.HasPermission(PermissionConnect) {
return nil, errors.New("not authorized to connect to this socket")
}
return &payload, nil
}
func (h *Handler) SendJson(v *Message) error {
// Do not send JSON down the line if the JWT on the connection is not
// valid!
if err := h.TokenValid(); err != nil {
return nil
}
// If we're sending installation output but the user does not have the required
// permissions to see the output, don't send it down the line.
if v.Event == server.InstallOutputEvent {
zap.S().Debugf("%+v", v.Args)
if h.JWT != nil && !h.JWT.HasPermission(PermissionReceiveInstall) {
return nil
}
}
return h.unsafeSendJson(v)
}
// Sends JSON over the websocket connection, ignoring the authentication state of the
// socket user. Do not call this directly unless you are positive a response should be
// sent back to the client!
func (h *Handler) unsafeSendJson(v interface{}) error {
h.mutex.Lock()
defer h.mutex.Unlock()
return h.Connection.WriteJSON(v)
}
// Checks if the JWT is still valid.
func (h *Handler) TokenValid() error {
if h.JWT == nil {
return errors.New("no jwt present")
}
if err := jwt.ExpirationTimeValidator(time.Now())(&h.JWT.Payload); err != nil {
return err
}
if !h.JWT.HasPermission(PermissionConnect) {
return errors.New("jwt does not have connect permission")
}
if h.server.Uuid != h.JWT.ServerUUID {
return errors.New("jwt server uuid mismatch")
}
return nil
}
// Sends an error back to the connected websocket instance by checking the permissions
// of the token. If the user has the "receive-errors" grant we will send back the actual
// error message, otherwise we just send back a standard error message.
func (h *Handler) SendErrorJson(err error) error {
h.mutex.Lock()
defer h.mutex.Unlock()
message := "an unexpected error was encountered while handling this request"
if h.JWT != nil {
if server.IsSuspendedError(err) || h.JWT.HasPermission(PermissionReceiveErrors) {
message = err.Error()
}
}
m, u := h.GetErrorMessage(message)
wsm := Message{Event: ErrorEvent}
wsm.Args = []string{m}
if !server.IsSuspendedError(err) {
zap.S().Errorw(
"an error was encountered in the websocket process",
zap.String("server", h.server.Uuid),
zap.String("error_identifier", u.String()),
zap.Error(err),
)
}
return h.Connection.WriteJSON(wsm)
}
// Converts an error message into a more readable representation and returns a UUID
// that can be cross-referenced to find the specific error that triggered.
func (h *Handler) GetErrorMessage(msg string) (string, uuid.UUID) {
u := uuid.Must(uuid.NewRandom())
m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg)
return m, u
}
// Handle the inbound socket request and route it to the proper server action.
func (h *Handler) HandleInbound(m Message) error {
if m.Event != AuthenticationEvent {
if err := h.TokenValid(); err != nil {
zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error()))
h.unsafeSendJson(Message{
Event: ErrorEvent,
Args: []string{"could not authenticate client: " + err.Error()},
})
return nil
}
}
switch m.Event {
case AuthenticationEvent:
{
token, err := ParseJWT([]byte(strings.Join(m.Args, "")))
if err != nil {
return err
}
if token.HasPermission(PermissionConnect) {
h.JWT = token
}
// On every authentication event, send the current server status back
// to the client. :)
h.server.Events().Publish(server.StatusEvent, h.server.State)
h.unsafeSendJson(Message{
Event: AuthenticationSuccessEvent,
Args: []string{},
})
return nil
}
case SetStateEvent:
{
if !h.JWT.HasPermission(PermissionSendPower) {
return nil
}
switch strings.Join(m.Args, "") {
case "start":
return h.server.Environment.Start()
case "stop":
return h.server.Environment.Stop()
case "restart":
{
if err := h.server.Environment.WaitForStop(60, false); err != nil {
return err
}
return h.server.Environment.Start()
}
case "kill":
return h.server.Environment.Terminate(os.Kill)
}
return nil
}
case SendServerLogsEvent:
{
if running, _ := h.server.Environment.IsRunning(); !running {
return nil
}
logs, err := h.server.Environment.Readlog(1024 * 16)
if err != nil {
return err
}
for _, line := range logs {
h.SendJson(&Message{
Event: server.ConsoleOutputEvent,
Args: []string{line},
})
}
return nil
}
case SendCommandEvent:
{
if !h.JWT.HasPermission(PermissionSendCommand) {
return nil
}
if h.server.State == server.ProcessOfflineState {
return nil
}
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
}
}
return nil
}