Merge pull request #5 from pterodactyl/feature/docker-environment

Feature/docker environment
This commit is contained in:
Jakob 2018-05-17 21:22:26 +02:00 committed by GitHub
commit 4ef7bef4fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1365 additions and 651 deletions

View File

@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
echo "Provisioning development environment for Pterodactyl go daemon." echo "Provisioning development environment for Pterodactyl go daemon."
cp /home/ubuntu/go/github.com/pterodactyl/wings.go/.dev/vagrant/motd.txt /etc/motd cp /home/vagrant/go/github.com/pterodactyl/wings.go/.dev/vagrant/motd.txt /etc/motd
chown -R ubuntu:ubuntu /home/ubuntu/go chown -R vagrant:vagrant /home/vagrant/go
chown -R ubuntu:ubuntu /srv chown -R vagrant:vagrant /srv
echo "Update apt repositories" echo "Update apt repositories"
sudo add-apt-repository ppa:longsleep/golang-backports sudo add-apt-repository ppa:longsleep/golang-backports
@ -13,24 +13,28 @@ apt-get update > /dev/null
echo "Install docker" echo "Install docker"
curl -sSL https://get.docker.com/ | sh curl -sSL https://get.docker.com/ | sh
systemctl enable docker systemctl enable docker
usermod -aG docker ubuntu usermod -aG docker vagrant
echo "Install go" echo "Install go"
apt-get install -y golang-go apt-get install -y golang-go
echo "export GOPATH=/home/ubuntu/go" >> /home/ubuntu/.profile echo "export GOPATH=/home/vagrant/go" >> /home/vagrant/.profile
export GOPATH=/go export GOPATH=/go
echo 'export PATH=$PATH:$GOPATH/bin' >> /home/ubuntu/.profile echo 'export PATH=$PATH:$GOPATH/bin' >> /home/vagrant/.profile
echo "Install go dep" echo "Install go dep"
sudo -H -u ubuntu bash -c 'go get -u github.com/golang/dep/cmd/dep' sudo -H -u vagrant bash -c 'go get -u github.com/golang/dep/cmd/dep'
echo "Install delve for debugging" echo "Install delve for debugging"
sudo -H -u ubuntu bash -c 'go get -u github.com/derekparker/delve/cmd/dlv' sudo -H -u vagrant bash -c 'go get -u github.com/derekparker/delve/cmd/dlv'
echo "Install additional dependencies" echo "Install additional dependencies"
apt-get -y install mercurial #tar unzip make gcc g++ python > /dev/null apt-get -y install mercurial #tar unzip make gcc g++ python > /dev/null
echo "Install ctop for fancy container monitoring"
wget https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-linux-amd64 -O /usr/local/bin/ctop
chmod +x /usr/local/bin/ctop
echo " ------------" echo " ------------"
echo "Gopath is /home/ubuntu/go" echo "Gopath is /home/vagrant/go"
echo "The project is mounted to /home/ubuntu/go/src/github.com/pterodactyl/wings.go" echo "The project is mounted to /home/vagrant/go/src/github.com/pterodactyl/wings"
echo "Provisioning is completed." echo "Provisioning is completed."

2
.gitignore vendored
View File

@ -39,6 +39,8 @@ wings.exe
# IDE/Editor files (VS Code) # IDE/Editor files (VS Code)
/.vscode /.vscode
# test files
test_*/
# Keep all gitkeep files (This needs to stay at the bottom) # Keep all gitkeep files (This needs to stay at the bottom)
!.gitkeep !.gitkeep

View File

@ -6,23 +6,17 @@ go:
services: services:
- docker - docker
addons:
apt:
sources:
- sourceline: 'ppa:masterminds/glide'
packages:
- glide
install: install:
- mkdir -p $GOPATH/bin - mkdir -p $GOPATH/bin
# Install other tools # Install used tools
- go get github.com/golang/dep/cmd/dep
- go get github.com/mitchellh/gox - go get github.com/mitchellh/gox
- go get github.com/haya14busa/goverage - go get github.com/haya14busa/goverage
- go get github.com/schrej/godacov - go get github.com/schrej/godacov
# Install project dependencies with glide # Install project dependencies with dep
- glide install - dep ensure
script: script:
- make cross-build - make cross-build

156
Gopkg.lock generated
View File

