Remove initial iterations of code for now, will ref it later
This commit is contained in:
@@ -1 +0,0 @@
|
||||
authKey: 'existingkey'
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"uuid": "existingserver",
|
||||
"keys": {
|
||||
"existinggkey": [
|
||||
"g:*"
|
||||
],
|
||||
"existingspecificskey": [
|
||||
"s:test"
|
||||
],
|
||||
"existingglobalskey": [
|
||||
"s:*"
|
||||
]
|
||||
}
|
||||
}
|
||||
95
api/api.go
95
api/api.go
@@ -1,95 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type InternalAPI struct {
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
// Configure the API and begin listening on the configured IP and Port.
|
||||
func (api *InternalAPI) Listen() {
|
||||
if !viper.GetBool(config.Debug) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
api.router = gin.Default()
|
||||
api.router.RedirectTrailingSlash = false
|
||||
|
||||
// Setup Access-Control origin headers. Down the road once this is closer to
|
||||
// release we should setup this header properly and lock it down to the domain
|
||||
// used to run the Panel.
|
||||
api.router.Use(func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
})
|
||||
|
||||
api.router.OPTIONS("/", func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization")
|
||||
})
|
||||
|
||||
// Register all of the API route bindings.
|
||||
api.register()
|
||||
|
||||
l := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort))
|
||||
api.router.Run(l)
|
||||
|
||||
logrus.Info("API Server is now listening on %s", l)
|
||||
}
|
||||
|
||||
// Register routes for v1 of the API. This API should be fully backwards compatable with
|
||||
// the existing Nodejs Daemon API.
|
||||
//
|
||||
// Routes that are not yet completed are commented out. Routes are grouped where possible
|
||||
// to keep this function organized.
|
||||
func (api *InternalAPI) register() {
|
||||
v1 := api.router.Group("/api/v1")
|
||||
{
|
||||
v1.GET("", AuthHandler(""), GetIndex)
|
||||
//v1.PATCH("/config", AuthHandler("c:config"), PatchConfiguration)
|
||||
|
||||
v1.GET("/servers", AuthHandler("c:list"), handleGetServers)
|
||||
v1.POST("/servers", AuthHandler("c:create"), handlePostServers)
|
||||
|
||||
v1ServerRoutes := v1.Group("/servers/:server")
|
||||
{
|
||||
v1ServerRoutes.GET("", AuthHandler("s:get"), handleGetServer)
|
||||
v1ServerRoutes.PATCH("", AuthHandler("s:config"), handlePatchServer)
|
||||
v1ServerRoutes.DELETE("", AuthHandler("g:server:delete"), handleDeleteServer)
|
||||
v1ServerRoutes.POST("/reinstall", AuthHandler("s:install-server"), handlePostServerReinstall)
|
||||
v1ServerRoutes.POST("/rebuild", AuthHandler("g:server:rebuild"), handlePostServerRebuild)
|
||||
v1ServerRoutes.POST("/password", AuthHandler(""), handlePostServerPassword)
|
||||
v1ServerRoutes.POST("/power", AuthHandler("s:power"), handlePostServerPower)
|
||||
v1ServerRoutes.POST("/command", AuthHandler("s:command"), handlePostServerCommand)
|
||||
v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetConsole)
|
||||
v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend)
|
||||
v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend)
|
||||
}
|
||||
|
||||
//v1ServerFileRoutes := v1.Group("/servers/:server/files")
|
||||
//{
|
||||
// v1ServerFileRoutes.GET("/file/:file", AuthHandler("s:files:read"), handleGetFile)
|
||||
// v1ServerFileRoutes.GET("/stat/:file", AuthHandler("s:files:"), handleGetFileStat)
|
||||
// v1ServerFileRoutes.GET("/dir/:directory", AuthHandler("s:files:get"), handleGetDirectory)
|
||||
//
|
||||
// v1ServerFileRoutes.POST("/dir/:directory", AuthHandler("s:files:create"), handlePostFilesFolder)
|
||||
// v1ServerFileRoutes.POST("/file/:file", AuthHandler("s:files:post"), handlePostFile)
|
||||
//
|
||||
// v1ServerFileRoutes.POST("/copy/:file", AuthHandler("s:files:copy"), handlePostFileCopy)
|
||||
// v1ServerFileRoutes.POST("/move/:file", AuthHandler("s:files:move"), handlePostFileMove)
|
||||
// v1ServerFileRoutes.POST("/rename/:file", AuthHandler("s:files:move"), handlePostFileMove)
|
||||
// v1ServerFileRoutes.POST("/compress/:file", AuthHandler("s:files:compress"), handlePostFileCompress)
|
||||
// v1ServerFileRoutes.POST("/decompress/:file", AuthHandler("s:files:decompress"), handlePostFileDecompress)
|
||||
//
|
||||
// v1ServerFileRoutes.DELETE("/file/:file", AuthHandler("s:files:delete"), handleDeleteFile)
|
||||
//
|
||||
// v1ServerFileRoutes.GET("/download/:token", handleGetDownloadFile)
|
||||
//}
|
||||
}
|
||||
}
|
||||
123
api/auth.go
123
api/auth.go
@@ -1,123 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/jsonapi"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/control"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
accessTokenHeader = "Authorization"
|
||||
|
||||
contextVarServer = "server"
|
||||
contextVarAuth = "auth"
|
||||
)
|
||||
|
||||
type responseError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// AuthorizationManager handles permission checks
|
||||
type AuthorizationManager interface {
|
||||
HasPermission(string) bool
|
||||
}
|
||||
|
||||
type authorizationManager struct {
|
||||
token string
|
||||
server control.Server
|
||||
}
|
||||
|
||||
var _ AuthorizationManager = &authorizationManager{}
|
||||
|
||||
func newAuthorizationManager(token string, server control.Server) *authorizationManager {
|
||||
return &authorizationManager{
|
||||
token: token,
|
||||
server: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authorizationManager) HasPermission(permission string) bool {
|
||||
if permission == "" {
|
||||
return true
|
||||
}
|
||||
prefix := permission[:1]
|
||||
if prefix == "c" {
|
||||
return config.ContainsAuthKey(a.token)
|
||||
}
|
||||
if a.server == nil {
|
||||
log.WithField("permission", permission).Error("Auth: Server required but none found.")
|
||||
return false
|
||||
}
|
||||
if prefix == "g" {
|
||||
return config.ContainsAuthKey(a.token)
|
||||
}
|
||||
if prefix == "s" {
|
||||
return a.server.HasPermission(a.token, permission) || config.ContainsAuthKey(a.token)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthHandler returns a HandlerFunc that checks request authentication
|
||||
// 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 {
|
||||
return func(c *gin.Context) {
|
||||
requestToken := c.Request.Header.Get(accessTokenHeader)
|
||||
if requestToken != "" && strings.HasPrefix(requestToken, "Baerer ") {
|
||||
requestToken = requestToken[7:]
|
||||
} else {
|
||||
requestToken = c.Query("token")
|
||||
}
|
||||
requestServer := c.Param("server")
|
||||
var server control.Server
|
||||
|
||||
if requestToken == "" && permission != "" {
|
||||
sendErrors(c, http.StatusUnauthorized, &jsonapi.ErrorObject{
|
||||
Title: "Missing required " + accessTokenHeader + " header or token param.",
|
||||
Status: strconv.Itoa(http.StatusUnauthorized),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if requestServer != "" {
|
||||
server = control.GetServer(requestServer)
|
||||
//fmt.Println(server)
|
||||
if server == nil {
|
||||
log.WithField("serverUUID", requestServer).Error("Auth: Requested server not found.")
|
||||
}
|
||||
}
|
||||
|
||||
auth := newAuthorizationManager(requestToken, server)
|
||||
|
||||
if auth.HasPermission(permission) {
|
||||
c.Set(contextVarServer, server)
|
||||
c.Set(contextVarAuth, auth)
|
||||
return
|
||||
}
|
||||
|
||||
sendForbidden(c)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
// GetContextAuthManager returns a AuthorizationManager contained in
|
||||
// a gin.Context or nil
|
||||
func GetContextAuthManager(c *gin.Context) AuthorizationManager {
|
||||
auth, exists := c.Get(contextVarAuth)
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if auth, ok := auth.(AuthorizationManager); ok {
|
||||
return auth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
143
api/auth_test.go
143
api/auth_test.go
@@ -1,143 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/control"
|
||||
)
|
||||
|
||||
const configFile = "_testdata/config.yml"
|
||||
|
||||
func TestAuthHandler(t *testing.T) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
t.Run("rejects missing token", func(t *testing.T) {
|
||||
loadConfiguration(t, false)
|
||||
|
||||
responded, rec := requestMiddlewareWith("c:somepermission", "", "")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("rejects c:* with invalid key", func(t *testing.T) {
|
||||
loadConfiguration(t, false)
|
||||
|
||||
responded, rec := requestMiddlewareWith("c:somepermission", "invalidkey", "")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("accepts existing c: key", func(t *testing.T) {
|
||||
loadConfiguration(t, false)
|
||||
|
||||
responded, rec := requestMiddlewareWith("c:somepermission", "existingkey", "") // TODO: working token
|
||||
|
||||
assert.True(t, responded)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("rejects not existing server", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("g:testnotexisting", "existingkey", "notexistingserver")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("accepts server with existing g: key", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("g:test", "existingkey", "existingserver")
|
||||
|
||||
assert.True(t, responded)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("rejects server with not existing g: key", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("g:test", "notexistingkey", "existingserver")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("rejects server with not existing s: key", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("s:test", "notexistingskey", "existingserver")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("accepts server with existing s: key with specific permission", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("s:test", "existingspecificskey", "existingserver")
|
||||
|
||||
assert.True(t, responded)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("accepts server with existing s: key with gloabl permission", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("s:test", "existingglobalskey", "existingserver")
|
||||
|
||||
assert.True(t, responded)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("rejects server with existing s: key without permission", func(t *testing.T) {
|
||||
loadConfiguration(t, true)
|
||||
|
||||
responded, rec := requestMiddlewareWith("s:without", "existingspecificskey", "existingserver")
|
||||
|
||||
assert.False(t, responded)
|
||||
assert.Equal(t, http.StatusForbidden, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func requestMiddlewareWith(neededPermission string, token string, serverUUID string) (responded bool, recorder *httptest.ResponseRecorder) {
|
||||
router := gin.New()
|
||||
responded = false
|
||||
recorder = httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/"+serverUUID, nil)
|
||||
|
||||
endpoint := "/"
|
||||
if serverUUID != "" {
|
||||
endpoint += ":server"
|
||||
}
|
||||
|
||||
router.GET(endpoint, AuthHandler(neededPermission), func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "Access granted.")
|
||||
responded = true
|
||||
})
|
||||
|
||||
req.Header.Set(accessTokenHeader, token)
|
||||
router.ServeHTTP(recorder, req)
|
||||
return
|
||||
}
|
||||
|
||||
func loadConfiguration(t *testing.T, serverConfig bool) {
|
||||
if err := config.LoadConfiguration(configFile); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if serverConfig {
|
||||
if err := control.LoadServerConfigurations("_testdata/servers/"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
144
api/handlers.go
144
api/handlers.go
@@ -1,144 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
//"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
//"github.com/shirou/gopsutil/host"
|
||||
//"github.com/shirou/gopsutil/mem"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GetIndex(c *gin.Context) {
|
||||
//auth := GetContextAuthManager(c)
|
||||
//if auth == nil {
|
||||
// c.Header("Content-Type", "text/html")
|
||||
// c.String(http.StatusOK, constants.IndexPage)
|
||||
//}
|
||||
|
||||
s, err := cpu.Counts(true)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to retrieve host information.")
|
||||
}
|
||||
|
||||
fmt.Println(s)
|
||||
i := struct {
|
||||
Name string
|
||||
Cpu struct {
|
||||
Cores int
|
||||
}
|
||||
}{
|
||||
Name: "Wings",
|
||||
}
|
||||
|
||||
i.Cpu.Cores = s
|
||||
|
||||
c.JSON(http.StatusOK, i)
|
||||
return
|
||||
|
||||
//if auth != nil && auth.HasPermission("c:info") {
|
||||
// hostInfo, err := host.Info()
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("Failed to retrieve host information.")
|
||||
// }
|
||||
// cpuInfo, err := cpu.Info()
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("Failed to retrieve CPU information.")
|
||||
// }
|
||||
// memInfo, err := mem.VirtualMemory()
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("Failed to retrieve memory information.")
|
||||
// }
|
||||
//
|
||||
// info := struct {
|
||||
// Name string `json:"name"`
|
||||
// Version string `json:"version"`
|
||||
// System struct {
|
||||
// SystemType string `json:"type"`
|
||||
// Platform string `json:"platform"`
|
||||
// Arch string `json:"arch"`
|
||||
// Release string `json:"release"`
|
||||
// Cpus int32 `json:"cpus"`
|
||||
// Freemem uint64 `json:"freemem"`
|
||||
// } `json:"system"`
|
||||
// }{
|
||||
// Name: "Pterodactyl wings",
|
||||
// Version: constants.Version,
|
||||
// }
|
||||
// info.System.SystemType = hostInfo.OS
|
||||
// info.System.Platform = hostInfo.Platform
|
||||
// info.System.Arch = runtime.GOARCH
|
||||
// info.System.Release = hostInfo.KernelVersion
|
||||
// info.System.Cpus = cpuInfo[0].Cores
|
||||
// info.System.Freemem = memInfo.Free
|
||||
//
|
||||
// c.JSON(http.StatusOK, info)
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
||||
type incomingConfiguration struct {
|
||||
Debug bool `mapstructure:"debug"`
|
||||
Web struct {
|
||||
ListenHost string `mapstructure:"host"`
|
||||
ListenPort int16 `mapstructure:"port"`
|
||||
SSL struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Certificate string `mapstructure:"certificate"`
|
||||
Key string `mapstructure:"key"`
|
||||
} `mapstructure:"ssl"`
|
||||
|
||||
Uploads struct {
|
||||
MaximumSize int64 `mapstructure:"maximumSize"`
|
||||
} `mapstructure:"uploads"`
|
||||
} `mapstructure:"web"`
|
||||
|
||||
Docker struct {
|
||||
Socket string `mapstructure:"socket"`
|
||||
AutoupdateImages bool `mapstructure:"autoupdateImages"`
|
||||
NetworkInterface string `mapstructure:"networkInterface"`
|
||||
TimezonePath string `mapstructure:"timezonePath"`
|
||||
} `mapstructure:"docker"`
|
||||
|
||||
Sftp struct {
|
||||
Path string `mapstructure:"path"`
|
||||
Port int16 `mapstructure:"port"`
|
||||
} `mapstructure:"sftp"`
|
||||
|
||||
Query struct {
|
||||
KillOnFail bool `mapstructure:"killOnFail"`
|
||||
FailLimit bool `mapstructure:"failLimit"`
|
||||
} `mapstructure:"query"`
|
||||
|
||||
Remote string `mapstructure:"remote"`
|
||||
|
||||
Log struct {
|
||||
Path string `mapstructure:"path"`
|
||||
Level string `mapstructure:"level"`
|
||||
DeleteAfterDays int `mapstructure:"deleteAfterDays"`
|
||||
} `mapstructure:"log"`
|
||||
|
||||
AuthKeys []string `mapstructure:"authKeys"`
|
||||
}
|
||||
|
||||
// handlePatchConfig handles PATCH /config
|
||||
func PatchConfiguration(c *gin.Context) {
|
||||
// reqBody, err := ioutil.ReadAll(c.Request.Body)
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("Failed to read input.")
|
||||
// return
|
||||
// }
|
||||
// reqJSON := new(incomingConfiguration)
|
||||
// err = json.Unmarshal(reqBody, reqJSON)
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("Failed to decode JSON.")
|
||||
// return
|
||||
// }
|
||||
var json incomingConfiguration
|
||||
if err := c.BindJSON(&json); err != nil {
|
||||
log.WithError(err).Error("Failed to bind Json.")
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/jsonapi"
|
||||
"github.com/pterodactyl/wings/control"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GET /servers
|
||||
func handleGetServers(c *gin.Context) {
|
||||
servers := control.GetServers()
|
||||
sendData(c, servers)
|
||||
}
|
||||
|
||||
// POST /servers
|
||||
func handlePostServers(c *gin.Context) {
|
||||
server := control.ServerStruct{}
|
||||
if err := c.BindJSON(&server); err != nil {
|
||||
log.WithField("server", server).WithError(err).Error("Failed to parse server request.")
|
||||
sendErrors(c, http.StatusBadRequest, &jsonapi.ErrorObject{
|
||||
Status: strconv.Itoa(http.StatusBadRequest),
|
||||
Title: "The passed server object is invalid.",
|
||||
})
|
||||
return
|
||||
}
|
||||
var srv control.Server
|
||||
var err error
|
||||
if srv, err = control.CreateServer(&server); err != nil {
|
||||
if _, ok := err.(control.ErrServerExists); ok {
|
||||
log.WithError(err).Error("Cannot create server, it already exists.")
|
||||
c.Status(http.StatusBadRequest)
|
||||
sendErrors(c, http.StatusConflict, &jsonapi.ErrorObject{
|
||||
Status: strconv.Itoa(http.StatusConflict),
|
||||
Title: "A server with this ID already exists.",
|
||||
})
|
||||
return
|
||||
}
|
||||
log.WithField("server", server).WithError(err).Error("Failed to create server.")
|
||||
sendInternalError(c, "Failed to create the server", "")
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
env, err := srv.Environment()
|
||||
if err != nil {
|
||||
log.WithField("server", srv).WithError(err).Error("Failed to get server environment.")
|
||||
}
|
||||
env.Create()
|
||||
}()
|
||||
sendDataStatus(c, http.StatusCreated, srv)
|
||||
}
|
||||
|
||||
// GET /servers/:server
|
||||
func handleGetServer(c *gin.Context) {
|
||||
id := c.Param("server")
|
||||
server := control.GetServer(id)
|
||||
if server == nil {
|
||||
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
|
||||
}
|
||||
sendData(c, server)
|
||||
}
|
||||
|
||||
// PATCH /servers/:server
|
||||
func handlePatchServer(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
// DELETE /servers/:server
|
||||
func handleDeleteServer(c *gin.Context) {
|
||||
id := c.Param("server")
|
||||
server := control.GetServer(id)
|
||||
if server == nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
env, err := server.Environment()
|
||||
if err != nil {
|
||||
sendInternalError(c, "The server could not be deleted.", "")
|
||||
return
|
||||
}
|
||||
if err := env.Destroy(); err != nil {
|
||||
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 {
|
||||
log.WithError(err).Error("Failed to delete server.")
|
||||
sendInternalError(c, "The server could not be deleted.", "")
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handlePostServerReinstall(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func handlePostServerPassword(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func handlePostServerRebuild(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
// POST /servers/:server/power
|
||||
func handlePostServerPower(c *gin.Context) {
|
||||
server := getServerFromContext(c)
|
||||
if server == nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
auth := GetContextAuthManager(c)
|
||||
if auth == nil {
|
||||
sendInternalError(c, "An internal error occured.", "")
|
||||
return
|
||||
}
|
||||
|
||||
switch c.Query("action") {
|
||||
case "start":
|
||||
{
|
||||
if !auth.HasPermission("s:power:start") {
|
||||
sendForbidden(c)
|
||||
return
|
||||
}
|
||||
server.Start()
|
||||
}
|
||||
case "stop":
|
||||
{
|
||||
if !auth.HasPermission("s:power:stop") {
|
||||
sendForbidden(c)
|
||||
return
|
||||
}
|
||||
server.Stop()
|
||||
}
|
||||
case "restart":
|
||||
{
|
||||
if !auth.HasPermission("s:power:restart") {
|
||||
sendForbidden(c)
|
||||
return
|
||||
}
|
||||
server.Restart()
|
||||
}
|
||||
case "kill":
|
||||
{
|
||||
if !auth.HasPermission("s:power:kill") {
|
||||
sendForbidden(c)
|
||||
return
|
||||
}
|
||||
server.Kill()
|
||||
}
|
||||
default:
|
||||
{
|
||||
c.Status(http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST /servers/:server/command
|
||||
func handlePostServerCommand(c *gin.Context) {
|
||||
server := getServerFromContext(c)
|
||||
cmd := c.Query("command")
|
||||
server.Exec(cmd)
|
||||
c.Status(204)
|
||||
}
|
||||
|
||||
func handleGetConsole(c *gin.Context) {
|
||||
server := getServerFromContext(c)
|
||||
server.Websockets().Upgrade(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func handlePostServerSuspend(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func handlePostServerUnsuspend(c *gin.Context) {
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pterodactyl/wings/control"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandleGetServers(t *testing.T) {
|
||||
router := gin.New()
|
||||
req, _ := http.NewRequest("GET", "/servers", nil)
|
||||
|
||||
router.GET("/servers", handleGetServers)
|
||||
|
||||
t.Run("returns an empty json array when no servers are configured", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
body, err := ioutil.ReadAll(rec.Body)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "[]\n", string(body))
|
||||
})
|
||||
|
||||
t.Run("returns an array of servers", func(t *testing.T) {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
var testServer = control.ServerStruct{
|
||||
ID: "id1",
|
||||
}
|
||||
|
||||
func TestHandlePostServer(t *testing.T) {
|
||||
// We need to decide how deep we want to go with testing here
|
||||
// Should we just verify it works in general or test validation and
|
||||
// minimal required config options as well?
|
||||
// This will also vary depending on the Environment the server runs in
|
||||
}
|
||||
|
||||
func TestHandleGetServer(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.GET("/servers/:server", handleGetServer)
|
||||
|
||||
control.CreateServer(&testServer)
|
||||
|
||||
t.Run("returns not found when the server doesn't exist", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/servers/id0", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("returns the server as json", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "/servers/id1", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandlePatchServer(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestHandleDeleteServer(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.DELETE("/servers/:server", handleDeleteServer)
|
||||
|
||||
control.CreateServer(&testServer)
|
||||
|
||||
t.Run("returns not found when the server doesn't exist", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/servers/id0", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusNotFound, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("deletes the server", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("DELETE", "/servers/id1", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func bodyToJSON(body *bytes.Buffer, v interface{}) error {
|
||||
reqBody, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(reqBody, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandleGetIndex(t *testing.T) {
|
||||
router := gin.New()
|
||||
recorder := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
|
||||
router.GET("/", handleGetIndex)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
func TestHandlePatchConfig(t *testing.T) {
|
||||
router := gin.New()
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
req, _ := http.NewRequest("PATCH", "/", strings.NewReader("{}"))
|
||||
|
||||
router.PATCH("/", handlePatchConfig)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
45
api/utils.go
45
api/utils.go
@@ -1,45 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/jsonapi"
|
||||
"github.com/pterodactyl/wings/control"
|
||||
)
|
||||
|
||||
func getServerFromContext(context *gin.Context) control.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)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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[*Socket]bool
|
||||
|
||||
type Collection struct {
|
||||
sockets websocketMap
|
||||
|
||||
Broadcast chan Message
|
||||
|
||||
register chan *Socket
|
||||
unregister chan *Socket
|
||||
close chan bool
|
||||
}
|
||||
|
||||
//var _ io.Writer = &Collection{}
|
||||
|
||||
func NewCollection() *Collection {
|
||||
return &Collection{
|
||||
Broadcast: make(chan Message),
|
||||
register: make(chan *Socket),
|
||||
unregister: make(chan *Socket),
|
||||
close: make(chan bool),
|
||||
sockets: make(websocketMap),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collection) 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
|
||||
}
|
||||
s := &Socket{
|
||||
collection: c,
|
||||
conn: socket,
|
||||
send: make(chan []byte, 256),
|
||||
}
|
||||
c.register <- s
|
||||
|
||||
go s.readPump()
|
||||
go s.writePump()
|
||||
}
|
||||
|
||||
func (c *Collection) Add(s *Socket) {
|
||||
c.register <- s
|
||||
}
|
||||
|
||||
func (c *Collection) Remove(s *Socket) {
|
||||
c.unregister <- s
|
||||
}
|
||||
|
||||
func (c *Collection) Run() {
|
||||
defer func() {
|
||||
for s := range c.sockets {
|
||||
close(s.send)
|
||||
delete(c.sockets, s)
|
||||
}
|
||||
close(c.register)
|
||||
close(c.unregister)
|
||||
close(c.Broadcast)
|
||||
close(c.close)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case s := <-c.register:
|
||||
c.sockets[s] = true
|
||||
case s := <-c.unregister:
|
||||
if _, ok := c.sockets[s]; ok {
|
||||
delete(c.sockets, s)
|
||||
close(s.send)
|
||||
}
|
||||
case m := <-c.Broadcast:
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to encode websocket message.")
|
||||
continue
|
||||
}
|
||||
for s := range c.sockets {
|
||||
select {
|
||||
case s.send <- b:
|
||||
default:
|
||||
close(s.send)
|
||||
delete(c.sockets, s)
|
||||
}
|
||||
}
|
||||
case <-c.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collection) Close() {
|
||||
c.close <- true
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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 (c *Collection) Log(l ConsoleLevel, m string) {
|
||||
c.Broadcast <- Message{
|
||||
Type: MessageTypeConsole,
|
||||
Payload: ConsolePayload{
|
||||
Source: ConsoleSourceWings,
|
||||
Level: l,
|
||||
Line: m,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package websockets
|
||||
|
||||
import "github.com/gorilla/websocket"
|
||||
import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Socket struct {
|
||||
collection *Collection
|
||||
|
||||
conn *websocket.Conn
|
||||
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
func (s *Socket) readPump() {
|
||||
defer func() {
|
||||
s.collection.unregister <- s
|
||||
s.conn.Close()
|
||||
}()
|
||||
s.conn.SetReadLimit(maxMessageSize)
|
||||
s.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
s.conn.SetPongHandler(func(string) error {
|
||||
s.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
for {
|
||||
t, m, err := s.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.WithError(err).Debug("Websocket closed unexpectedly.")
|
||||
}
|
||||
return
|
||||
}
|
||||
// Handle websocket responses somehow
|
||||
if t == websocket.TextMessage {
|
||||
log.Debug("Received websocket message: " + string(m))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Socket) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
s.conn.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case m, ok := <-s.send:
|
||||
s.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if !ok {
|
||||
// The collection closed the channel
|
||||
s.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
w, err := s.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write([]byte{'['})
|
||||
w.Write(m)
|
||||
n := len(s.send) - 1
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write([]byte{','})
|
||||
w.Write(<-s.send)
|
||||
}
|
||||
w.Write([]byte{']'})
|
||||
if err := w.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
s.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := s.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user