lots of api changes

use jsonapi to format responses
add somewhat working websockets
This commit is contained in:
Jakob Schrettenbrunner 2018-02-20 23:36:17 +01:00
parent 31f4b465c1
commit 184d7e0afe
8 changed files with 370 additions and 36 deletions

View File

@ -3,7 +3,10 @@ package api
import ( import (
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/jsonapi"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/control" "github.com/pterodactyl/wings/control"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -62,15 +65,23 @@ func (a *authorizationManager) HasPermission(permission string) bool {
// AuthHandler returns a HandlerFunc that checks request authentication // AuthHandler returns a HandlerFunc that checks request authentication
// permission is a permission string describing the required permission to access the route // permission is a permission string describing the required permission to access the route
//
// The AuthHandler looks for an access token header (defined in accessTokenHeader)
// or a `token` request parameter
func AuthHandler(permission string) gin.HandlerFunc { func AuthHandler(permission string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
requestToken := c.Request.Header.Get(accessTokenHeader) requestToken := c.Request.Header.Get(accessTokenHeader)
if requestToken == "" {
requestToken = c.Query("token")
}
requestServer := c.Param("server") requestServer := c.Param("server")
var server control.Server var server control.Server
if requestToken == "" && permission != "" { if requestToken == "" && permission != "" {
log.Debug("Token missing in request.") sendErrors(c, http.StatusUnauthorized, &jsonapi.ErrorObject{
c.JSON(http.StatusBadRequest, responseError{"Missing required " + accessTokenHeader + " header."}) Title: "Missing required " + accessTokenHeader + " header or token param.",
Status: strconv.Itoa(http.StatusUnauthorized),
})
c.Abort() c.Abort()
return return
} }
@ -90,7 +101,7 @@ func AuthHandler(permission string) gin.HandlerFunc {
return return
} }
c.JSON(http.StatusForbidden, responseError{"You do not have permission to perform this action."}) sendForbidden(c)
c.Abort() c.Abort()
} }
} }
@ -107,16 +118,3 @@ func GetContextAuthManager(c *gin.Context) AuthorizationManager {
} }
return nil return nil
} }
// GetContextServer returns a control.Server contained in a gin.Context
// or null
func GetContextServer(c *gin.Context) control.Server {
server, exists := c.Get(contextVarAuth)
if !exists {
return nil
}
if server, ok := server.(control.Server); ok {
return server
}
return nil
}

View File