@ -1,15 +1,6 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Azure/go-ansiterm"
packages = [
".",
"winterm"
]
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
[[projects]] [[projects]]
name = "github.com/Microsoft/go-winio" name = "github.com/Microsoft/go-winio"
packages = ["."] packages = ["."]
@ -17,22 +8,10 @@
version = "v0.4.7" version = "v0.4.7"
[[projects]] [[projects]]
branch = "master"
name = "github.com/Nvveen/Gotty"
packages = ["."]
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
branch = "master"
name = "github.com/StackExchange/wmi" name = "github.com/StackExchange/wmi"
packages = ["."] packages = ["."]
revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338" revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338"
version = "1.0.0"
[[projects]]
branch = "master"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
[[projects]] [[projects]]
name = "github.com/davecgh/go-spew" name = "github.com/davecgh/go-spew"
@ -40,40 +19,45 @@
revision = "346938d642f2ec3594ed81d874461961cd0faa76" revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0" version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/docker/distribution"
packages = [
"digestset",
"reference"
]
revision = "83389a148052d74ac602f5f1d62f86ff2f3c4aa5"
[[projects]] [[projects]]
name = "github.com/docker/docker" name = "github.com/docker/docker"
packages = [ packages = [
"api",
"api/types", "api/types",
"api/types/blkiodev", "api/types/blkiodev",
"api/types/container", "api/types/container",
"api/types/events",
"api/types/filters", "api/types/filters",
"api/types/image",
"api/types/mount", "api/types/mount",
"api/types/network", "api/types/network",
"api/types/registry", "api/types/registry",
"api/types/strslice", "api/types/strslice",
"api/types/swarm", "api/types/swarm",
"api/types/swarm/runtime", "api/types/swarm/runtime",
"api/types/time",
"api/types/versions", "api/types/versions",
"opts", "api/types/volume",
"pkg/archive", "client"
"pkg/fileutils",
"pkg/homedir",
"pkg/idtools",
"pkg/ioutils",
"pkg/jsonmessage",
"pkg/longpath",
"pkg/mount",
"pkg/pools",
"pkg/stdcopy",
"pkg/system",
"pkg/term",
"pkg/term/windows"
] ]
revision = "fe8aac6f5ae413a967adb0adad0b54abdfb825c4" revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd"
[[projects]] [[projects]]
name = "github.com/docker/go-connections" name = "github.com/docker/go-connections"
packages = ["nat"] packages = [
"nat",
"sockets",
"tlsconfig"
]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d" revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0" version = "v0.3.0"
@ -89,12 +73,6 @@
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7" version = "v1.4.7"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "2ff310040c161b75fa19fb9b287a90a6e03c0012"
version = "1.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/gin-contrib/sse" name = "github.com/gin-contrib/sse"
@ -132,6 +110,18 @@
revision = "925541529c1fa6821df4e44ce2723319eb2be768" revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
name = "github.com/google/jsonapi"
packages = ["."]
revision = "46d3ced0434461be12e555852e2f1a9ed382e139"
version = "1.0.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/hashicorp/hcl" name = "github.com/hashicorp/hcl"
@ -139,6 +129,7 @@
".", ".",
"hcl/ast", "hcl/ast",
"hcl/parser", "hcl/parser",
"hcl/printer",
"hcl/scanner", "hcl/scanner",
"hcl/strconv", "hcl/strconv",
"hcl/token", "hcl/token",
@ -146,7 +137,7 @@
"json/scanner", "json/scanner",
"json/token" "json/token"
] ]
revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda"
[[projects]] [[projects]]
name = "github.com/inconshreveable/mousetrap" name = "github.com/inconshreveable/mousetrap"
@ -154,6 +145,18 @@
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0" version = "v1.0"
[[projects]]
name = "github.com/lestrrat-go/file-rotatelogs"
packages = ["."]
revision = "9df8b44f21785240553882138c5df2e9cc1db910"
version = "v2.1.0"
[[projects]]
branch = "master"
name = "github.com/lestrrat/go-strftime"
packages = ["."]
revision = "ba3bf9c1d0421aa146564a632931730344f1f9f1"
[[projects]] [[projects]]
name = "github.com/magiconair/properties" name = "github.com/magiconair/properties"
packages = ["."] packages = ["."]
@ -170,7 +173,7 @@
branch = "master" branch = "master"
name = "github.com/mitchellh/mapstructure" name = "github.com/mitchellh/mapstructure"
packages = ["."] packages = ["."]
revision = "a4e142e9c047c904fa2f1e144d9a84e6133024bc" revision = "00c29f56e2386353d58c599509e8dc3801b0d716"
[[projects]] [[projects]]
name = "github.com/opencontainers/go-digest" name = "github.com/opencontainers/go-digest"
@ -187,15 +190,6 @@
revision = "d60099175f88c47cd379c4738d158884749ed235" revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1" version = "v1.0.1"
[[projects]]
name = "github.com/opencontainers/runc"
packages = [
"libcontainer/system",
"libcontainer/user"
]
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]] [[projects]]
name = "github.com/pelletier/go-toml" name = "github.com/pelletier/go-toml"
packages = ["."] packages = ["."]
@ -214,29 +208,26 @@
revision = "792786c7400a136282c1664665ae0a8db921c6c2" revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
name = "github.com/rifflock/lfshook"
packages = ["."]
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
version = "v2.3"
[[projects]] [[projects]]
name = "github.com/shirou/gopsutil" name = "github.com/shirou/gopsutil"
packages = [ packages = [
"cpu", "cpu",
"host", "internal/common"
"internal/common",
"mem",
"net",
"process"
] ]
revision = "543a05cce094293c7747322720256bee15d88a12" revision = "5776ff9c7c5d063d574ef53d740f75c68b448e53"
version = "v2.18.02"
[[projects]]
branch = "master"
name = "github.com/shirou/w32"
packages = ["."]
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
[[projects]] [[projects]]
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"
packages = ["."] packages = ["."]
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.4" version = "v1.0.5"
[[projects]] [[projects]]
name = "github.com/spf13/afero" name = "github.com/spf13/afero"
@ -256,8 +247,8 @@
[[projects]] [[projects]]
name = "github.com/spf13/cobra" name = "github.com/spf13/cobra"
packages = ["."] packages = ["."]
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
version = "v0.0.1" version = "v0.0.2"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -274,8 +265,8 @@
[[projects]] [[projects]]
name = "github.com/spf13/viper" name = "github.com/spf13/viper"
packages = ["."] packages = ["."]
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736"
version = "v1.0.0" version = "v1.0.2"
[[projects]] [[projects]]
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
@ -293,16 +284,17 @@
branch = "master" branch = "master"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = ["ssh/terminal"] packages = ["ssh/terminal"]
revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" revision = "12892e8c234f4fe6f6803f052061de9057903bb2"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"context", "context",
"context/ctxhttp" "context/ctxhttp",
"proxy"
] ]
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" revision = "b68f30494add4df6bd8ef5e82803f308e7f7c59c"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -311,10 +303,9 @@
"unix", "unix",
"windows" "windows"
] ]
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" revision = "378d26f46672a356c46195c28f61bdb4c0a781dd"
[[projects]] [[projects]]
branch = "master"
name = "golang.org/x/text" name = "golang.org/x/text"
packages = [ packages = [
"internal/gen", "internal/gen",
@ -324,7 +315,8 @@
"unicode/cldr", "unicode/cldr",
"unicode/norm" "unicode/norm"
] ]
revision = "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]] [[projects]]
name = "gopkg.in/go-playground/validator.v8" name = "gopkg.in/go-playground/validator.v8"
@ -333,14 +325,14 @@
version = "v8.18.2" version = "v8.18.2"
[[projects]] [[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2" name = "gopkg.in/yaml.v2"
packages = ["."] packages = ["."]
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "3f1bdf5882e27f292b13d4b95c9f51eb1a7609af61ccda0fae50a806b0a2ba4f" inputs-digest = "8e17495db05ff2c85d228a20157c41c223e428d28217e14f89ab7b764a8706dd"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -1,6 +1,6 @@
# Gopkg.toml example # Gopkg.toml example
# #
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation. # for detailed Gopkg.toml documentation.
# #
# required = ["github.com/user/thing/cmd/thing"] # required = ["github.com/user/thing/cmd/thing"]
@ -25,33 +25,38 @@
# unused-packages = true # unused-packages = true
#[[constraint]]
# branch = "develop" [[constraint]]
# name = "github.com/pterodactyl/wings" name = "github.com/google/jsonapi"
version = "~1.0.0"
[[constraint]]
name = "github.com/gorilla/websocket"
version = "~1.2.0"
[[constraint]]
name = "github.com/shirou/gopsutil"
version = "2.17.6"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "~1.0.0"
[[constraint]] [[constraint]]
name = "github.com/gin-gonic/gin" name = "github.com/gin-gonic/gin"
version = "1.2.0" version = "1.2.0"
[[constraint]] [[constraint]]
name = "github.com/lestrrat/go-file-rotatelogs" name = "github.com/lestrrat-go/file-rotatelogs"
version = "2.1.0" version = "2.1.0"
[[constraint]] [[constraint]]
name = "github.com/rifflock/lfshook" name = "github.com/rifflock/lfshook"
version = "2.2.0" version = "2.2.0"
[[constraint]]
name = "github.com/shirou/gopsutil"
revision = "543a05cce094293c7747322720256bee15d88a12"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.4"
[[constraint]] [[constraint]]
name = "github.com/spf13/cobra" name = "github.com/spf13/cobra"
version = "0.0.1" version = "0.0.2"
[[constraint]] [[constraint]]
name = "github.com/spf13/viper" name = "github.com/spf13/viper"
@ -64,3 +69,11 @@
[prune] [prune]
go-tests = true go-tests = true
unused-packages = true unused-packages = true
[[constraint]]
name = "github.com/docker/docker"
revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd"
[[override]]
name = "github.com/docker/distribution"
branch = "master"

4
Vagrantfile vendored
View File

@ -1,7 +1,7 @@
Vagrant.configure("2") do |cfg| Vagrant.configure("2") do |cfg|
cfg.vm.box = "ubuntu/xenial64" cfg.vm.box = "bento/ubuntu-16.04"
cfg.vm.synced_folder "./", "/home/ubuntu/go/src/github.com/pterodactyl/wings" cfg.vm.synced_folder "./", "/home/vagrant/go/src/github.com/pterodactyl/wings"
cfg.vm.provision :shell, path: ".dev/vagrant/provision.sh" cfg.vm.provision :shell, path: ".dev/vagrant/provision.sh"

View File

@ -2,7 +2,6 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
@ -41,8 +40,7 @@ func (api *InternalAPI) Listen() {
l := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort)) l := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort))
api.router.Run(l) api.router.Run(l)
logrus.Info("Now listening on %s", l) logrus.Info("API Server is now listening on %s", l)
logrus.Fatal(http.ListenAndServe(l, nil))
} }
// Register routes for v1 of the API. This API should be fully backwards compatable with // Register routes for v1 of the API. This API should be fully backwards compatable with
@ -53,7 +51,7 @@ func (api *InternalAPI) Listen() {
func (api *InternalAPI) register() { func (api *InternalAPI) register() {
v1 := api.router.Group("/api/v1") v1 := api.router.Group("/api/v1")
{ {
v1.GET("/", AuthHandler(""), GetIndex) v1.GET("", AuthHandler(""), GetIndex)
//v1.PATCH("/config", AuthHandler("c:config"), PatchConfiguration) //v1.PATCH("/config", AuthHandler("c:config"), PatchConfiguration)
v1.GET("/servers", AuthHandler("c:list"), handleGetServers) v1.GET("/servers", AuthHandler("c:list"), handleGetServers)
@ -61,15 +59,15 @@ func (api *InternalAPI) register() {
v1ServerRoutes := v1.Group("/servers/:server") v1ServerRoutes := v1.Group("/servers/:server")
{ {
v1ServerRoutes.GET("/", AuthHandler("s:get"), handleGetServer) v1ServerRoutes.GET("", AuthHandler("s:get"), handleGetServer)
v1ServerRoutes.PATCH("/", AuthHandler("s:config"), handlePatchServer) v1ServerRoutes.PATCH("", AuthHandler("s:config"), handlePatchServer)
v1ServerRoutes.DELETE("/", AuthHandler("g:server:delete"), handleDeleteServer) v1ServerRoutes.DELETE("", AuthHandler("g:server:delete"), handleDeleteServer)
v1ServerRoutes.POST("/reinstall", AuthHandler("s:install-server"), handlePostServerReinstall) v1ServerRoutes.POST("/reinstall", AuthHandler("s:install-server"), handlePostServerReinstall)
v1ServerRoutes.POST("/rebuild", AuthHandler("g:server:rebuild"), handlePostServerRebuild) v1ServerRoutes.POST("/rebuild", AuthHandler("g:server:rebuild"), handlePostServerRebuild)
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("/log", AuthHandler("s:console"), handleGetServerLog) v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetConsole)
v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend) v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend)
v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend) v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend)
} }

View File

@ -2,8 +2,12 @@ package api
import ( import (
"net/http" "net/http"
"strings"
"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 +66,25 @@ 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 != "" && strings.HasPrefix(requestToken, "Baerer ") {
requestToken = requestToken[7:]
} else {
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 +104,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 +121,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

@ -6,9 +6,6 @@ import (
//"runtime" //"runtime"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
//"github.com/pterodactyl/wings/constants"
//"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/cpu"
//"github.com/shirou/gopsutil/host" //"github.com/shirou/gopsutil/host"
//"github.com/shirou/gopsutil/mem" //"github.com/shirou/gopsutil/mem"
@ -128,7 +125,7 @@ type incomingConfiguration struct {
} }
// handlePatchConfig handles PATCH /config // handlePatchConfig handles PATCH /config
func handlePatchConfig(c *gin.Context) { func PatchConfiguration(c *gin.Context) {
// reqBody, err := ioutil.ReadAll(c.Request.Body) // reqBody, err := ioutil.ReadAll(c.Request.Body)
// if err != nil { // if err != nil {
// log.WithError(err).Error("Failed to read input.") // log.WithError(err).Error("Failed to read input.")

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,15 +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 handleGetServerLog(c *gin.Context) { func handleGetConsole(c *gin.Context) {
server := getServerFromContext(c)
server.Websockets().Upgrade(c.Writer, c.Request)
} }
func handlePostServerSuspend(c *gin.Context) { func handlePostServerSuspend(c *gin.Context) {

50
api/routes.go Normal file
View File

@ -0,0 +1,50 @@
package api
func (api *InternalAPI) RegisterRoutes() {
// Register routes for v1 of the API. This API should be fully backwards compatable with
// the existing Nodejs Daemon API.
v1 := api.router.Group("/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("/console", AuthHandler("s:console"), handleGetConsole)
v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetServerLog)
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)
//}
}
}

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)
}

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[*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
}

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 (c *Collection) Log(l ConsoleLevel, m string) {
c.Broadcast <- Message{
Type: MessageTypeConsole,
Payload: ConsolePayload{
Source: ConsoleSourceWings,
Level: l,
Line: m,
},
}
}

81
api/websockets/socket.go Normal file
View File

@ -0,0 +1,81 @@
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
}
}
}
}

