diff --git a/.dev/vagrant/provision.sh b/.dev/vagrant/provision.sh index f473fac..94c732b 100644 --- a/.dev/vagrant/provision.sh +++ b/.dev/vagrant/provision.sh @@ -1,10 +1,10 @@ #!/bin/bash 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 ubuntu:ubuntu /srv +chown -R vagrant:vagrant /home/vagrant/go +chown -R vagrant:vagrant /srv echo "Update apt repositories" sudo add-apt-repository ppa:longsleep/golang-backports @@ -13,24 +13,28 @@ apt-get update > /dev/null echo "Install docker" curl -sSL https://get.docker.com/ | sh systemctl enable docker -usermod -aG docker ubuntu +usermod -aG docker vagrant echo "Install 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 -echo 'export PATH=$PATH:$GOPATH/bin' >> /home/ubuntu/.profile +echo 'export PATH=$PATH:$GOPATH/bin' >> /home/vagrant/.profile 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" -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" 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 "Gopath is /home/ubuntu/go" -echo "The project is mounted to /home/ubuntu/go/src/github.com/pterodactyl/wings.go" +echo "Gopath is /home/vagrant/go" +echo "The project is mounted to /home/vagrant/go/src/github.com/pterodactyl/wings" echo "Provisioning is completed." diff --git a/.gitignore b/.gitignore index 281344a..a92947b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ wings.exe # IDE/Editor files (VS Code) /.vscode +# test files +test_*/ # Keep all gitkeep files (This needs to stay at the bottom) !.gitkeep diff --git a/.travis.yml b/.travis.yml index 9347948..0fd2fd6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,23 +6,17 @@ go: services: - docker -addons: - apt: - sources: - - sourceline: 'ppa:masterminds/glide' - packages: - - glide - install: - 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/haya14busa/goverage - go get github.com/schrej/godacov -# Install project dependencies with glide -- glide install +# Install project dependencies with dep +- dep ensure script: - make cross-build diff --git a/Gopkg.lock b/Gopkg.lock index f35a959..9bfca83 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,15 +1,6 @@ # 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]] name = "github.com/Microsoft/go-winio" packages = ["."] @@ -17,22 +8,10 @@ version = "v0.4.7" [[projects]] - branch = "master" - name = "github.com/Nvveen/Gotty" - packages = ["."] - revision = "cd527374f1e5bff4938207604a14f2e38a9cf512" - -[[projects]] - branch = "master" name = "github.com/StackExchange/wmi" packages = ["."] revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338" - -[[projects]] - branch = "master" - name = "github.com/containerd/continuity" - packages = ["pathdriver"] - revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371" + version = "1.0.0" [[projects]] name = "github.com/davecgh/go-spew" @@ -40,40 +19,45 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + branch = "master" + name = "github.com/docker/distribution" + packages = [ + "digestset", + "reference" + ] + revision = "83389a148052d74ac602f5f1d62f86ff2f3c4aa5" + [[projects]] name = "github.com/docker/docker" packages = [ + "api", "api/types", "api/types/blkiodev", "api/types/container", + "api/types/events", "api/types/filters", + "api/types/image", "api/types/mount", "api/types/network", "api/types/registry", "api/types/strslice", "api/types/swarm", "api/types/swarm/runtime", + "api/types/time", "api/types/versions", - "opts", - "pkg/archive", - "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" + "api/types/volume", + "client" ] - revision = "fe8aac6f5ae413a967adb0adad0b54abdfb825c4" + revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd" [[projects]] name = "github.com/docker/go-connections" - packages = ["nat"] + packages = [ + "nat", + "sockets", + "tlsconfig" + ] revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d" version = "v0.3.0" @@ -89,12 +73,6 @@ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" -[[projects]] - name = "github.com/fsouza/go-dockerclient" - packages = ["."] - revision = "2ff310040c161b75fa19fb9b287a90a6e03c0012" - version = "1.1" - [[projects]] branch = "master" name = "github.com/gin-contrib/sse" @@ -132,6 +110,18 @@ revision = "925541529c1fa6821df4e44ce2723319eb2be768" 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]] branch = "master" name = "github.com/hashicorp/hcl" @@ -139,6 +129,7 @@ ".", "hcl/ast", "hcl/parser", + "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -146,7 +137,7 @@ "json/scanner", "json/token" ] - revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" [[projects]] name = "github.com/inconshreveable/mousetrap" @@ -154,6 +145,18 @@ revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 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]] name = "github.com/magiconair/properties" packages = ["."] @@ -170,7 +173,7 @@ branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "a4e142e9c047c904fa2f1e144d9a84e6133024bc" + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" [[projects]] name = "github.com/opencontainers/go-digest" @@ -187,15 +190,6 @@ revision = "d60099175f88c47cd379c4738d158884749ed235" version = "v1.0.1" -[[projects]] - name = "github.com/opencontainers/runc" - packages = [ - "libcontainer/system", - "libcontainer/user" - ] - revision = "baf6536d6259209c3edfa2b22237af82942d3dfa" - version = "v0.1.1" - [[projects]] name = "github.com/pelletier/go-toml" packages = ["."] @@ -214,29 +208,26 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + name = "github.com/rifflock/lfshook" + packages = ["."] + revision = "bf539943797a1f34c1f502d07de419b5238ae6c6" + version = "v2.3" + [[projects]] name = "github.com/shirou/gopsutil" packages = [ "cpu", - "host", - "internal/common", - "mem", - "net", - "process" + "internal/common" ] - revision = "543a05cce094293c7747322720256bee15d88a12" - -[[projects]] - branch = "master" - name = "github.com/shirou/w32" - packages = ["."] - revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b" + revision = "5776ff9c7c5d063d574ef53d740f75c68b448e53" + version = "v2.18.02" [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] - revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" - version = "v1.0.4" + revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" + version = "v1.0.5" [[projects]] name = "github.com/spf13/afero" @@ -256,8 +247,8 @@ [[projects]] name = "github.com/spf13/cobra" packages = ["."] - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" + revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4" + version = "v0.0.2" [[projects]] branch = "master" @@ -274,8 +265,8 @@ [[projects]] name = "github.com/spf13/viper" packages = ["."] - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" - version = "v1.0.0" + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" [[projects]] name = "github.com/stretchr/testify" @@ -293,16 +284,17 @@ branch = "master" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" + revision = "12892e8c234f4fe6f6803f052061de9057903bb2" [[projects]] branch = "master" name = "golang.org/x/net" packages = [ "context", - "context/ctxhttp" + "context/ctxhttp", + "proxy" ] - revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" + revision = "b68f30494add4df6bd8ef5e82803f308e7f7c59c" [[projects]] branch = "master" @@ -311,10 +303,9 @@ "unix", "windows" ] - revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" + revision = "378d26f46672a356c46195c28f61bdb4c0a781dd" [[projects]] - branch = "master" name = "golang.org/x/text" packages = [ "internal/gen", @@ -324,7 +315,8 @@ "unicode/cldr", "unicode/norm" ] - revision = "4e4a3210bb54bb31f6ab2cdca2edcc0b50c420c1" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" [[projects]] name = "gopkg.in/go-playground/validator.v8" @@ -333,14 +325,14 @@ version = "v8.18.2" [[projects]] - branch = "v2" name = "gopkg.in/yaml.v2" packages = ["."] - revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "3f1bdf5882e27f292b13d4b95c9f51eb1a7609af61ccda0fae50a806b0a2ba4f" + inputs-digest = "8e17495db05ff2c85d228a20157c41c223e428d28217e14f89ab7b764a8706dd" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index b165b96..c4a2c68 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,6 +1,6 @@ # 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. # # required = ["github.com/user/thing/cmd/thing"] @@ -25,33 +25,38 @@ # unused-packages = true -#[[constraint]] -# branch = "develop" -# name = "github.com/pterodactyl/wings" + +[[constraint]] + 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]] name = "github.com/gin-gonic/gin" version = "1.2.0" [[constraint]] - name = "github.com/lestrrat/go-file-rotatelogs" + name = "github.com/lestrrat-go/file-rotatelogs" version = "2.1.0" [[constraint]] name = "github.com/rifflock/lfshook" version = "2.2.0" -[[constraint]] - name = "github.com/shirou/gopsutil" - revision = "543a05cce094293c7747322720256bee15d88a12" - -[[constraint]] - name = "github.com/sirupsen/logrus" - version = "1.0.4" - [[constraint]] name = "github.com/spf13/cobra" - version = "0.0.1" + version = "0.0.2" [[constraint]] name = "github.com/spf13/viper" @@ -64,3 +69,11 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/docker/docker" + revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd" + +[[override]] + name = "github.com/docker/distribution" + branch = "master" diff --git a/Vagrantfile b/Vagrantfile index d98475d..22bcea7 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,7 +1,7 @@ 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" diff --git a/api/api.go b/api/api.go index f54d7f6..7fde00a 100644 --- a/api/api.go +++ b/api/api.go @@ -2,7 +2,6 @@ package api import ( "fmt" - "net/http" "github.com/gin-gonic/gin" "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)) api.router.Run(l) - logrus.Info("Now listening on %s", l) - logrus.Fatal(http.ListenAndServe(l, nil)) + 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 @@ -53,7 +51,7 @@ func (api *InternalAPI) Listen() { func (api *InternalAPI) register() { v1 := api.router.Group("/api/v1") { - v1.GET("/", AuthHandler(""), GetIndex) + v1.GET("", AuthHandler(""), GetIndex) //v1.PATCH("/config", AuthHandler("c:config"), PatchConfiguration) v1.GET("/servers", AuthHandler("c:list"), handleGetServers) @@ -61,15 +59,15 @@ func (api *InternalAPI) register() { 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.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"), handleGetServerLog) + v1ServerRoutes.GET("/log", AuthHandler("s:console"), handleGetConsole) v1ServerRoutes.POST("/suspend", AuthHandler(""), handlePostServerSuspend) v1ServerRoutes.POST("/unsuspend", AuthHandler(""), handlePostServerUnsuspend) } diff --git a/api/auth.go b/api/auth.go index d958b1f..f19efee 100644 --- a/api/auth.go +++ b/api/auth.go @@ -2,8 +2,12 @@ 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" @@ -62,15 +66,25 @@ func (a *authorizationManager) HasPermission(permission string) bool { // 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 != "" { - log.Debug("Token missing in request.") - c.JSON(http.StatusBadRequest, responseError{"Missing required " + accessTokenHeader + " header."}) + sendErrors(c, http.StatusUnauthorized, &jsonapi.ErrorObject{ + Title: "Missing required " + accessTokenHeader + " header or token param.", + Status: strconv.Itoa(http.StatusUnauthorized), + }) c.Abort() return } @@ -90,7 +104,7 @@ func AuthHandler(permission string) gin.HandlerFunc { return } - c.JSON(http.StatusForbidden, responseError{"You do not have permission to perform this action."}) + sendForbidden(c) c.Abort() } } @@ -107,16 +121,3 @@ func GetContextAuthManager(c *gin.Context) AuthorizationManager { } 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 -} diff --git a/api/handlers.go b/api/handlers.go index 469f271..61bacd4 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -6,9 +6,6 @@ import ( //"runtime" "github.com/gin-gonic/gin" - //"github.com/pterodactyl/wings/constants" - //"github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/cpu" //"github.com/shirou/gopsutil/host" //"github.com/shirou/gopsutil/mem" @@ -128,7 +125,7 @@ type incomingConfiguration struct { } // handlePatchConfig handles PATCH /config -func handlePatchConfig(c *gin.Context) { +func PatchConfiguration(c *gin.Context) { // reqBody, err := ioutil.ReadAll(c.Request.Body) // if err != nil { // log.WithError(err).Error("Failed to read input.") diff --git a/api/handlers_server.go b/api/handlers_server.go index 09b10c3..3107f12 100644 --- a/api/handlers_server.go +++ b/api/handlers_server.go @@ -3,25 +3,29 @@ 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 -// TODO: make jsonapi compliant func handleGetServers(c *gin.Context) { servers := control.GetServers() - c.JSON(http.StatusOK, servers) + sendData(c, servers) } // POST /servers -// TODO: make jsonapi compliant 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.") - c.Status(http.StatusBadRequest) + sendErrors(c, http.StatusBadRequest, &jsonapi.ErrorObject{ + Status: strconv.Itoa(http.StatusBadRequest), + Title: "The passed server object is invalid.", + }) return } var srv control.Server @@ -30,10 +34,14 @@ func handlePostServers(c *gin.Context) { 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.") - c.Status(http.StatusInternalServerError) + sendInternalError(c, "Failed to create the server", "") return } go func() { @@ -43,19 +51,22 @@ func handlePostServers(c *gin.Context) { } env.Create() }() - c.JSON(http.StatusOK, srv) + sendDataStatus(c, http.StatusCreated, srv) } // GET /servers/:server -// TODO: make jsonapi compliant func handleGetServer(c *gin.Context) { id := c.Param("server") server := control.GetServer(id) 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 } - c.JSON(http.StatusOK, server) + sendData(c, server) } // PATCH /servers/:server @@ -64,7 +75,6 @@ func handlePatchServer(c *gin.Context) { } // DELETE /servers/:server -// TODO: make jsonapi compliant func handleDeleteServer(c *gin.Context) { id := c.Param("server") server := control.GetServer(id) @@ -75,18 +85,21 @@ func handleDeleteServer(c *gin.Context) { env, err := server.Environment() 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 { 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.") - c.Status(http.StatusInternalServerError) + sendInternalError(c, "The server could not be deleted.", "") return } - c.Status(http.StatusOK) + c.Status(http.StatusNoContent) } func handlePostServerReinstall(c *gin.Context) { @@ -102,7 +115,6 @@ func handlePostServerRebuild(c *gin.Context) { } // POST /servers/:server/power -// TODO: make jsonapi compliant func handlePostServerPower(c *gin.Context) { server := getServerFromContext(c) if server == nil { @@ -112,7 +124,7 @@ func handlePostServerPower(c *gin.Context) { auth := GetContextAuthManager(c) if auth == nil { - c.Status(http.StatusInternalServerError) + sendInternalError(c, "An internal error occured.", "") return } @@ -120,7 +132,7 @@ func handlePostServerPower(c *gin.Context) { case "start": { if !auth.HasPermission("s:power:start") { - c.Status(http.StatusForbidden) + sendForbidden(c) return } server.Start() @@ -128,7 +140,7 @@ func handlePostServerPower(c *gin.Context) { case "stop": { if !auth.HasPermission("s:power:stop") { - c.Status(http.StatusForbidden) + sendForbidden(c) return } server.Stop() @@ -136,7 +148,7 @@ func handlePostServerPower(c *gin.Context) { case "restart": { if !auth.HasPermission("s:power:restart") { - c.Status(http.StatusForbidden) + sendForbidden(c) return } server.Restart() @@ -144,7 +156,7 @@ func handlePostServerPower(c *gin.Context) { case "kill": { if !auth.HasPermission("s:power:kill") { - c.Status(http.StatusForbidden) + sendForbidden(c) return } server.Kill() @@ -157,15 +169,16 @@ func handlePostServerPower(c *gin.Context) { } // POST /servers/:server/command -// TODO: make jsonapi compliant func handlePostServerCommand(c *gin.Context) { server := getServerFromContext(c) cmd := c.Query("command") 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) { diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..678c589 --- /dev/null +++ b/api/routes.go @@ -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) + //} + } +} diff --git a/api/utils.go b/api/utils.go index c748c3a..9f00adb 100644 --- a/api/utils.go +++ b/api/utils.go @@ -1,10 +1,45 @@ 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) +} diff --git a/api/websockets/collection.go b/api/websockets/collection.go new file mode 100644 index 0000000..dc5425b --- /dev/null +++ b/api/websockets/collection.go @@ -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 +} diff --git a/api/websockets/message.go b/api/websockets/message.go new file mode 100644 index 0000000..8ce2230 --- /dev/null +++ b/api/websockets/message.go @@ -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, + }, + } +} diff --git a/api/websockets/socket.go b/api/websockets/socket.go new file mode 100644 index 0000000..fe977cd --- /dev/null +++ b/api/websockets/socket.go @@ -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 + } + } + } +} diff --git a/command/root.go b/command/root.go new file mode 100644 index 0000000..124c2f3 --- /dev/null +++ b/command/root.go @@ -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() +} diff --git a/config/config_test.go b/config/config_test.go index 1773208..7450c44 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -const configFile = "../config.yml.example" +const configFile = "../config.example.yml" func TestLoadConfiguraiton(t *testing.T) { err := LoadConfiguration(configFile) diff --git a/config/keys.go b/config/keys.go index fcf6304..a7076da 100644 --- a/config/keys.go +++ b/config/keys.go @@ -6,7 +6,7 @@ const ( // DataPath is a string containing the path where data should // be stored on the system - DataPath = "datapath" + DataPath = "data" // APIHost is a string containing the interface ip address // on what the api should listen on diff --git a/constants/constants.go b/constants/constants.go index 29c8dfb..614262e 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -26,3 +26,10 @@ const ServerDataPath = "data" // JSONIndent is the indent to use with the json.MarshalIndent() function. 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 diff --git a/control/console_handler.go b/control/console_handler.go new file mode 100644 index 0000000..cc88ea3 --- /dev/null +++ b/control/console_handler.go @@ -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 +} diff --git a/control/docker_environment.go b/control/docker_environment.go deleted file mode 100644 index 03482bd..0000000 --- a/control/docker_environment.go +++ /dev/null @@ -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 -} diff --git a/control/environment_docker.go b/control/environment_docker.go new file mode 100644 index 0000000..1d7daec --- /dev/null +++ b/control/environment_docker.go @@ -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 +} diff --git a/control/docker_environment_test.go b/control/environment_docker_test.go similarity index 59% rename from control/docker_environment_test.go rename to control/environment_docker_test.go index 4564ac3..f5ce62e 100644 --- a/control/docker_environment_test.go +++ b/control/environment_docker_test.go @@ -1,19 +1,28 @@ package control import ( + "context" "fmt" "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" ) func testServer() *ServerStruct { + viper.SetDefault(config.DataPath, "./test_data") return &ServerStruct{ ID: "testuuid-something-something", - service: &service{ + Service: &Service{ 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.NotNil(t, env) - assert.NotNil(t, env.container) + assert.NotNil(t, env.server.DockerContainer) eenv.Destroy() } @@ -45,12 +54,9 @@ func TestCreateDockerEnvironment(t *testing.T) { a := assert.New(t) a.Nil(err) - a.NotNil(env.container) - a.Equal(env.container.Name, "ptdl_testuuid") + a.NotNil(env.server.DockerContainer.ID) - if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ - ID: env.container.ID, - }); err != nil { + if err := env.client.ContainerRemove(context.TODO(), env.server.DockerContainer.ID, types.ContainerRemoveOptions{}); err != nil { fmt.Println(err) } } @@ -61,10 +67,10 @@ func TestDestroyDockerEnvironment(t *testing.T) { 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.IsType(t, ierr, &docker.NoSuchContainer{}) + assert.True(t, client.IsErrNotFound(ierr)) } func TestStartDockerEnvironment(t *testing.T) { @@ -72,15 +78,13 @@ func TestStartDockerEnvironment(t *testing.T) { env.Create() 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, ierr) assert.True(t, i.State.Running) - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") env.Destroy() } @@ -90,15 +94,13 @@ func TestStopDockerEnvironment(t *testing.T) { env.Start() 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, ierr) assert.False(t, i.State.Running) - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") env.Destroy() } @@ -108,15 +110,13 @@ func TestKillDockerEnvironment(t *testing.T) { env.Start() 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, ierr) assert.False(t, i.State.Running) - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") env.Destroy() } diff --git a/control/server.go b/control/server.go index 289fa04..c21fcd3 100644 --- a/control/server.go +++ b/control/server.go @@ -1,17 +1,19 @@ package control import ( - "encoding/json" "errors" - "io/ioutil" - "os" - "path/filepath" - "strings" - "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/constants" + "github.com/pterodactyl/wings/api/websockets" 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. @@ -35,6 +37,7 @@ type Server interface { Save() error Environment() (Environment, error) + Websockets() *websockets.Collection HasPermission(string, string) bool } @@ -42,32 +45,36 @@ type Server interface { // ServerStruct is a single instance of a Service managed by the panel type ServerStruct struct { // 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 // in the config - ServiceName string `json:"serviceName"` - service *Service + ServiceName string `json:"serviceName"` + Service *Service `json:"-" jsonapi:"relation,service"` environment Environment // 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 // 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 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 allocations `json:"allocation"` + Allocations allocations `json:"allocation" jsonapi:"attr,allocations"` // 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. + // TODO remove Keys map[string][]string `json:"keys"` + + websockets *websockets.Collection + status Status } type allocations struct { @@ -99,73 +106,6 @@ type serversMap map[string]*ServerStruct 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 func GetServers() []Server { serverArray := make([]Server, len(servers)) @@ -193,6 +133,11 @@ func CreateServer(server *ServerStruct) (Server, error) { } servers[server.ID] = server 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 server, nil @@ -208,13 +153,40 @@ func DeleteServer(id string) error { 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 { + s.SetStatus(StatusStarting) env, err := s.Environment() if err != nil { + s.SetStatus(StatusStopped) return err } if !env.Exists() { if err := env.Create(); err != nil { + s.SetStatus(StatusStopped) return err } } @@ -222,8 +194,10 @@ func (s *ServerStruct) Start() error { } func (s *ServerStruct) Stop() error { + s.SetStatus(StatusStopping) env, err := s.Environment() if err != nil { + s.SetStatus(StatusRunning) return err } return env.Stop() @@ -259,70 +233,3 @@ func (s *ServerStruct) Rebuild() error { } 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) -} diff --git a/control/server_persistence.go b/control/server_persistence.go new file mode 100644 index 0000000..14c2520 --- /dev/null +++ b/control/server_persistence.go @@ -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) +} diff --git a/control/server_util.go b/control/server_util.go new file mode 100644 index 0000000..d5d6ada --- /dev/null +++ b/control/server_util.go @@ -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 +} diff --git a/control/service.go b/control/service.go index e982e7c..8b7f06c 100644 --- a/control/service.go +++ b/control/service.go @@ -4,7 +4,7 @@ type Service struct { server *Server // 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"` } diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..5368cd9 --- /dev/null +++ b/glide.lock @@ -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 diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..e79a056 --- /dev/null +++ b/glide.yaml @@ -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 diff --git a/main.go b/main.go deleted file mode 100644 index 4e1e7e8..0000000 --- a/main.go +++ /dev/null @@ -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() -} diff --git a/utils/logger.go b/utils/logger.go index e988a62..ade097b 100644 --- a/utils/logger.go +++ b/utils/logger.go @@ -3,12 +3,13 @@ package utils import ( "os" "path/filepath" + "time" //"time" "github.com/pterodactyl/wings/constants" - //"github.com/lestrrat/go-file-rotatelogs" - //"github.com/rifflock/lfshook" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/rifflock/lfshook" log "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -30,20 +31,23 @@ func ConfigureLogging() error { if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil { return err } - //writer := rotatelogs.New( - // path+"/wings.%Y%m%d-%H%M.log", - // rotatelogs.WithLinkName(path), - // rotatelogs.WithMaxAge(time.Duration(viper.GetInt(config.LogDeleteAfterDays))*time.Hour*24), - // rotatelogs.WithRotationTime(time.Duration(604800)*time.Second), - //) - // - //log.AddHook(lfshook.NewHook(lfshook.WriterMap{ - // log.DebugLevel: writer, - // log.InfoLevel: writer, - // log.WarnLevel: writer, - // log.ErrorLevel: writer, - // log.FatalLevel: writer, - //})) + writer, err := rotatelogs.New( + path+"/wings.%Y%m%d-%H%M.log", + rotatelogs.WithLinkName(path), + rotatelogs.WithMaxAge(time.Duration(viper.GetInt(config.LogDeleteAfterDays))*time.Hour*24), + rotatelogs.WithRotationTime(time.Hour*24), + ) + if err != nil { + return err + } + + log.AddHook(lfshook.NewHook(lfshook.WriterMap{ + log.DebugLevel: writer, + log.InfoLevel: writer, + log.WarnLevel: writer, + log.ErrorLevel: writer, + log.FatalLevel: writer, + }, &log.JSONFormatter{})) level := viper.GetString(config.LogLevel) diff --git a/wings-api.paw b/wings-api.paw new file mode 100644 index 0000000..af01f9c Binary files /dev/null and b/wings-api.paw differ diff --git a/wings.go b/wings.go new file mode 100644 index 0000000..99a4704 --- /dev/null +++ b/wings.go @@ -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) + } +}