@ -3,25 +3,29 @@ package api
import ( import (
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/jsonapi"
"github.com/pterodactyl/wings/control" "github.com/pterodactyl/wings/control"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// GET /servers // GET /servers
// TODO: make jsonapi compliant
func handleGetServers(c *gin.Context) { func handleGetServers(c *gin.Context) {
servers := control.GetServers() servers := control.GetServers()
c.JSON(http.StatusOK, servers) sendData(c, servers)
} }
// POST /servers // POST /servers
// TODO: make jsonapi compliant
func handlePostServers(c *gin.Context) { func handlePostServers(c *gin.Context) {
server := control.ServerStruct{} server := control.ServerStruct{}
if err := c.BindJSON(&server); err != nil { if err := c.BindJSON(&server); err != nil {
log.WithField("server", server).WithError(err).Error("Failed to parse server request.") log.WithField("server", server).WithError(err).Error("Failed to parse server request.")
c.Status(http.StatusBadRequest) sendErrors(c, http.StatusBadRequest, &jsonapi.ErrorObject{
Status: strconv.Itoa(http.StatusBadRequest),
Title: "The passed server object is invalid.",
})
return return
} }
var srv control.Server var srv control.Server
@ -30,10 +34,14 @@ func handlePostServers(c *gin.Context) {
if _, ok := err.(control.ErrServerExists); ok { if _, ok := err.(control.ErrServerExists); ok {
log.WithError(err).Error("Cannot create server, it already exists.") log.WithError(err).Error("Cannot create server, it already exists.")
c.Status(http.StatusBadRequest) c.Status(http.StatusBadRequest)
sendErrors(c, http.StatusConflict, &jsonapi.ErrorObject{
Status: strconv.Itoa(http.StatusConflict),
Title: "A server with this ID already exists.",
})
return return
} }
log.WithField("server", server).WithError(err).Error("Failed to create server.") log.WithField("server", server).WithError(err).Error("Failed to create server.")
c.Status(http.StatusInternalServerError) sendInternalError(c, "Failed to create the server", "")
return return
} }
go func() { go func() {
@ -43,19 +51,22 @@ func handlePostServers(c *gin.Context) {
} }
env.Create() env.Create()
}() }()
c.JSON(http.StatusOK, srv) sendDataStatus(c, http.StatusCreated, srv)
} }
// GET /servers/:server // GET /servers/:server
// TODO: make jsonapi compliant
func handleGetServer(c *gin.Context) { func handleGetServer(c *gin.Context) {
id := c.Param("server") id := c.Param("server")
server := control.GetServer(id) server := control.GetServer(id)
if server == nil { if server == nil {
c.Status(http.StatusNotFound) sendErrors(c, http.StatusNotFound, &jsonapi.ErrorObject{
Code: strconv.Itoa(http.StatusNotFound),
Title: "Server not found.",
Detail: "The requested Server with the id " + id + " couldn't be found.",
})
return return
} }
c.JSON(http.StatusOK, server) sendData(c, server)
} }
// PATCH /servers/:server // PATCH /servers/:server
@ -64,7 +75,6 @@ func handlePatchServer(c *gin.Context) {
} }
// DELETE /servers/:server // DELETE /servers/:server
// TODO: make jsonapi compliant
func handleDeleteServer(c *gin.Context) { func handleDeleteServer(c *gin.Context) {
id := c.Param("server") id := c.Param("server")
server := control.GetServer(id) server := control.GetServer(id)
@ -75,18 +85,21 @@ func handleDeleteServer(c *gin.Context) {
env, err := server.Environment() env, err := server.Environment()
if err != nil { if err != nil {
log.WithError(err).WithField("server", server).Error("Failed to delete server.") sendInternalError(c, "The server could not be deleted.", "")
return
} }
if err := env.Destroy(); err != nil { if err := env.Destroy(); err != nil {
log.WithError(err).Error("Failed to delete server, the environment couldn't be destroyed.") log.WithError(err).Error("Failed to delete server, the environment couldn't be destroyed.")
sendInternalError(c, "The server could not be deleted.", "The server environment couldn't be destroyed.")
return
} }
if err := control.DeleteServer(id); err != nil { if err := control.DeleteServer(id); err != nil {
log.WithError(err).Error("Failed to delete server.") log.WithError(err).Error("Failed to delete server.")
c.Status(http.StatusInternalServerError) sendInternalError(c, "The server could not be deleted.", "")
return return
} }
c.Status(http.StatusOK) c.Status(http.StatusNoContent)
} }
func handlePostServerReinstall(c *gin.Context) { func handlePostServerReinstall(c *gin.Context) {
@ -102,7 +115,6 @@ func handlePostServerRebuild(c *gin.Context) {
} }
// POST /servers/:server/power // POST /servers/:server/power
// TODO: make jsonapi compliant
func handlePostServerPower(c *gin.Context) { func handlePostServerPower(c *gin.Context) {
server := getServerFromContext(c) server := getServerFromContext(c)
if server == nil { if server == nil {
@ -112,7 +124,7 @@ func handlePostServerPower(c *gin.Context) {
auth := GetContextAuthManager(c) auth := GetContextAuthManager(c)
if auth == nil { if auth == nil {
c.Status(http.StatusInternalServerError) sendInternalError(c, "An internal error occured.", "")
return return
} }
@ -120,7 +132,7 @@ func handlePostServerPower(c *gin.Context) {
case "start": case "start":
{ {
if !auth.HasPermission("s:power:start") { if !auth.HasPermission("s:power:start") {
c.Status(http.StatusForbidden) sendForbidden(c)
return return
} }
server.Start() server.Start()
@ -128,7 +140,7 @@ func handlePostServerPower(c *gin.Context) {
case "stop": case "stop":
{ {
if !auth.HasPermission("s:power:stop") { if !auth.HasPermission("s:power:stop") {
c.Status(http.StatusForbidden) sendForbidden(c)
return return
} }
server.Stop() server.Stop()
@ -136,7 +148,7 @@ func handlePostServerPower(c *gin.Context) {
case "restart": case "restart":
{ {
if !auth.HasPermission("s:power:restart") { if !auth.HasPermission("s:power:restart") {
c.Status(http.StatusForbidden) sendForbidden(c)
return return
} }
server.Restart() server.Restart()
@ -144,7 +156,7 @@ func handlePostServerPower(c *gin.Context) {
case "kill": case "kill":
{ {
if !auth.HasPermission("s:power:kill") { if !auth.HasPermission("s:power:kill") {
c.Status(http.StatusForbidden) sendForbidden(c)
return return
} }
server.Kill() server.Kill()
@ -157,11 +169,16 @@ func handlePostServerPower(c *gin.Context) {
} }
// POST /servers/:server/command // POST /servers/:server/command
// TODO: make jsonapi compliant
func handlePostServerCommand(c *gin.Context) { func handlePostServerCommand(c *gin.Context) {
server := getServerFromContext(c) server := getServerFromContext(c)
cmd := c.Query("command") cmd := c.Query("command")
server.Exec(cmd) server.Exec(cmd)
c.Status(204)
}
func handleGetConsole(c *gin.Context) {
server := getServerFromContext(c)
server.Websockets().Upgrade(c.Writer, c.Request)
} }
func handleGetServerLog(c *gin.Context) { func handleGetServerLog(c *gin.Context) {

View File

@ -21,6 +21,7 @@ func (api *InternalAPI) RegisterRoutes() {
v1ServerRoutes.POST("/password", AuthHandler(""), handlePostServerPassword) v1ServerRoutes.POST("/password", AuthHandler(""), handlePostServerPassword)
v1ServerRoutes.POST("/power", AuthHandler("s:power"), handlePostServerPower) v1ServerRoutes.POST("/power", AuthHandler("s:power"), handlePostServerPower)
v1ServerRoutes.POST("/command", AuthHandler("s:command"), handlePostServerCommand) v1ServerRoutes.POST("/command", AuthHandler("s:command"), handlePostServerCommand)
v1ServerRoutes.GET("/console", AuthHandler("s:console"), handleGetConsole)
v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetServerLog) v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetServerLog)
v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend) v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend)
v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend) v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend)

View File

@ -1,10 +1,45 @@
package api package api
import ( import (
"net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/jsonapi"
"github.com/pterodactyl/wings/control" "github.com/pterodactyl/wings/control"
) )
func getServerFromContext(context *gin.Context) control.Server { func getServerFromContext(context *gin.Context) control.Server {
return control.GetServer(context.Param("server")) return control.GetServer(context.Param("server"))
} }
func sendErrors(c *gin.Context, s int, err ...*jsonapi.ErrorObject) {
c.Status(s)
c.Header("Content-Type", "application/json")
jsonapi.MarshalErrors(c.Writer, err)
}
func sendInternalError(c *gin.Context, title string, detail string) {
sendErrors(c, http.StatusInternalServerError, &jsonapi.ErrorObject{
Status: strconv.Itoa(http.StatusInternalServerError),
Title: title,
Detail: detail,
})
}
func sendForbidden(c *gin.Context) {
sendErrors(c, http.StatusForbidden, &jsonapi.ErrorObject{
Title: "The provided token has insufficient permissions to perform this action.",
Status: strconv.Itoa(http.StatusForbidden),
})
}
func sendData(c *gin.Context, payload interface{}) {
sendDataStatus(c, http.StatusOK, payload)
}
func sendDataStatus(c *gin.Context, status int, payload interface{}) {
c.Status(status)
c.Header("Content-Type", "application/json")
jsonapi.MarshalPayload(c.Writer, payload)
}

76
api/websockets/client.go Normal file
View File

@ -0,0 +1,76 @@
package websockets
import "github.com/gorilla/websocket"
import (
"time"
log "github.com/sirupsen/logrus"
)
type Client struct {
hub *Hub
socket *websocket.Conn
send chan []byte
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.socket.Close()
}()
c.socket.SetReadLimit(maxMessageSize)
c.socket.SetReadDeadline(time.Now().Add(pongWait))
c.socket.SetPongHandler(func(string) error {
c.socket.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, _, err := c.socket.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.WithError(err).Debug("Websocket closed unexpectedly.")
}
return
}
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.socket.Close()
}()
for {
select {
case m, ok := <-c.send:
c.socket.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel
c.socket.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.socket.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write([]byte{'['})
w.Write(m)
for i := 0; i < len(c.send)+1; i++ {
w.Write([]byte{','})
w.Write(<-c.send)
}
w.Write([]byte{']'})
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.socket.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.socket.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}

View File

@ -0,0 +1,28 @@
package websockets
import "io"
type ConsoleWriter struct {
Hub *Hub
HandlerFunc *func(string)
}
var _ io.Writer = ConsoleWriter{}
func (c ConsoleWriter) Write(b []byte) (n int, e error) {
line := make([]byte, len(b))
copy(line, b)
m := Message{
Type: MessageTypeConsole,
Payload: ConsolePayload{
Line: string(line),
Level: ConsoleLevelPlain,
Source: ConsoleSourceServer,
},
}
c.Hub.Broadcast <- m
if c.HandlerFunc != nil {
(*c.HandlerFunc)(string(line))
}
return len(b), nil
}

120
api/websockets/hub.go Normal file
View File

@ -0,0 +1,120 @@
package websockets
import (
"encoding/json"
"net/http"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = pongWait * 9 / 10
maxMessageSize = 512
)
var wsupgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type websocketMap map[*Client]bool
type Hub struct {
clients websocketMap
Broadcast chan Message
register chan *Client
unregister chan *Client
close chan bool
}
//var _ io.Writer = &Hub{}
func NewHub() *Hub {
return &Hub{
Broadcast: make(chan Message),
register: make(chan *Client),
unregister: make(chan *Client),
close: make(chan bool),
clients: make(websocketMap),
}
}
func (h *Hub) Upgrade(w http.ResponseWriter, r *http.Request) {
socket, err := wsupgrader.Upgrade(w, r, nil)
if err != nil {
log.WithError(err).Error("Failed to upgrade to websocket")
return
}
c := &Client{
hub: h,
socket: socket,
send: make(chan []byte, 256),
}
h.register <- c
go c.readPump()
go c.writePump()
}
func (h *Hub) Subscribe(c *Client) {
h.register <- c
}
func (h *Hub) Unsubscribe(c *Client) {
h.unregister <- c
}
func (h *Hub) Run() {
defer func() {
for s := range h.clients {
close(s.send)
delete(h.clients, s)
}
close(h.register)
close(h.unregister)
close(h.Broadcast)
close(h.close)
}()
for {
select {
case s := <-h.register:
h.clients[s] = true
case s := <-h.unregister:
if _, ok := h.clients[s]; ok {
delete(h.clients, s)
close(s.send)
}
case m := <-h.Broadcast:
b, err := json.Marshal(m)
if err != nil {
log.WithError(err).Error("Failed to encode websocket message.")
continue
}
for s := range h.clients {
select {
case s.send <- b:
default:
close(s.send)
delete(h.clients, s)
}
}
case <-h.close:
return
}
}
}
func (h *Hub) Close() {
h.close <- true
}

59
api/websockets/message.go Normal file
View File

@ -0,0 +1,59 @@
package websockets
type MessageType string
const (
MessageTypeProc MessageType = "proc"
MessageTypeConsole MessageType = "console"
MessageTypeStatus MessageType = "status"
)
// Message is a message that can be sent using a websocket in JSON format
type Message struct {
// Type is the type of a websocket message
Type MessageType `json:"type"`
// Payload is the payload of the message
// The payload needs to support encoding in JSON
Payload interface{} `json:"payload"`
}
type ProcPayload struct {
Memory int `json:"memory"`
CPUCores []int `json:"cpu_cores"`
CPUTotal int `json:"cpu_total"`
Disk int `json:"disk"`
}
type ConsoleSource string
type ConsoleLevel string
const (
ConsoleSourceWings ConsoleSource = "wings"
ConsoleSourceServer ConsoleSource = "server"
ConsoleLevelPlain ConsoleLevel = "plain"
ConsoleLevelInfo ConsoleLevel = "info"
ConsoleLevelWarn ConsoleLevel = "warn"
ConsoleLevelError ConsoleLevel = "error"
)
type ConsolePayload struct {
// Source is the source of the console line, either ConsoleSourceWings or ConsoleSourceServer
Source ConsoleSource `json:"source"`
// Level is the level of the message.
// Use one of plain, info, warn or error. If omitted the default is plain.
Level ConsoleLevel `json:"level,omitempty"`
// Line is the actual line to print to the console.
Line string `json:"line"`
}
func (h *Hub) Log(l ConsoleLevel, m string) {
h.Broadcast <- Message{
Type: MessageTypeConsole,
Payload: ConsolePayload{
Source: ConsoleSourceWings,
Level: l,
Line: m,
},
}
}