68
command/root.go Normal file
View File

@ -0,0 +1,68 @@
package command
import (
"path/filepath"
"strconv"
"github.com/spf13/viper"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/constants"
"github.com/pterodactyl/wings/control"
"github.com/pterodactyl/wings/utils"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// RootCommand is the root command of wings
var RootCommand = &cobra.Command{
Use: "wings",
Short: "Wings is the next generation server control daemon for Pterodactyl",
Long: "Wings is the next generation server control daemon for Pterodactyl",
Run: run,
Version: constants.Version,
}
var configPath string
func init() {
RootCommand.Flags().StringVarP(&configPath, "config", "c", "./config.yml", "Allows to set the path of the configuration file.")
}
// Execute registers the RootCommand
func Execute() {
RootCommand.Execute()
}
func run(cmd *cobra.Command, args []string) {
utils.InitLogging()
log.Info("Loading configuration...")
if err := config.LoadConfiguration(configPath); err != nil {
log.WithError(err).Fatal("Could not locate a suitable config.yml file. Aborting startup.")
log.Exit(1)
}
utils.ConfigureLogging()
log.Info(` ____`)
log.Info(`__ Pterodactyl _____/___/_______ _______ ______`)
log.Info(`\_____\ \/\/ / / / __ / ___/`)
log.Info(` \___\ / / / / /_/ /___ /`)
log.Info(` \___/\___/___/___/___/___ /______/`)
log.Info(` /_______/ v` + constants.Version)
log.Info()
log.Info("Configuration loaded successfully.")
log.Info("Loading configured servers.")
if err := control.LoadServerConfigurations(filepath.Join(viper.GetString(config.DataPath), constants.ServersPath)); err != nil {
log.WithError(err).Error("Failed to load configured servers.")
}
if amount := len(control.GetServers()); amount > 0 {
log.Info("Loaded " + strconv.Itoa(amount) + " server(s).")
}
log.Info("Starting API server.")
api := &api.InternalAPI{}
api.Listen()
}

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const configFile = "../config.yml.example" const configFile = "../config.example.yml"
func TestLoadConfiguraiton(t *testing.T) { func TestLoadConfiguraiton(t *testing.T) {
err := LoadConfiguration(configFile) err := LoadConfiguration(configFile)

View File

@ -6,7 +6,7 @@ const (
// DataPath is a string containing the path where data should // DataPath is a string containing the path where data should
// be stored on the system // be stored on the system
DataPath = "datapath" DataPath = "data"
// APIHost is a string containing the interface ip address // APIHost is a string containing the interface ip address
// on what the api should listen on // on what the api should listen on

View File

@ -26,3 +26,10 @@ const ServerDataPath = "data"
// JSONIndent is the indent to use with the json.MarshalIndent() function. // JSONIndent is the indent to use with the json.MarshalIndent() function.
const JSONIndent = " " const JSONIndent = " "
// DockerContainerPrefix is the prefix used for naming Docker containers.
// It's also used to prefix the hostnames of the docker containers.
const DockerContainerPrefix = "ptdl-"
// WSMaxMessages is the maximum number of messages that are sent in one transfer.
const WSMaxMessages = 10

View File

@ -0,0 +1,33 @@
package control
import (
"io"
"github.com/pterodactyl/wings/api/websockets"
)
type ConsoleHandler struct {
Websockets *websockets.Collection
HandlerFunc *func(string)
}
var _ io.Writer = ConsoleHandler{}
func (c ConsoleHandler) Write(b []byte) (n int, e error) {
l := make([]byte, len(b))
copy(l, b)
line := string(l)
m := websockets.Message{
Type: websockets.MessageTypeConsole,
Payload: websockets.ConsolePayload{
Line: line,
Level: websockets.ConsoleLevelPlain,
Source: websockets.ConsoleSourceServer,
},
}
c.Websockets.Broadcast <- m
if c.HandlerFunc != nil {
(*c.HandlerFunc)(line)
}
return len(b), nil
}

View File

@ -1,220 +0,0 @@
package control
import (
"context"
"io"
"os"
"strings"
"github.com/pterodactyl/wings/constants"
"github.com/fsouza/go-dockerclient"
log "github.com/sirupsen/logrus"
)
type dockerEnvironment struct {
baseEnvironment
client *docker.Client
container *docker.Container
context context.Context
containerInput io.Writer
containerOutput io.Writer
closeWaiter docker.CloseWaiter
server *ServerStruct
}
// Ensure DockerEnvironment implements Environment
var _ Environment = &dockerEnvironment{}
// NewDockerEnvironment creates a new docker enviornment
// instance and connects to the docker client on the host system
// If the container is already running it will try to reattach
// to the running container
func NewDockerEnvironment(server *ServerStruct) (Environment, error) {
env := dockerEnvironment{}
env.server = server
client, err := docker.NewClient("unix:///var/run/docker.sock")
env.client = client
if err != nil {
log.WithError(err).Fatal("Failed to connect to docker.")
return nil, err
}
if env.server.DockerContainer.ID != "" {
if err := env.checkContainerExists(); err != nil {
log.WithError(err).Error("Failed to find the container with stored id, removing id.")
env.server.DockerContainer.ID = ""
env.server.Save()
}
}
return &env, nil
}
func (env *dockerEnvironment) checkContainerExists() error {
container, err := env.client.InspectContainer(env.server.DockerContainer.ID)
if err != nil {
return err
}
env.container = container
return nil
}
func (env *dockerEnvironment) attach() error {
pr, pw := io.Pipe()
success := make(chan struct{})
w, err := env.client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{
Container: env.server.DockerContainer.ID,
InputStream: pr,
OutputStream: os.Stdout,
Stdin: true,
Stdout: true,
Stream: true,
Success: success,
})
env.closeWaiter = w
env.containerInput = pw
<-success
close(success)
return err
}
// Create creates the docker container for the environment and applies all
// settings to it
func (env *dockerEnvironment) Create() error {
log.WithField("server", env.server.ID).Debug("Creating docker environment")
// Split image repository and tag to feed it to the library
imageParts := strings.Split(env.server.Service().DockerImage, ":")
imageRepoParts := strings.Split(imageParts[0], "/")
if len(imageRepoParts) >= 3 {
// Handle possibly required authentication
}
// Pull docker image
var pullImageOpts = docker.PullImageOptions{
Repository: imageParts[0],
}
if len(imageParts) >= 2 {
pullImageOpts.Tag = imageParts[1]
}
log.WithField("image", env.server.service.DockerImage).Debug("Pulling docker image")
err := env.client.PullImage(pullImageOpts, docker.AuthConfiguration{})
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker environment")
return err
}
if err := os.MkdirAll(env.server.dataPath(), constants.DefaultFolderPerms); err != nil {
return err
}
// Create docker container
// TODO: apply cpu, io, disk limits.
containerConfig := &docker.Config{
Image: env.server.Service().DockerImage,
Cmd: strings.Split(env.server.StartupCommand, " "),
OpenStdin: true,
}
containerHostConfig := &docker.HostConfig{
Memory: env.server.Settings.Memory,
MemorySwap: env.server.Settings.Swap,
Binds: []string{env.server.dataPath() + ":/home/container"},
}
createContainerOpts := docker.CreateContainerOptions{
Name: "ptdl-" + env.server.UUIDShort(),
Config: containerConfig,
HostConfig: containerHostConfig,
Context: env.context,
}
container, err := env.client.CreateContainer(createContainerOpts)
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker container")
return err
}
env.server.DockerContainer.ID = container.ID
env.server.Save()
env.container = container
log.WithField("server", env.server.ID).Debug("Docker environment created")
return nil
}
// Destroy removes the environment's docker container
func (env *dockerEnvironment) Destroy() error {
log.WithField("server", env.server.ID).Debug("Destroying docker environment")
if _, err := env.client.InspectContainer(env.server.DockerContainer.ID); err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok {
log.WithField("server", env.server.ID).Debug("Container not found, docker environment destroyed already.")
return nil
}
log.WithError(err).WithField("server", env.server.ID).Error("Could not destroy docker environment")
return err
}
err := env.client.RemoveContainer(docker.RemoveContainerOptions{
ID: env.server.DockerContainer.ID,
})
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to destroy docker environment")
return err
}
log.WithField("server", env.server.ID).Debug("Docker environment destroyed")
return nil
}
func (env *dockerEnvironment) Exists() bool {
if env.container != nil {
return true
}
env.checkContainerExists()
return env.container != nil
}
// Start starts the environment's docker container
func (env *dockerEnvironment) Start() error {
log.WithField("server", env.server.ID).Debug("Starting service in docker environment")
if err := env.attach(); err != nil {
log.WithError(err).Error("Failed to attach to docker container")
}
if err := env.client.StartContainer(env.container.ID, nil); err != nil {
log.WithError(err).Error("Failed to start docker container")
return err
}
return nil
}
// Stop stops the environment's docker container
func (env *dockerEnvironment) Stop() error {
log.WithField("server", env.server.ID).Debug("Stopping service in docker environment")
if err := env.client.StopContainer(env.container.ID, 20000); err != nil {
log.WithError(err).Error("Failed to stop docker container")
return err
}
return nil
}
func (env *dockerEnvironment) Kill() error {
log.WithField("server", env.server.ID).Debug("Killing service in docker environment")
if err := env.client.KillContainer(docker.KillContainerOptions{
ID: env.container.ID,
}); err != nil {
log.WithError(err).Error("Failed to kill docker container")
return err
}
return nil
}
// Exec sends commands to the standard input of the docker container
func (env *dockerEnvironment) Exec(command string) error {
log.Debug("Command: " + command)
_, err := env.containerInput.Write([]byte(command + "\n"))
return err
}

