lots of api changes
use jsonapi to format responses add somewhat working websockets
This commit is contained in:
parent
31f4b465c1
commit
184d7e0afe
30
api/auth.go
30
api/auth.go
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
35
api/utils.go
35
api/utils.go
|
@ -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
76
api/websockets/client.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
api/websockets/consolewriter.go
Normal file
28
api/websockets/consolewriter.go
Normal 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
120
api/websockets/hub.go
Normal 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
59
api/websockets/message.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user