View File

@ -0,0 +1,245 @@
package control
import (
"context"
"io"
"os"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pterodactyl/wings/constants"
log "github.com/sirupsen/logrus"
)
type dockerEnvironment struct {
baseEnvironment
client *client.Client
hires types.HijackedResponse
attached bool
server *ServerStruct
}
// Ensure DockerEnvironment implements Environment
var _ Environment = &dockerEnvironment{}
// NewDockerEnvironment creates a new docker enviornment
// instance and connects to the docker client on the host system
// If the container is already running it will try to reattach
// to the running container
func NewDockerEnvironment(server *ServerStruct) (Environment, error) {
env := dockerEnvironment{}
env.server = server
env.attached = false
cli, err := client.NewEnvClient()
env.client = cli
ctx := context.TODO()
cli.NegotiateAPIVersion(ctx)
if err != nil {
log.WithError(err).Fatal("Failed to connect to docker.")
return nil, err
}
if env.server.DockerContainer.ID != "" {
if err := env.inspectContainer(ctx); err != nil {
log.WithError(err).Error("Failed to find the container with stored id, removing id.")
env.server.DockerContainer.ID = ""
env.server.Save()
}
}
return &env, nil
}
func (env *dockerEnvironment) inspectContainer(ctx context.Context) error {
_, err := env.client.ContainerInspect(ctx, env.server.DockerContainer.ID)
return err
}
func (env *dockerEnvironment) attach() error {
if env.attached {
return nil
}
cw := ConsoleHandler{
Websockets: env.server.websockets,
}
var err error
env.hires, err = env.client.ContainerAttach(context.TODO(), env.server.DockerContainer.ID,
types.ContainerAttachOptions{
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
})
if err != nil {
log.WithField("server", env.server.ID).WithError(err).Error("Failed to attach to docker container.")
return err
}
env.attached = true
go func() {
defer env.hires.Close()
defer func() {
env.attached = false
}()
io.Copy(cw, env.hires.Reader)
}()
return nil
}
// Create creates the docker container for the environment and applies all
// settings to it
func (env *dockerEnvironment) Create() error {
log.WithField("server", env.server.ID).Debug("Creating docker environment")
ctx := context.TODO()
if err := env.pullImage(ctx); err != nil {
log.WithError(err).WithField("image", env.server.GetService().DockerImage).WithField("server", env.server.ID).Error("Failed to pull docker image.")
return err
}
if err := os.MkdirAll(env.server.dataPath(), constants.DefaultFolderPerms); err != nil {
return err
}
// Create docker container
// TODO: apply cpu, io, disk limits.
containerConfig := &container.Config{
Image: env.server.GetService().DockerImage,
Cmd: strings.Split(env.server.StartupCommand, " "),
AttachStdin: true,
OpenStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Hostname: constants.DockerContainerPrefix + env.server.UUIDShort(),
}
containerHostConfig := &container.HostConfig{
Resources: container.Resources{
Memory: env.server.Settings.Memory,
MemorySwap: env.server.Settings.Swap,
},
// TODO: Allow custom binds via some kind of settings in the service
Binds: []string{env.server.dataPath() + ":/home/container"},
// TODO: Add port bindings
}
containerHostConfig.Memory = 0
container, err := env.client.ContainerCreate(ctx, containerConfig, containerHostConfig, nil, constants.DockerContainerPrefix+env.server.UUIDShort())
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker container")
return err
}
env.server.DockerContainer.ID = container.ID
env.server.Save()
log.WithField("server", env.server.ID).Debug("Docker environment created")
return nil
}
// Destroy removes the environment's docker container
func (env *dockerEnvironment) Destroy() error {
log.WithField("server", env.server.ID).Debug("Destroying docker environment")
ctx := context.TODO()
if err := env.inspectContainer(ctx); err != nil {
log.WithError(err).Debug("Container not found error")
log.WithField("server", env.server.ID).Debug("Container not found, docker environment destroyed already.")
return nil
}
if err := env.client.ContainerRemove(ctx, env.server.DockerContainer.ID, types.ContainerRemoveOptions{}); err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to destroy docker environment")
return err
}
log.WithField("server", env.server.ID).Debug("Docker environment destroyed")
return nil
}
func (env *dockerEnvironment) Exists() bool {
if err := env.inspectContainer(context.TODO()); err != nil {
return false
}
return true
}
// Start starts the environment's docker container
func (env *dockerEnvironment) Start() error {
log.WithField("server", env.server.ID).Debug("Starting service in docker environment")
if err := env.attach(); err != nil {
log.WithError(err).Error("Failed to attach to docker container")
}
if err := env.client.ContainerStart(context.TODO(), env.server.DockerContainer.ID, types.ContainerStartOptions{}); err != nil {
log.WithError(err).Error("Failed to start docker container")
return err
}
return nil
}
// Stop stops the environment's docker container
func (env *dockerEnvironment) Stop() error {
log.WithField("server", env.server.ID).Debug("Stopping service in docker environment")
// TODO: Decide after what timeout to kill the container, currently 30 seconds
timeout := 30 * time.Second
if err := env.client.ContainerStop(context.TODO(), env.server.DockerContainer.ID, &timeout); err != nil {
log.WithError(err).Error("Failed to stop docker container")
return err
}
return nil
}
func (env *dockerEnvironment) Kill() error {
log.WithField("server", env.server.ID).Debug("Killing service in docker environment")
if err := env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL"); err != nil {
log.WithError(err).Error("Failed to kill docker container")
return err
}
return nil
}
// Exec sends commands to the standard input of the docker container
func (env *dockerEnvironment) Exec(command string) error {
log.Debug("Command: " + command)
_, err := env.hires.Conn.Write([]byte(command + "\n"))
return err
}
func (env *dockerEnvironment) pullImage(ctx context.Context) error {
// Split image repository and tag
//imageParts := strings.Split(env.server.GetService().DockerImage, ":")
//imageRepoParts := strings.Split(imageParts[0], "/")
//if len(imageRepoParts) >= 3 {
// TODO: Handle possibly required authentication
//}
// Pull docker image
log.WithField("image", env.server.GetService().DockerImage).Debug("Pulling docker image")
rc, err := env.client.ImagePull(ctx, env.server.GetService().DockerImage, types.ImagePullOptions{})
if err != nil {
return err
}
defer rc.Close()
return nil
}

View File

@ -1,19 +1,28 @@
package control package control
import ( import (
"context"
"fmt" "fmt"
"testing" "testing"
docker "github.com/fsouza/go-dockerclient" "github.com/pterodactyl/wings/api/websockets"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pterodactyl/wings/config"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testServer() *ServerStruct { func testServer() *ServerStruct {
viper.SetDefault(config.DataPath, "./test_data")
return &ServerStruct{ return &ServerStruct{
ID: "testuuid-something-something", ID: "testuuid-something-something",
service: &service{ Service: &Service{
DockerImage: "alpine:latest", DockerImage: "alpine:latest",
}, },
StartupCommand: "/bin/ash echo hello && sleep 100",
websockets: websockets.NewCollection(),
} }
} }
@ -33,7 +42,7 @@ func TestNewDockerEnvironmentExisting(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, env) assert.NotNil(t, env)
assert.NotNil(t, env.container) assert.NotNil(t, env.server.DockerContainer)
eenv.Destroy() eenv.Destroy()
} }
@ -45,12 +54,9 @@ func TestCreateDockerEnvironment(t *testing.T) {
a := assert.New(t) a := assert.New(t)
a.Nil(err) a.Nil(err)
a.NotNil(env.container) a.NotNil(env.server.DockerContainer.ID)
a.Equal(env.container.Name, "ptdl_testuuid")
if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ if err := env.client.ContainerRemove(context.TODO(), env.server.DockerContainer.ID, types.ContainerRemoveOptions{}); err != nil {
ID: env.container.ID,
}); err != nil {
fmt.Println(err) fmt.Println(err)
} }
} }
@ -61,10 +67,10 @@ func TestDestroyDockerEnvironment(t *testing.T) {
err := env.Destroy() err := env.Destroy()
_, ierr := env.client.InspectContainer(env.container.ID) _, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID)
assert.Nil(t, err) assert.Nil(t, err)
assert.IsType(t, ierr, &docker.NoSuchContainer{}) assert.True(t, client.IsErrNotFound(ierr))
} }
func TestStartDockerEnvironment(t *testing.T) { func TestStartDockerEnvironment(t *testing.T) {
@ -72,15 +78,13 @@ func TestStartDockerEnvironment(t *testing.T) {
env.Create() env.Create()
err := env.Start() err := env.Start()
i, ierr := env.client.InspectContainer(env.container.ID) i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID)
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, ierr) assert.Nil(t, ierr)
assert.True(t, i.State.Running) assert.True(t, i.State.Running)
env.client.KillContainer(docker.KillContainerOptions{ env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL")
ID: env.container.ID,
})
env.Destroy() env.Destroy()
} }
@ -90,15 +94,13 @@ func TestStopDockerEnvironment(t *testing.T) {
env.Start() env.Start()
err := env.Stop() err := env.Stop()
i, ierr := env.client.InspectContainer(env.container.ID) i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID)
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, ierr) assert.Nil(t, ierr)
assert.False(t, i.State.Running) assert.False(t, i.State.Running)
env.client.KillContainer(docker.KillContainerOptions{ env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL")
ID: env.container.ID,
})
env.Destroy() env.Destroy()
} }
@ -108,15 +110,13 @@ func TestKillDockerEnvironment(t *testing.T) {
env.Start() env.Start()
err := env.Kill() err := env.Kill()
i, ierr := env.client.InspectContainer(env.container.ID) i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID)
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, ierr) assert.Nil(t, ierr)
assert.False(t, i.State.Running) assert.False(t, i.State.Running)
env.client.KillContainer(docker.KillContainerOptions{ env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL")
ID: env.container.ID,
})
env.Destroy() env.Destroy()
} }

View File

@ -1,17 +1,19 @@
package control package control
import ( import (
"encoding/json"
"errors" "errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/api/websockets"
"github.com/pterodactyl/wings/constants"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" )
type Status string
const (
StatusStopped Status = "stopped"
StatusStarting Status = "starting"
StatusRunning Status = "running"
StatusStopping Status = "stopping"
) )
// ErrServerExists is returned when a server already exists on creation. // ErrServerExists is returned when a server already exists on creation.
@ -35,6 +37,7 @@ type Server interface {
Save() error Save() error
Environment() (Environment, error) Environment() (Environment, error)
Websockets() *websockets.Collection
HasPermission(string, string) bool HasPermission(string, string) bool
} }
@ -42,32 +45,36 @@ type Server interface {
// ServerStruct is a single instance of a Service managed by the panel // ServerStruct is a single instance of a Service managed by the panel
type ServerStruct struct { type ServerStruct struct {
// ID is the unique identifier of the server // ID is the unique identifier of the server
ID string `json:"uuid"` ID string `json:"uuid" jsonapi:"primary,server"`
// ServiceName is the name of the service. It is mainly used to allow storing the service // ServiceName is the name of the service. It is mainly used to allow storing the service
// in the config // in the config
ServiceName string `json:"serviceName"` ServiceName string `json:"serviceName"`
service *Service Service *Service `json:"-" jsonapi:"relation,service"`
environment Environment environment Environment
// StartupCommand is the command executed in the environment to start the server // StartupCommand is the command executed in the environment to start the server
StartupCommand string `json:"startupCommand"` StartupCommand string `json:"startupCommand" jsonapi:"attr,startup_command"`
// DockerContainer holds information regarding the docker container when the server // DockerContainer holds information regarding the docker container when the server
// is running in a docker environment // is running in a docker environment
DockerContainer dockerContainer `json:"dockerContainer"` DockerContainer dockerContainer `json:"dockerContainer" jsonapi:"attr,docker_container"`
// EnvironmentVariables are set in the Environment the server is running in // EnvironmentVariables are set in the Environment the server is running in
EnvironmentVariables map[string]string `json:"env"` EnvironmentVariables map[string]string `json:"environmentVariables" jsonapi:"attr,environment_variables"`
// Allocations contains the ports and ip addresses assigned to the server // Allocations contains the ports and ip addresses assigned to the server
Allocations allocations `json:"allocation"` Allocations allocations `json:"allocation" jsonapi:"attr,allocations"`
// Settings are the environment settings and limitations for the server // Settings are the environment settings and limitations for the server
Settings settings `json:"settings"` Settings settings `json:"settings" jsonapi:"attr,settings"`
// Keys are some auth keys we will hopefully replace by something better. // Keys are some auth keys we will hopefully replace by something better.
// TODO remove
Keys map[string][]string `json:"keys"` Keys map[string][]string `json:"keys"`
websockets *websockets.Collection
status Status
} }
type allocations struct { type allocations struct {
@ -99,73 +106,6 @@ type serversMap map[string]*ServerStruct
var servers = make(serversMap) var servers = make(serversMap)
// LoadServerConfigurations loads the configured servers from a specified path
func LoadServerConfigurations(path string) error {
serverFiles, err := ioutil.ReadDir(path)
if err != nil {
return err
}
servers = make(serversMap)
for _, file := range serverFiles {
if file.IsDir() {
server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile))
if err != nil {
return err
}
servers[server.ID] = server
}
}
return nil
}
func loadServerConfiguration(path string) (*ServerStruct, error) {
file, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
server := &ServerStruct{}
if err := json.Unmarshal(file, server); err != nil {
return nil, err
}
return server, nil
}
func storeServerConfiguration(server *ServerStruct) error {
serverJSON, err := json.MarshalIndent(server, "", constants.JSONIndent)
if err != nil {
return err
}
if err := os.MkdirAll(server.path(), constants.DefaultFolderPerms); err != nil {
return err
}
if err := ioutil.WriteFile(server.configFilePath(), serverJSON, constants.DefaultFilePerms); err != nil {
return err
}
return nil
}
func storeServerConfigurations() error {
for _, s := range servers {
if err := storeServerConfiguration(s); err != nil {
return err
}
}
return nil
}
func deleteServerFolder(id string) error {
path := filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, id)
folder, err := os.Stat(path)
if os.IsNotExist(err) || !folder.IsDir() {
return err
}
return os.RemoveAll(path)
}
// GetServers returns an array of all servers the daemon manages // GetServers returns an array of all servers the daemon manages
func GetServers() []Server { func GetServers() []Server {
serverArray := make([]Server, len(servers)) serverArray := make([]Server, len(servers))
@ -193,6 +133,11 @@ func CreateServer(server *ServerStruct) (Server, error) {
} }
servers[server.ID] = server servers[server.ID] = server
if err := server.Save(); err != nil { if err := server.Save(); err != nil {
delete(servers, server.ID)
return nil, err
}
if err := server.init(); err != nil {
DeleteServer(server.ID)
return nil, err return nil, err
} }
return server, nil return server, nil
@ -208,13 +153,40 @@ func DeleteServer(id string) error {
return nil return nil
} }
func (s *ServerStruct) init() error {
// TODO: Properly use the correct service, mock for now.
s.Service = &Service{
DockerImage: "quay.io/pterodactyl/core:java",
EnvironmentName: "docker",
}
s.status = StatusStopped
s.websockets = websockets.NewCollection()
go s.websockets.Run()
var err error
if s.environment == nil {
switch s.GetService().EnvironmentName {
case "docker":
s.environment, err = NewDockerEnvironment(s)
default:
log.WithField("service", s.ServiceName).Error("Invalid environment name")
return errors.New("Invalid environment name")
}
}
return err
}
func (s *ServerStruct) Start() error { func (s *ServerStruct) Start() error {
s.SetStatus(StatusStarting)
env, err := s.Environment() env, err := s.Environment()
if err != nil { if err != nil {
s.SetStatus(StatusStopped)
return err return err
} }
if !env.Exists() { if !env.Exists() {
if err := env.Create(); err != nil { if err := env.Create(); err != nil {
s.SetStatus(StatusStopped)
return err return err
} }
} }
@ -222,8 +194,10 @@ func (s *ServerStruct) Start() error {
} }
func (s *ServerStruct) Stop() error { func (s *ServerStruct) Stop() error {
s.SetStatus(StatusStopping)
env, err := s.Environment() env, err := s.Environment()
if err != nil { if err != nil {
s.SetStatus(StatusRunning)
return err return err
} }
return env.Stop() return env.Stop()
@ -259,70 +233,3 @@ func (s *ServerStruct) Rebuild() error {
} }
return env.ReCreate() return env.ReCreate()
} }
// Service returns the server's service configuration
func (s *ServerStruct) Service() *Service {
if s.service == nil {
// TODO: Properly use the correct service, mock for now.
s.service = &Service{
DockerImage: "quay.io/pterodactyl/core:java",
EnvironmentName: "docker",
}
}
return s.service
}
// UUIDShort returns the first block of the UUID
func (s *ServerStruct) UUIDShort() string {
return s.ID[0:strings.Index(s.ID, "-")]
}
// Environment returns the servers environment
func (s *ServerStruct) Environment() (Environment, error) {
var err error
if s.environment == nil {
switch s.Service().EnvironmentName {
case "docker":
s.environment, err = NewDockerEnvironment(s)
default:
log.WithField("service", s.ServiceName).Error("Invalid environment name")
return nil, errors.New("Invalid environment name")
}
}
return s.environment, err
}
// HasPermission checks wether a provided token has a specific permission
func (s *ServerStruct) HasPermission(token string, permission string) bool {
for key, perms := range s.Keys {
if key == token {
for _, perm := range perms {
if perm == permission || perm == "s:*" {
return true
}
}
return false
}
}
return false
}
func (s *ServerStruct) Save() error {
if err := storeServerConfiguration(s); err != nil {
log.WithField("server", s.ID).WithError(err).Error("Failed to store server configuration.")
return err
}
return nil
}
func (s *ServerStruct) path() string {
return filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, s.ID)
}
func (s *ServerStruct) dataPath() string {
return filepath.Join(s.path(), constants.ServerDataPath)
}
func (s *ServerStruct) configFilePath() string {
return filepath.Join(s.path(), constants.ServerConfigFile)
}

View File

@ -0,0 +1,108 @@
package control
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/constants"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
// LoadServerConfigurations loads the configured servers from a specified path
func LoadServerConfigurations(path string) error {
serverFiles, err := ioutil.ReadDir(path)
if err != nil {
return err
}
servers = make(serversMap)
for _, file := range serverFiles {
if file.IsDir() {
server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile))
if err != nil {
return err
}
servers[server.ID] = server
}
}
return nil
}
func loadServerConfiguration(path string) (*ServerStruct, error) {
file, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
server := &ServerStruct{}
if err := json.Unmarshal(file, server); err != nil {
return nil, err
}
if err := server.init(); err != nil {
return nil, err
}
return server, nil
}
func storeServerConfiguration(server *ServerStruct) error {
serverJSON, err := json.MarshalIndent(server, "", constants.JSONIndent)
if err != nil {
return err
}
if err := os.MkdirAll(server.path(), constants.DefaultFolderPerms); err != nil {
return err
}
if err := ioutil.WriteFile(server.configFilePath(), serverJSON, constants.DefaultFilePerms); err != nil {
return err
}
return nil
}
func storeServerConfigurations() error {
for _, s := range servers {
if err := storeServerConfiguration(s); err != nil {
return err
}
}
return nil
}
func deleteServerFolder(id string) error {
path := filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, id)
folder, err := os.Stat(path)
if os.IsNotExist(err) || !folder.IsDir() {
return err
}
return os.RemoveAll(path)
}
func (s *ServerStruct) Save() error {
if err := storeServerConfiguration(s); err != nil {
log.WithField("server", s.ID).WithError(err).Error("Failed to store server configuration.")
return err
}
return nil
}
func (s *ServerStruct) path() string {
p, err := filepath.Abs(viper.GetString(config.DataPath))
if err != nil {
log.WithError(err).WithField("server", s.ID).Error("Failed to get absolute data path for server.")
p = viper.GetString(config.DataPath)
}
return filepath.Join(p, constants.ServersPath, s.ID)
}
func (s *ServerStruct) dataPath() string {
return filepath.Join(s.path(), constants.ServerDataPath)
}
func (s *ServerStruct) configFilePath() string {
return filepath.Join(s.path(), constants.ServerConfigFile)
}

49
control/server_util.go Normal file
View File

@ -0,0 +1,49 @@
package control
import (
"strings"
"github.com/pterodactyl/wings/api/websockets"
)
func (s *ServerStruct) SetStatus(st Status) {
s.status = st
s.websockets.Broadcast <- websockets.Message{
Type: websockets.MessageTypeStatus,
Payload: s.status,
}
}
// Service returns the server's service configuration
func (s *ServerStruct) GetService() *Service {
return s.Service
}
// UUIDShort returns the first block of the UUID
func (s *ServerStruct) UUIDShort() string {
return s.ID[0:strings.Index(s.ID, "-")]
}
// Environment returns the servers environment
func (s *ServerStruct) Environment() (Environment, error) {
return s.environment, nil
}
func (s *ServerStruct) Websockets() *websockets.Collection {
return s.websockets
}
// HasPermission checks wether a provided token has a specific permission
func (s *ServerStruct) HasPermission(token string, permission string) bool {
for key, perms := range s.Keys {
if key == token {
for _, perm := range perms {
if perm == permission || perm == "s:*" {
return true
}
}
return false
}
}
return false
}

View File

@ -4,7 +4,7 @@ type Service struct {
server *Server server *Server
// EnvironmentName is the name of the environment used by the service // EnvironmentName is the name of the environment used by the service
EnvironmentName string `json:"environmentName"` EnvironmentName string `json:"environmentName" jsonapi:"primary,service"`
DockerImage string `json:"dockerImage"` DockerImage string `json:"dockerImage" jsonapi:"attr,docker_image"`
} }

177
glide.lock generated Normal file
View File

@ -0,0 +1,177 @@
hash: 59375e40229965f33d35e16d1ce13db87188dc5a70f89cf54c2921eb9475ba4f
updated: 2017-11-03T18:09:41.337408376+01:00
imports:
- name: bitbucket.org/tebeka/strftime
version: 2194253a23c090a4d5953b152a3c63fb5da4f5a4
- name: github.com/Azure/go-ansiterm
version: 19f72df4d05d31cbe1c56bfc8045c96babff6c7e
subpackages:
- winterm
- name: github.com/docker/docker
version: 90d35abf7b3535c1c319c872900fbd76374e521c
subpackages:
- api/types
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/network
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/versions
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: 3ede32e2033de7505e6500d6c868c2b9ed9f169d
subpackages:
- nat
- name: github.com/docker/go-units
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
- name: github.com/fsnotify/fsnotify
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
- name: github.com/fsouza/go-dockerclient
version: 4df4873b288c855e4186534280c3a3a1af403e67
- name: github.com/gin-gonic/gin
version: e2212d40c62a98b388a5eb48ecbdcf88534688ba
subpackages:
- binding
- render
- name: github.com/go-ole/go-ole
version: 085abb85892dc1949567b726dff00fa226c60c45
subpackages:
- oleutil
- name: github.com/golang/protobuf
version: 2402d76f3d41f928c7902a765dfc872356dd3aad
subpackages:
- proto
- name: github.com/google/jsonapi
version: 46d3ced0434461be12e555852e2f1a9ed382e139
- name: github.com/gorilla/websocket
version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b
- name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/hashicorp/hcl
version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca
subpackages:
- hcl/ast
- hcl/parser
- hcl/scanner
- hcl/strconv
- hcl/token
- json/parser
- json/scanner
- json/token
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/lestrrat/go-file-rotatelogs
version: ab335c655133cea61d8164853c1ed0e97d6c77cb
- name: github.com/magiconair/properties
version: 51463bfca2576e06c62a8504b5c0f06d61312647
- name: github.com/manucorporat/sse
version: ee05b128a739a0fb76c7ebd3ae4810c1de808d6d
- name: github.com/mattn/go-isatty
version: fc9e8d8ef48496124e79ae0df75490096eccf6fe
- name: github.com/Microsoft/go-winio
version: c4dc1301f1dc0307acd38e611aa375a64dfe0642
- name: github.com/mitchellh/mapstructure
version: d0303fe809921458f417bcf828397a65db30a7e4
- name: github.com/moby/moby
version: 90d35abf7b3535c1c319c872900fbd76374e521c
subpackages:
- client
- name: github.com/Nvveen/Gotty
version: cd527374f1e5bff4938207604a14f2e38a9cf512
- name: github.com/opencontainers/runc
version: 6ca8b741bb67839b7170d96257dde5c246f8b784
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/pelletier/go-buffruneio
version: c37440a7cf42ac63b919c752ca73a85067e05992
- name: github.com/pelletier/go-toml
version: 4a000a21a414d139727f616a8bb97f847b1b310b
- name: github.com/rifflock/lfshook
version: 6844c808343cb8fa357d7f141b1b990e05d24e41
- name: github.com/shirou/gopsutil
version: 6e221c482653ef05c9f6a7bf71ddceea0e40bac5
subpackages:
- cpu
- host
- internal/common
- mem
- net
- process
- name: github.com/shirou/w32
version: bb4de0191aa41b5507caa14b0650cdbddcd9280b
- name: github.com/Sirupsen/logrus
version: f006c2ac4710855cf0f916dd6b77acf6b048dc6e
repo: https://github.com/sirupsen/logrus.git
vcs: git
- name: github.com/sirupsen/logrus
version: f006c2ac4710855cf0f916dd6b77acf6b048dc6e
- name: github.com/spf13/afero
version: 9be650865eab0c12963d8753212f4f9c66cdcf12
subpackages:
- mem
- name: github.com/spf13/cast
version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4
- name: github.com/spf13/cobra
version: 90fc11bbc0a789c29272c21b5ff9e93db183f8dc
- name: github.com/spf13/jwalterweatherman
version: 0efa5202c04663c757d84f90f5219c1250baf94f
- name: github.com/spf13/pflag
version: e57e3eeb33f795204c1ca35f56c44f83227c6e66
- name: github.com/spf13/viper
version: c1de95864d73a5465492829d7cb2dd422b19ac96
- name: github.com/StackExchange/wmi
version: ea383cf3ba6ec950874b8486cd72356d007c768f
- name: golang.org/x/crypto
version: bd6f299fb381e4c3393d1c4b1f0b94f5e77650c8
subpackages:
- ssh/terminal
- name: golang.org/x/net
version: f315505cf3349909cdf013ea56690da34e96a451
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: f7928cfef4d09d1b080aa2b6fd3ca9ba1567c733
subpackages:
- unix
- windows
- name: golang.org/x/text
version: 5a2c30c33799f1e813f7f3259000d594a5ed493a
subpackages:
- transform
- unicode/norm
- name: gopkg.in/go-playground/validator.v8
version: c193cecd124b5cc722d7ee5538e945bdb3348435
- name: gopkg.in/yaml.v2
version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b
testImports:
- name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: 792786c7400a136282c1664665ae0a8db921c6c2
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert

33
glide.yaml Normal file
View File

@ -0,0 +1,33 @@
package: github.com/pterodactyl/wings
import:
- package: github.com/gin-gonic/gin
version: ~1.1.4
- package: github.com/lestrrat/go-file-rotatelogs
version: ~2.0.0
- package: github.com/rifflock/lfshook
version: ~1.7.0
- package: github.com/sirupsen/logrus
version: ~1.0.0
- package: github.com/Sirupsen/logrus
version: ~1.0.0
repo: https://github.com/sirupsen/logrus.git
vcs: git
- package: github.com/spf13/viper
- package: github.com/spf13/cobra
- package: github.com/shirou/gopsutil
version: ^2.17.6
- package: github.com/moby/moby
version: ~17.5.0-ce-rc3
subpackages:
- client
- package: github.com/fsouza/go-dockerclient
- package: github.com/hashicorp/go-cleanhttp
- package: github.com/gorilla/websocket
version: ~1.2.0
- package: github.com/google/jsonapi
version: ~1.0.0
testImport:
- package: github.com/stretchr/testify
version: ~1.1.4
subpackages:
- assert

71
main.go
View File

@ -1,71 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/constants"
"github.com/pterodactyl/wings/control"
"github.com/pterodactyl/wings/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var configPath string
var RootCommand = &cobra.Command{
Use: "wings",
Short: "Wings is the next generation server control daemon for Pterodactyl",
Long: "Wings is the next generation server control daemon for Pterodactyl",
Run: run,
}
// Entrypoint of the application. Currently just boots up the cobra command
// and lets that handle everything else.
func main() {
RootCommand.Flags().StringVarP(&configPath, "config", "c", "./config.yml", "Allows to set the path of the configuration file.")
if err := RootCommand.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// Bootstraps the application and beging the process of running the API and
// server instances.
func run(cmd *cobra.Command, args []string) {
utils.InitLogging()
logrus.Info("Booting configuration file...")
if err := config.LoadConfiguration(configPath); err != nil {
logrus.WithError(err).Fatal("Could not locate a suitable config.yml file for this Daemon.")
}
logrus.Info("Configuration successfully loaded, booting application.")
utils.ConfigureLogging()
logrus.Info(` ____`)
logrus.Info(`__ Pterodactyl _____/___/_______ _______ ______`)
logrus.Info(`\_____\ \/\/ / / / __ / ___/`)
logrus.Info(` \___\ / / / / /_/ /___ /`)
logrus.Info(` \___/\___/___/___/___/___ /______/`)
logrus.Info(` /_______/ v` + constants.Version)
logrus.Info()
logrus.Info("Loading configured servers.")
if err := control.LoadServerConfigurations(filepath.Join(viper.GetString(config.DataPath), constants.ServersPath)); err != nil {
logrus.WithError(err).Error("Failed to load configured servers.")
}
if amount := len(control.GetServers()); amount == 1 {
logrus.Info("Found and loaded " + strconv.Itoa(amount) + " server(s).")
}
logrus.Info("Registering API server and booting.")
a := api.InternalAPI{}
a.Listen()
}

View File

@ -3,12 +3,13 @@ package utils
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"time"
//"time" //"time"
"github.com/pterodactyl/wings/constants" "github.com/pterodactyl/wings/constants"
//"github.com/lestrrat/go-file-rotatelogs" rotatelogs "github.com/lestrrat-go/file-rotatelogs"
//"github.com/rifflock/lfshook" "github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -30,20 +31,23 @@ func ConfigureLogging() error {
if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil { if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil {
return err return err
} }
//writer := rotatelogs.New( writer, err := rotatelogs.New(
// path+"/wings.%Y%m%d-%H%M.log", path+"/wings.%Y%m%d-%H%M.log",
// rotatelogs.WithLinkName(path), rotatelogs.WithLinkName(path),
// rotatelogs.WithMaxAge(time.Duration(viper.GetInt(config.LogDeleteAfterDays))*time.Hour*24), rotatelogs.WithMaxAge(time.Duration(viper.GetInt(config.LogDeleteAfterDays))*time.Hour*24),
// rotatelogs.WithRotationTime(time.Duration(604800)*time.Second), rotatelogs.WithRotationTime(time.Hour*24),
//) )
// if err != nil {
//log.AddHook(lfshook.NewHook(lfshook.WriterMap{ return err
// log.DebugLevel: writer, }
// log.InfoLevel: writer,
// log.WarnLevel: writer, log.AddHook(lfshook.NewHook(lfshook.WriterMap{
// log.ErrorLevel: writer, log.DebugLevel: writer,
// log.FatalLevel: writer, log.InfoLevel: writer,
//})) log.WarnLevel: writer,
log.ErrorLevel: writer,
log.FatalLevel: writer,
}, &log.JSONFormatter{}))
level := viper.GetString(config.LogLevel) level := viper.GetString(config.LogLevel)

BIN
wings-api.paw Normal file

Binary file not shown.

15
wings.go Normal file
View File

@ -0,0 +1,15 @@
package main
import (
"fmt"
"os"
"github.com/pterodactyl/wings/command"
)
func main() {
if err := command.RootCommand.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}