Compare commits
18 Commits
v1.0.0-alp
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90cdea83d4 | ||
|
|
ab54d2c416 | ||
|
|
59299d3cda | ||
|
|
7533e38543 | ||
|
|
99a11f81c3 | ||
|
|
c6fcd8cabb | ||
|
|
5c3823de9a | ||
|
|
5350a2d5a5 | ||
|
|
6ef2773c01 | ||
|
|
853d215b1d | ||
|
|
3bbb8a3769 | ||
|
|
a51cb79d3b | ||
|
|
beec5723e6 | ||
|
|
d7bd10fcee | ||
|
|
06f495682c | ||
|
|
32e389db21 | ||
|
|
d9ceb9601d | ||
|
|
ea867d16a5 |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Jakob Schrettenbrunner <dev@schrej.net>
|
||||
Copyright (c) 2019 Dane Everitt <dane@daneeveritt.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -41,6 +41,14 @@ type ProcessConfiguration struct {
|
||||
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
||||
}
|
||||
|
||||
// Defines installation script information for a server process. This is used when
|
||||
// a server is installed for the first time, and when a server is marked for re-installation.
|
||||
type InstallationScript struct {
|
||||
ContainerImage string `json:"container_image"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
// Fetches the server configuration and returns the struct for it.
|
||||
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
||||
@@ -64,3 +72,53 @@ func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfiguration
|
||||
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
// Fetches installation information for the server process.
|
||||
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
|
||||
res := InstallationScript{}
|
||||
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
|
||||
if err != nil {
|
||||
return res, nil, errors.WithStack(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
|
||||
if r.HasError() {
|
||||
return res, r.Error(), nil
|
||||
}
|
||||
|
||||
b, _ := r.ReadBody()
|
||||
|
||||
if err := json.Unmarshal(b, &res); err != nil {
|
||||
return res, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
type installRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
}
|
||||
|
||||
// Marks a server as being installed successfully or unsuccessfully on the panel.
|
||||
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
|
||||
b, err := json.Marshal(installRequest{Successful: successful})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ type DockerConfiguration struct {
|
||||
|
||||
// Defines the location of the timezone file on the host system that should
|
||||
// be mounted into the created containers so that they all use the same time.
|
||||
TimezonePath string `yaml:"timezone_path"`
|
||||
TimezonePath string `default:"/etc/timezone" yaml:"timezone_path"`
|
||||
}
|
||||
|
||||
// Defines the configuration for the internal API that is exposed by the
|
||||
|
||||
27
const.go
27
const.go
@@ -1,29 +1,6 @@
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
Version = "1.0.0-alpha.1"
|
||||
|
||||
// DefaultFilePerms are the file perms used for created files.
|
||||
DefaultFilePerms os.FileMode = 0644
|
||||
|
||||
// DefaultFolderPerms are the file perms used for created folders.
|
||||
DefaultFolderPerms os.FileMode = 0755
|
||||
|
||||
// ServersPath is the path of the servers within the configured DataPath.
|
||||
ServersPath = "servers"
|
||||
|
||||
// ServerConfigFile is the filename of the server config file.
|
||||
ServerConfigFile = "server.json"
|
||||
|
||||
// ServerDataPath is the path of the data of a single server.
|
||||
ServerDataPath = "data"
|
||||
|
||||
// DockerContainerPrefix is the prefix used for naming Docker containers.
|
||||
// It's also used to prefix the hostnames of the docker containers.
|
||||
DockerContainerPrefix = "ptdl-"
|
||||
|
||||
// WSMaxMessages is the maximum number of messages that are sent in one transfer.
|
||||
WSMaxMessages = 10
|
||||
// The current version of this software.
|
||||
Version = "1.0.0-alpha.2"
|
||||
)
|
||||
|
||||
4
data/.gitignore
vendored
4
data/.gitignore
vendored
@@ -1 +1,3 @@
|
||||
servers/*.yml
|
||||
servers/*.yml
|
||||
!install_logs/.gitkeep
|
||||
install_logs/*
|
||||
0
data/install_logs/.gitkeep
Normal file
0
data/install_logs/.gitkeep
Normal file
2
go.mod
2
go.mod
@@ -44,7 +44,7 @@ require (
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pkg/sftp v1.10.1 // indirect
|
||||
github.com/pterodactyl/sftp-server v1.1.0
|
||||
github.com/pterodactyl/sftp-server v1.1.1
|
||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||
github.com/sirupsen/logrus v1.0.5 // indirect
|
||||
go.uber.org/atomic v1.5.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -102,6 +102,8 @@ github.com/pterodactyl/sftp-server v1.0.4 h1:hPUaUQvA6U/R8/bybQFDMBDcZaqqj+kufGB
|
||||
github.com/pterodactyl/sftp-server v1.0.4/go.mod h1:0LKDl+f1qY2TH9+B5jxdROktW0+10UM1qJ472iWbyvQ=
|
||||
github.com/pterodactyl/sftp-server v1.1.0 h1:NcYh+UqEH8pfvFsee6yt7eb08RLLidw6q+cNOCdh/V0=
|
||||
github.com/pterodactyl/sftp-server v1.1.0/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
|
||||
github.com/pterodactyl/sftp-server v1.1.1 h1:IjuOy21BNZxfejKnXG1RgLxXAYylDqBVpbKZ6+fG5FQ=
|
||||
github.com/pterodactyl/sftp-server v1.1.1/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
|
||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
|
||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
|
||||
27
http.go
27
http.go
@@ -420,6 +420,19 @@ func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request,
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
s := rt.GetServer(ps.ByName("server"))
|
||||
defer r.Body.Close()
|
||||
|
||||
go func (serv *server.Server) {
|
||||
if err := serv.Install(); err != nil {
|
||||
zap.S().Errorw("failed to execute server installation process", zap.String("server", s.Uuid), zap.Error(err))
|
||||
}
|
||||
}(s)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
s := rt.GetServer(ps.ByName("server"))
|
||||
defer r.Body.Close()
|
||||
@@ -451,11 +464,17 @@ func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps h
|
||||
// requests from here-on out.
|
||||
server.GetServers().Add(inst.Server())
|
||||
|
||||
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
|
||||
// Begin the installation process in the background to not block the request
|
||||
// cycle. If there are any errors they will be logged and communicated back
|
||||
// to the Panel where a reinstall may take place.
|
||||
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
|
||||
go inst.Execute()
|
||||
go func(i *installer.Installer) {
|
||||
i.Execute()
|
||||
|
||||
if err := i.Server().Install(); err != nil {
|
||||
zap.S().Errorw("failed to run install process for server", zap.String("server", i.Uuid()), zap.Error(err))
|
||||
}
|
||||
}(inst)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
@@ -482,6 +501,7 @@ func (rt *Router) routeServerDelete(w http.ResponseWriter, r *http.Request, ps h
|
||||
// to start it while this process is running.
|
||||
s.Suspended = true
|
||||
|
||||
zap.S().Infow("processing server deletion request", zap.String("server", s.Uuid))
|
||||
// Destroy the environment; in Docker this will handle a running container and
|
||||
// forcibly terminate it before removing the container, so we do not need to handle
|
||||
// that here.
|
||||
@@ -545,7 +565,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
|
||||
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
|
||||
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
|
||||
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer));
|
||||
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer))
|
||||
router.POST("/api/servers/:server/install", rt.AuthenticateRequest(rt.routeServerInstall))
|
||||
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
|
||||
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
|
||||
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
@@ -102,6 +104,18 @@ func (i *Installer) Server() *server.Server {
|
||||
// associated installation process based on the parameters passed through for
|
||||
// the server instance.
|
||||
func (i *Installer) Execute() {
|
||||
zap.S().Debugw("creating required server data directory", zap.String("server", i.Uuid()))
|
||||
if err := os.MkdirAll(path.Join(config.Get().System.Data, i.Uuid()), 0755); err != nil {
|
||||
zap.S().Errorw("failed to create server data directory", zap.String("server", i.Uuid()), zap.Error(errors.WithStack(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Chown(path.Join(config.Get().System.Data, i.Uuid()), config.Get().System.User.Uid, config.Get().System.User.Gid); err != nil {
|
||||
zap.S().Errorw("failed to chown server data directory", zap.String("server", i.Uuid()), zap.Error(errors.WithStack(err)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
zap.S().Debugw("creating required environment for server instance", zap.String("server", i.Uuid()))
|
||||
if err := i.server.Environment.Create(); err != nil {
|
||||
zap.S().Errorw("failed to create environment for server", zap.String("server", i.Uuid()), zap.Error(err))
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package server
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Console struct {
|
||||
Server *Server
|
||||
Server *Server
|
||||
HandlerFunc *func(string)
|
||||
}
|
||||
|
||||
@@ -18,4 +22,13 @@ func (c Console) Write(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Sends output to the server console formatted to appear correctly as being sent
|
||||
// from Wings.
|
||||
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
||||
s.Events().Publish(
|
||||
ConsoleOutputEvent,
|
||||
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (s *Server) handleServerCrash() error {
|
||||
if !s.CrashDetection.Enabled {
|
||||
zap.S().Debugw("server triggered crash detection but handler is disabled for server process", zap.String("server", s.Uuid))
|
||||
|
||||
s.SendConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
|
||||
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -56,15 +56,15 @@ func (s *Server) handleServerCrash() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.SendConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
|
||||
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
|
||||
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
|
||||
s.PublishConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
|
||||
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
|
||||
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
|
||||
|
||||
c := s.CrashDetection.lastCrash
|
||||
// If the last crash time was within the last 60 seconds we do not want to perform
|
||||
// an automatic reboot of the process. Return an error that can be handled.
|
||||
if !c.IsZero() && c.Add(time.Second * 60).After(time.Now()) {
|
||||
s.SendConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
|
||||
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
|
||||
|
||||
return &crashTooFrequent{}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
@@ -26,13 +27,6 @@ import (
|
||||
type DockerEnvironment struct {
|
||||
Server *Server
|
||||
|
||||
// The user ID that containers should be running as.
|
||||
User int
|
||||
|
||||
// Defines the configuration for the Docker instance that will allow us to connect
|
||||
// and create and modify containers.
|
||||
TimezonePath string
|
||||
|
||||
// The Docker client being used for this instance.
|
||||
Client *client.Client
|
||||
|
||||
@@ -51,22 +45,18 @@ type DockerEnvironment struct {
|
||||
}
|
||||
|
||||
// Creates a new base Docker environment. A server must still be attached to it.
|
||||
func NewDockerEnvironment(opts ...func(*DockerEnvironment)) (*DockerEnvironment, error) {
|
||||
func NewDockerEnvironment(server *Server) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
env := &DockerEnvironment{
|
||||
User: 1000,
|
||||
server.Environment = &DockerEnvironment{
|
||||
Server: server,
|
||||
Client: cli,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(env)
|
||||
}
|
||||
|
||||
return env, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that the Docker environment is always implementing all of the methods
|
||||
@@ -210,11 +200,8 @@ func (d *DockerEnvironment) Start() error {
|
||||
// No reason to try starting a container that is already running.
|
||||
if c.State.Running {
|
||||
d.Server.SetState(ProcessRunningState)
|
||||
if !d.attached {
|
||||
return d.Attach()
|
||||
}
|
||||
|
||||
return nil
|
||||
return d.Attach()
|
||||
}
|
||||
|
||||
d.Server.SetState(ProcessStartingState)
|
||||
@@ -305,6 +292,9 @@ func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
||||
func (d *DockerEnvironment) Destroy() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Avoid crash detection firing off.
|
||||
d.Server.SetState(ProcessStoppingState)
|
||||
|
||||
return d.Client.ContainerRemove(ctx, d.Server.Uuid, types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
RemoveLinks: false,
|
||||
@@ -354,8 +344,12 @@ func (d *DockerEnvironment) Attach() error {
|
||||
Server: d.Server,
|
||||
}
|
||||
|
||||
d.EnableResourcePolling()
|
||||
d.attached = true
|
||||
go func() {
|
||||
if err := d.EnableResourcePolling(); err != nil {
|
||||
zap.S().Warnw("failed to enabled resource polling on server", zap.String("server", d.Server.Uuid), zap.Error(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer d.stream.Close()
|
||||
@@ -397,7 +391,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
|
||||
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
d.Server.Emit(ConsoleOutputEvent, s.Text())
|
||||
d.Server.Events().Publish(ConsoleOutputEvent, s.Text())
|
||||
}
|
||||
|
||||
if err := s.Err(); err != nil {
|
||||
@@ -430,7 +424,10 @@ func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||
var v *types.StatsJSON
|
||||
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
zap.S().Warnw("encountered error processing server stats; stopping collection", zap.Error(err))
|
||||
if err != io.EOF {
|
||||
zap.S().Warnw("encountered error processing server stats; stopping collection", zap.Error(err))
|
||||
}
|
||||
|
||||
d.DisableResourcePolling()
|
||||
return
|
||||
}
|
||||
@@ -456,7 +453,7 @@ func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(s.Resources)
|
||||
s.Emit(StatsEvent, string(b))
|
||||
s.Events().Publish(StatsEvent, string(b))
|
||||
}
|
||||
}(d.Server)
|
||||
|
||||
@@ -537,7 +534,7 @@ func (d *DockerEnvironment) Create() error {
|
||||
|
||||
conf := &container.Config{
|
||||
Hostname: "container",
|
||||
User: strconv.Itoa(d.User),
|
||||
User: strconv.Itoa(config.Get().System.User.Uid),
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@@ -550,7 +547,8 @@ func (d *DockerEnvironment) Create() error {
|
||||
Env: d.environmentVariables(),
|
||||
|
||||
Labels: map[string]string{
|
||||
"Service": "Pterodactyl",
|
||||
"Service": "Pterodactyl",
|
||||
"ContainerType": "server_process",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -558,9 +556,7 @@ func (d *DockerEnvironment) Create() error {
|
||||
PortBindings: d.portBindings(),
|
||||
|
||||
// Configure the mounts for this container. First mount the server data directory
|
||||
// into the container as a r/w bind. Additionally mount the host timezone data into
|
||||
// the container as a readonly bind so that software running in the container uses
|
||||
// the same time as the host system.
|
||||
// into the container as a r/w bind.
|
||||
Mounts: []mount.Mount{
|
||||
{
|
||||
Target: "/home/container",
|
||||
@@ -568,12 +564,6 @@ func (d *DockerEnvironment) Create() error {
|
||||
Type: mount.TypeBind,
|
||||
ReadOnly: false,
|
||||
},
|
||||
{
|
||||
Target: d.TimezonePath,
|
||||
Source: d.TimezonePath,
|
||||
Type: mount.TypeBind,
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Configure the /tmp folder mapping in containers. This is necessary for some
|
||||
@@ -610,6 +600,17 @@ func (d *DockerEnvironment) Create() error {
|
||||
NetworkMode: "pterodactyl_nw",
|
||||
}
|
||||
|
||||
// Pretty sure TZ=X in the environment variables negates the need for this
|
||||
// to happen. Leaving it until I can confirm that works for everything.
|
||||
//
|
||||
// if err := mountTimezoneData(hostConf); err != nil {
|
||||
// if os.IsNotExist(err) {
|
||||
// zap.S().Warnw("the timezone data path configured does not exist on the system", zap.Error(errors.WithStack(err)))
|
||||
// } else {
|
||||
// zap.S().Warnw("failed to mount timezone data into container", zap.Error(errors.WithStack(err)))
|
||||
// }
|
||||
// }
|
||||
|
||||
if _, err := cli.ContainerCreate(ctx, conf, hostConf, nil, d.Server.Uuid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@@ -617,6 +618,30 @@ func (d *DockerEnvironment) Create() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Given a host configuration mount, also mount the timezone data into it.
|
||||
func mountTimezoneData(c *container.HostConfig) error {
|
||||
p := config.Get().System.TimezonePath
|
||||
|
||||
// Check for the timezone file, if it exists use it assuming it isn't also a directory,
|
||||
// otherwise bubble the error back up the stack.
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if s.IsDir() {
|
||||
return errors.New("attempting to mount directory as timezone file")
|
||||
}
|
||||
}
|
||||
|
||||
c.Mounts = append(c.Mounts, mount.Mount{
|
||||
Target: p,
|
||||
Source: p,
|
||||
Type: mount.TypeBind,
|
||||
ReadOnly: true,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sends the specified command to the stdin of the running container instance. There is no
|
||||
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
||||
func (d *DockerEnvironment) SendCommand(c string) error {
|
||||
@@ -708,7 +733,10 @@ func (d *DockerEnvironment) parseLogToStrings(b []byte) ([]string, error) {
|
||||
|
||||
// Returns the environment variables for a server in KEY="VALUE" form.
|
||||
func (d *DockerEnvironment) environmentVariables() []string {
|
||||
zone, _ := time.Now().In(time.Local).Zone()
|
||||
|
||||
var out = []string{
|
||||
fmt.Sprintf("TZ=%s", zone),
|
||||
fmt.Sprintf("STARTUP=%s", d.Server.Invocation),
|
||||
fmt.Sprintf("SERVER_MEMORY=%d", d.Server.Build.MemoryLimit),
|
||||
fmt.Sprintf("SERVER_IP=%s", d.Server.Allocations.DefaultMapping.Ip),
|
||||
|
||||
@@ -1,63 +1,76 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EventListeners map[string][]EventListenerFunction
|
||||
|
||||
type EventListenerFunction *func(string)
|
||||
|
||||
// Defines all of the possible output events for a server.
|
||||
// noinspection GoNameStartsWithPackageName
|
||||
const (
|
||||
DaemonMessageEvent = "daemon message"
|
||||
InstallOutputEvent = "install output"
|
||||
ConsoleOutputEvent = "console output"
|
||||
StatusEvent = "status"
|
||||
StatsEvent = "stats"
|
||||
)
|
||||
|
||||
// Adds an event listener for the server instance.
|
||||
func (s *Server) AddListener(event string, f EventListenerFunction) {
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[string][]EventListenerFunction)
|
||||
type Event struct {
|
||||
Data string
|
||||
Topic string
|
||||
}
|
||||
|
||||
type EventBus struct {
|
||||
subscribers map[string][]chan Event
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Returns the server's emitter instance.
|
||||
func (s *Server) Events() *EventBus {
|
||||
if s.emitter == nil {
|
||||
s.emitter = &EventBus{
|
||||
subscribers: map[string][]chan Event{},
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := s.listeners[event]; ok {
|
||||
s.listeners[event] = append(s.listeners[event], f)
|
||||
} else {
|
||||
s.listeners[event] = []EventListenerFunction{f}
|
||||
return s.emitter
|
||||
}
|
||||
|
||||
// Publish data to a given topic.
|
||||
func (e *EventBus) Publish(topic string, data string) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if ch, ok := e.subscribers[topic]; ok {
|
||||
go func(data Event, cs []chan Event) {
|
||||
for _, channel := range cs {
|
||||
channel <- data
|
||||
}
|
||||
}(Event{Data: data, Topic: topic}, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Removes the event listener for the server instance.
|
||||
func (s *Server) RemoveListener(event string, f EventListenerFunction) {
|
||||
if _, ok := s.listeners[event]; ok {
|
||||
for i := range s.listeners[event] {
|
||||
if s.listeners[event][i] == f {
|
||||
s.listeners[event] = append(s.listeners[event][:i], s.listeners[event][i+1:]...)
|
||||
break
|
||||
// Subscribe to an emitter topic using a channel.
|
||||
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if p, ok := e.subscribers[topic]; ok {
|
||||
e.subscribers[topic] = append(p, ch)
|
||||
} else {
|
||||
e.subscribers[topic] = append([]chan Event{}, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe a channel from a topic.
|
||||
func (e *EventBus) Unsubscribe(topic string, ch chan Event) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if _, ok := e.subscribers[topic]; ok {
|
||||
for i := range e.subscribers[topic] {
|
||||
if ch == e.subscribers[topic][i] {
|
||||
e.subscribers[topic] = append(e.subscribers[topic][:i], e.subscribers[topic][i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emits an event to all of the active listeners for a server.
|
||||
func (s *Server) Emit(event string, data string) {
|
||||
if _, ok := s.listeners[event]; ok {
|
||||
for _, handler := range s.listeners[event] {
|
||||
go func(f EventListenerFunction, d string) {
|
||||
(*f)(d)
|
||||
}(handler, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sends output to the server console formatted to appear correctly as being sent
|
||||
// from Wings.
|
||||
func (s *Server) SendConsoleOutputFromDaemon(data string) {
|
||||
s.Emit(
|
||||
ConsoleOutputEvent,
|
||||
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
|
||||
)
|
||||
}
|
||||
}
|
||||
410
server/install.go
Normal file
410
server/install.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
||||
// function which should handle contacting the panel to notify it of the server state.
|
||||
func (s *Server) Install() error {
|
||||
err := s.internalInstall()
|
||||
|
||||
zap.S().Debugw("notifying panel of server install state", zap.String("server", s.Uuid))
|
||||
if serr := s.SyncInstallState(err == nil); serr != nil {
|
||||
zap.S().Warnw(
|
||||
"failed to notify panel of server install state",
|
||||
zap.String("server", s.Uuid),
|
||||
zap.Bool("was_successful", err == nil),
|
||||
zap.Error(serr),
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Internal installation function used to simplify reporting back to the Panel.
|
||||
func (s *Server) internalInstall() error {
|
||||
script, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)
|
||||
if err != nil || rerr != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
}
|
||||
|
||||
p, err := NewInstallationProcess(s, &script)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
zap.S().Infow("beginning installation process for server", zap.String("server", s.Uuid))
|
||||
|
||||
if err := p.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Infow("completed installation process for server", zap.String("server", s.Uuid))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InstallationProcess struct {
|
||||
Server *Server
|
||||
Script *api.InstallationScript
|
||||
|
||||
client *client.Client
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
// Generates a new installation process struct that will be used to create containers,
|
||||
// and otherwise perform installation commands for a server.
|
||||
func NewInstallationProcess(s *Server, script *api.InstallationScript) (*InstallationProcess, error) {
|
||||
proc := &InstallationProcess{
|
||||
Script: script,
|
||||
Server: s,
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
} else {
|
||||
proc.client = c
|
||||
}
|
||||
|
||||
return proc, nil
|
||||
}
|
||||
|
||||
// Runs the installation process, this is done as a backgrounded thread. This will configure
|
||||
// the required environment, and then spin up the installation container.
|
||||
//
|
||||
// Once the container finishes installing the results will be stored in an installation
|
||||
// log in the server's configuration directory.
|
||||
func (ip *InstallationProcess) Run() error {
|
||||
installPath, err := ip.BeforeExecute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cid, err := ip.Execute(installPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this step fails, log a warning but don't exit out of the process. This is completely
|
||||
// internal to the daemon's functionality, and does not affect the status of the server itself.
|
||||
if err := ip.AfterExecute(cid); err != nil {
|
||||
zap.S().Warnw("failed to complete after-execute step of installation process", zap.String("server", ip.Server.Uuid), zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writes the installation script to a temporary file on the host machine so that it
|
||||
// can be properly mounted into the installation container and then executed.
|
||||
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
||||
d, err := ioutil.TempDir("", "pterodactyl")
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filepath.Join(d, "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader([]byte(ip.Script.Script)))
|
||||
for scanner.Scan() {
|
||||
w.WriteString(scanner.Text() + "\n")
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Pulls the docker image to be used for the installation container.
|
||||
func (ip *InstallationProcess) pullInstallationImage() error {
|
||||
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Block continuation until the image has been pulled successfully.
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
zap.S().Debugw(scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Runs before the container is executed. This pulls down the required docker container image
|
||||
// as well as writes the installation script to the disk. This process is executed in an async
|
||||
// manner, if either one fails the error is returned.
|
||||
func (ip *InstallationProcess) BeforeExecute() (string, error) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
var e []error
|
||||
var fileName string
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
name, err := ip.writeScriptToDisk()
|
||||
if err != nil {
|
||||
e = append(e, err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName = name
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ip.pullInstallationImage(); err != nil {
|
||||
e = append(e, err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
opts := types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
Force: true,
|
||||
}
|
||||
|
||||
if err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", opts); err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
e = append(e, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Maybe a better way to handle this, but if there is at least one error
|
||||
// just bail out of the process now.
|
||||
if len(e) > 0 {
|
||||
return "", errors.WithStack(e[0])
|
||||
}
|
||||
|
||||
return fileName, nil
|
||||
}
|
||||
|
||||
// Cleans up after the execution of the installation process. This grabs the logs from the
|
||||
// process to store in the server configuration directory, and then destroys the associated
|
||||
// installation container.
|
||||
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
zap.S().Debugw("pulling installation logs for server", zap.String("server", ip.Server.Uuid), zap.String("container_id", containerId))
|
||||
reader, err := ip.client.ContainerLogs(ctx, containerId, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: false,
|
||||
})
|
||||
|
||||
if err != nil && !client.IsErrNotFound(err) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filepath.Join("data/install_logs/", ip.Server.Uuid+".log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// We write the contents of the container output to a more "permanent" file so that they
|
||||
// can be referenced after this container is deleted.
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
zap.S().Debugw("removing server installation container", zap.String("server", ip.Server.Uuid), zap.String("container_id", containerId))
|
||||
rErr := ip.client.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
RemoveLinks: false,
|
||||
Force: true,
|
||||
})
|
||||
|
||||
if rErr != nil && !client.IsErrNotFound(rErr) {
|
||||
return errors.WithStack(rErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Executes the installation process inside a specially created docker container.
|
||||
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
zap.S().Debugw(
|
||||
"creating server installer container",
|
||||
zap.String("server", ip.Server.Uuid),
|
||||
zap.String("script_path", installPath+"/install.sh"),
|
||||
)
|
||||
|
||||
conf := &container.Config{
|
||||
Hostname: "installer",
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
OpenStdin: true,
|
||||
Tty: true,
|
||||
Cmd: []string{ip.Script.Entrypoint, "./mnt/install/install.sh"},
|
||||
Image: ip.Script.ContainerImage,
|
||||
Env: ip.Server.GetEnvironmentVariables(),
|
||||
Labels: map[string]string{
|
||||
"Service": "Pterodactyl",
|
||||
"ContainerType": "server_installer",
|
||||
},
|
||||
}
|
||||
|
||||
hostConf := &container.HostConfig{
|
||||
Mounts: []mount.Mount{
|
||||
{
|
||||
Target: "/mnt/server",
|
||||
Source: ip.Server.Filesystem.Path(),
|
||||
Type: mount.TypeBind,
|
||||
ReadOnly: false,
|
||||
},
|
||||
{
|
||||
Target: "/mnt/install",
|
||||
Source: installPath,
|
||||
Type: mount.TypeBind,
|
||||
ReadOnly: false,
|
||||
},
|
||||
},
|
||||
Tmpfs: map[string]string{
|
||||
"/tmp": "rw,exec,nosuid,size=50M",
|
||||
},
|
||||
DNS: []string{"1.1.1.1", "8.8.8.8"},
|
||||
LogConfig: container.LogConfig{
|
||||
Type: "local",
|
||||
Config: map[string]string{
|
||||
"max-size": "5m",
|
||||
"max-file": "1",
|
||||
"compress": "false",
|
||||
},
|
||||
},
|
||||
Privileged: true,
|
||||
NetworkMode: "pterodactyl_nw",
|
||||
}
|
||||
|
||||
zap.S().Infow("creating installer container for server process", zap.String("server", ip.Server.Uuid))
|
||||
r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, ip.Server.Uuid+"_installer")
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
zap.S().Infow(
|
||||
"running installation script for server in container",
|
||||
zap.String("server", ip.Server.Uuid),
|
||||
zap.String("container_id", r.ID),
|
||||
)
|
||||
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
go func(id string) {
|
||||
ip.Server.Events().Publish(DaemonMessageEvent, "Starting installation process, this could take a few minutes...")
|
||||
if err := ip.StreamOutput(id); err != nil {
|
||||
zap.S().Errorw(
|
||||
"error handling streaming output for server install process",
|
||||
zap.String("container_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
||||
}(r.ID)
|
||||
|
||||
sChann, eChann := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
|
||||
select {
|
||||
case err := <-eChann:
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
case <-sChann:
|
||||
}
|
||||
|
||||
return r.ID, nil
|
||||
}
|
||||
|
||||
// Streams the output of the installation process to a log file in the server configuration
|
||||
// directory, as well as to a websocket listener so that the process can be viewed in
|
||||
// the panel by administrators.
|
||||
func (ip *InstallationProcess) StreamOutput(id string) error {
|
||||
reader, err := ip.client.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
s := bufio.NewScanner(reader)
|
||||
for s.Scan() {
|
||||
ip.Server.Events().Publish(InstallOutputEvent, s.Text())
|
||||
}
|
||||
|
||||
if err := s.Err(); err != nil {
|
||||
zap.S().Warnw(
|
||||
"error processing scanner line in installation output for server",
|
||||
zap.String("server", ip.Server.Uuid),
|
||||
zap.String("container_id", id),
|
||||
zap.Error(errors.WithStack(err)),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Makes a HTTP request to the Panel instance notifying it that the server has
|
||||
// completed the installation process, and what the state of the server is. A boolean
|
||||
// value of "true" means everything was successful, "false" means something went
|
||||
// wrong and the server must be deleted and re-created.
|
||||
func (s *Server) SyncInstallState(successful bool) error {
|
||||
r := api.NewRequester()
|
||||
|
||||
rerr, err := r.SendInstallationStatus(s.Uuid, successful)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,40 +6,41 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
// Adds all of the internal event listeners we want to use for a server.
|
||||
func (s *Server) AddEventListeners() {
|
||||
s.AddListener(ConsoleOutputEvent, s.onConsoleOutput())
|
||||
}
|
||||
consoleChannel := make(chan Event)
|
||||
s.Events().Subscribe(ConsoleOutputEvent, consoleChannel)
|
||||
|
||||
var onConsoleOutputListener func(string)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-consoleChannel:
|
||||
s.onConsoleOutput(data.Data)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Custom listener for console output events that will check if the given line
|
||||
// of output matches one that should mark the server as started or not.
|
||||
func (s *Server) onConsoleOutput() *func(string) {
|
||||
if onConsoleOutputListener == nil {
|
||||
onConsoleOutputListener = func (data string) {
|
||||
// If the specific line of output is one that would mark the server as started,
|
||||
// set the server to that state. Only do this if the server is not currently stopped
|
||||
// or stopping.
|
||||
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
|
||||
zap.S().Debugw(
|
||||
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
|
||||
)
|
||||
func (s *Server) onConsoleOutput(data string) {
|
||||
// If the specific line of output is one that would mark the server as started,
|
||||
// set the server to that state. Only do this if the server is not currently stopped
|
||||
// or stopping.
|
||||
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
|
||||
zap.S().Debugw(
|
||||
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
|
||||
)
|
||||
|
||||
s.SetState(ProcessRunningState)
|
||||
}
|
||||
|
||||
// If the command sent to the server is one that should stop the server we will need to
|
||||
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
||||
// cause the server to unexpectedly restart on the user.
|
||||
if s.State == ProcessStartingState || s.State == ProcessRunningState {
|
||||
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
|
||||
s.SetState(ProcessStoppingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.SetState(ProcessRunningState)
|
||||
}
|
||||
|
||||
return &onConsoleOutputListener
|
||||
}
|
||||
// If the command sent to the server is one that should stop the server we will need to
|
||||
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
||||
// cause the server to unexpectedly restart on the user.
|
||||
if s.State == ProcessStartingState || s.State == ProcessRunningState {
|
||||
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
|
||||
s.SetState(ProcessStoppingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ type Server struct {
|
||||
// certain long operations return faster. For example, FS disk space usage.
|
||||
Cache *cache.Cache `json:"-" yaml:"-"`
|
||||
|
||||
// All of the registered event listeners for this server instance.
|
||||
listeners EventListeners
|
||||
// Events emitted by the server instance.
|
||||
emitter *EventBus
|
||||
|
||||
// Defines the process configuration for the server instance. This is dynamically
|
||||
// fetched from the Pterodactyl Server instance each time the server process is
|
||||
@@ -199,7 +199,6 @@ func LoadDirectory(dir string, cfg *config.SystemConfiguration) error {
|
||||
|
||||
// Initializes the default required internal struct components for a Server.
|
||||
func (s *Server) Init() {
|
||||
s.listeners = make(map[string][]EventListenerFunction)
|
||||
s.mutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
@@ -221,23 +220,13 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
||||
|
||||
s.AddEventListeners()
|
||||
|
||||
withConfiguration := func(e *DockerEnvironment) {
|
||||
e.User = cfg.User.Uid
|
||||
e.TimezonePath = cfg.TimezonePath
|
||||
e.Server = s
|
||||
}
|
||||
|
||||
// Right now we only support a Docker based environment, so I'm going to hard code
|
||||
// this logic in. When we're ready to support other environment we'll need to make
|
||||
// some modifications here obviously.
|
||||
var env Environment
|
||||
if t, err := NewDockerEnvironment(withConfiguration); err == nil {
|
||||
env = t
|
||||
} else {
|
||||
if err := NewDockerEnvironment(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.Environment = env
|
||||
s.Cache = cache.New(time.Minute*10, time.Minute*15)
|
||||
s.Filesystem = Filesystem{
|
||||
Configuration: cfg,
|
||||
@@ -257,6 +246,33 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Returns all of the environment variables that should be assigned to a running
|
||||
// server instance.
|
||||
func (s *Server) GetEnvironmentVariables() []string {
|
||||
zone, _ := time.Now().In(time.Local).Zone()
|
||||
|
||||
var out = []string{
|
||||
fmt.Sprintf("TZ=%s", zone),
|
||||
fmt.Sprintf("STARTUP=%s", s.Invocation),
|
||||
fmt.Sprintf("SERVER_MEMORY=%d", s.Build.MemoryLimit),
|
||||
fmt.Sprintf("SERVER_IP=%s", s.Allocations.DefaultMapping.Ip),
|
||||
fmt.Sprintf("SERVER_PORT=%d", s.Allocations.DefaultMapping.Port),
|
||||
}
|
||||
|
||||
eloop:
|
||||
for k, v := range s.EnvVars {
|
||||
for _, e := range out {
|
||||
if strings.HasPrefix(e, strings.ToUpper(k)) {
|
||||
continue eloop
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, fmt.Sprintf("%s=%s", strings.ToUpper(k), v))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Syncs the state of the server on the Panel with Wings. This ensures that we're always
|
||||
// using the state of the server from the Panel and allows us to not require successful
|
||||
// API calls to Wings to do things.
|
||||
@@ -337,10 +353,10 @@ func (s *Server) SetState(state string) error {
|
||||
}
|
||||
}(s)
|
||||
|
||||
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", state))
|
||||
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", s.State))
|
||||
|
||||
// Emit the event to any listeners that are currently registered.
|
||||
s.Emit(StatusEvent, s.State)
|
||||
s.Events().Publish(StatusEvent, s.State)
|
||||
|
||||
// If server was in an online state, and is now in an offline state we should handle
|
||||
// that as a crash event. In that scenario, check the last crash time, and the crash
|
||||
|
||||
@@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
@@ -30,11 +29,6 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
|
||||
return errors.New("attempting to merge a data stack with an invalid UUID")
|
||||
}
|
||||
|
||||
// Set the default values in the interface that we unmarshaled into.
|
||||
if err := defaults.Set(src); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Merge the new data object that we have received with the existing server data object
|
||||
// and then save it to the disk so it is persistent.
|
||||
if err := mergo.Merge(s, src, mergo.WithOverride); err != nil {
|
||||
|
||||
97
websocket.go
97
websocket.go
@@ -62,10 +62,11 @@ type WebsocketTokenPayload struct {
|
||||
}
|
||||
|
||||
const (
|
||||
PermissionConnect = "connect"
|
||||
PermissionSendCommand = "send-command"
|
||||
PermissionSendPower = "send-power"
|
||||
PermissionReceiveErrors = "receive-errors"
|
||||
PermissionConnect = "connect"
|
||||
PermissionSendCommand = "send-command"
|
||||
PermissionSendPower = "send-power"
|
||||
PermissionReceiveErrors = "receive-errors"
|
||||
PermissionReceiveInstall = "receive-install"
|
||||
)
|
||||
|
||||
// Checks if the given token payload has a permission string.
|
||||
@@ -92,24 +93,17 @@ func ParseJWT(token []byte) (*WebsocketTokenPayload, error) {
|
||||
alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken))
|
||||
}
|
||||
|
||||
_, err := jwt.Verify(token, alg, &payload)
|
||||
now := time.Now()
|
||||
verifyOptions := jwt.ValidatePayload(
|
||||
&payload.Payload,
|
||||
jwt.ExpirationTimeValidator(now),
|
||||
)
|
||||
|
||||
_, err := jwt.Verify(token, alg, &payload, verifyOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check the time of the JWT becoming valid does not exceed more than 15 seconds
|
||||
// compared to the system time. This accounts for clock drift to some degree.
|
||||
if time.Now().Unix()-payload.NotBefore.Unix() <= -15 {
|
||||
return nil, errors.New("jwt violates nbf")
|
||||
}
|
||||
|
||||
// Compare the expiration time of the token to the current system time. Include
|
||||
// up to 15 seconds of clock drift, and if it has expired return an error and
|
||||
// do not process the action.
|
||||
if time.Now().Unix()-payload.ExpirationTime.Unix() > 15 {
|
||||
return nil, errors.New("jwt violates exp")
|
||||
}
|
||||
|
||||
if !payload.HasPermission(PermissionConnect) {
|
||||
return nil, errors.New("not authorized to connect to this socket")
|
||||
}
|
||||
@@ -123,8 +117,8 @@ func (wsh *WebsocketHandler) TokenValid() error {
|
||||
return errors.New("no jwt present")
|
||||
}
|
||||
|
||||
if time.Now().Unix()-wsh.JWT.ExpirationTime.Unix() > 15 {
|
||||
return errors.New("jwt violates nbf")
|
||||
if err := jwt.ExpirationTimeValidator(time.Now())(&wsh.JWT.Payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !wsh.JWT.HasPermission(PermissionConnect) {
|
||||
@@ -170,35 +164,39 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http
|
||||
JWT: nil,
|
||||
}
|
||||
|
||||
handleOutput := func(data string) {
|
||||
handler.SendJson(&WebsocketMessage{
|
||||
Event: server.ConsoleOutputEvent,
|
||||
Args: []string{data},
|
||||
})
|
||||
events := []string{
|
||||
server.StatsEvent,
|
||||
server.StatusEvent,
|
||||
server.ConsoleOutputEvent,
|
||||
server.InstallOutputEvent,
|
||||
server.DaemonMessageEvent,
|
||||
}
|
||||
|
||||
handleServerStatus := func(data string) {
|
||||
handler.SendJson(&WebsocketMessage{
|
||||
Event: server.StatusEvent,
|
||||
Args: []string{data},
|
||||
})
|
||||
eventChannel := make(chan server.Event)
|
||||
for _, event := range events {
|
||||
s.Events().Subscribe(event, eventChannel)
|
||||
}
|
||||
|
||||
handleResourceUse := func(data string) {
|
||||
handler.SendJson(&WebsocketMessage{
|
||||
Event: server.StatsEvent,
|
||||
Args: []string{data},
|
||||
})
|
||||
}
|
||||
defer func() {
|
||||
for _, event := range events {
|
||||
s.Events().Unsubscribe(event, eventChannel)
|
||||
}
|
||||
|
||||
s.AddListener(server.StatusEvent, &handleServerStatus)
|
||||
defer s.RemoveListener(server.StatusEvent, &handleServerStatus)
|
||||
close(eventChannel)
|
||||
}()
|
||||
|
||||
s.AddListener(server.ConsoleOutputEvent, &handleOutput)
|
||||
defer s.RemoveListener(server.ConsoleOutputEvent, &handleOutput)
|
||||
|
||||
s.AddListener(server.StatsEvent, &handleResourceUse)
|
||||
defer s.RemoveListener(server.StatsEvent, &handleResourceUse)
|
||||
// Listen for different events emitted by the server and respond to them appropriately.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case d := <-eventChannel:
|
||||
handler.SendJson(&WebsocketMessage{
|
||||
Event: d.Topic,
|
||||
Args: []string{d.Data},
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Sit here and check the time to expiration on the JWT every 30 seconds until
|
||||
// the token has expired. If we are within 3 minutes of the token expiring, send
|
||||
@@ -257,13 +255,22 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http
|
||||
// Perform a blocking send operation on the websocket since we want to avoid any
|
||||
// concurrent writes to the connection, which would cause a runtime panic and cause
|
||||
// the program to crash out.
|
||||
func (wsh *WebsocketHandler) SendJson(v interface{}) error {
|
||||
func (wsh *WebsocketHandler) SendJson(v *WebsocketMessage) error {
|
||||
// Do not send JSON down the line if the JWT on the connection is not
|
||||
// valid!
|
||||
if err := wsh.TokenValid(); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we're sending installation output but the user does not have the required
|
||||
// permissions to see the output, don't send it down the line.
|
||||
if v.Event == server.InstallOutputEvent {
|
||||
zap.S().Debugf("%+v", v.Args)
|
||||
if wsh.JWT != nil && !wsh.JWT.HasPermission(PermissionReceiveInstall) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return wsh.unsafeSendJson(v)
|
||||
}
|
||||
|
||||
@@ -351,7 +358,7 @@ func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error {
|
||||
|
||||
// On every authentication event, send the current server status back
|
||||
// to the client. :)
|
||||
wsh.Server.Emit(server.StatusEvent, wsh.Server.State)
|
||||
wsh.Server.Events().Publish(server.StatusEvent, wsh.Server.State)
|
||||
|
||||
wsh.unsafeSendJson(WebsocketMessage{
|
||||
Event: AuthenticationSuccessEvent,
|
||||
|
||||
65
wings.go
65
wings.go
@@ -106,48 +106,33 @@ func main() {
|
||||
zap.S().Errorw("failed to create an environment for server", zap.String("server", s.Uuid), zap.Error(err))
|
||||
}
|
||||
|
||||
if r, err := s.Environment.IsRunning(); err != nil {
|
||||
r, err := s.Environment.IsRunning()
|
||||
if err != nil {
|
||||
zap.S().Errorw("error checking server environment status", zap.String("server", s.Uuid), zap.Error(err))
|
||||
} else if r {
|
||||
// If the server is currently running on Docker, mark the process as being in that state.
|
||||
// We never want to stop an instance that is currently running external from Wings since
|
||||
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
|
||||
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
|
||||
if err := s.Sync(); err != nil {
|
||||
zap.S().Errorw("failed to sync server state, cannot mark as running", zap.String("server", s.Uuid), zap.Error(errors.WithStack(err)))
|
||||
} else {
|
||||
s.SetState(server.ProcessRunningState)
|
||||
|
||||
// If we cannot attach to the environment go ahead and mark the processs as being offline.
|
||||
if err := s.Environment.Attach(); err != nil {
|
||||
zap.S().Warnw("error attaching to server environment", zap.String("server", s.Uuid), zap.Error(err))
|
||||
s.SetState(server.ProcessOfflineState)
|
||||
}
|
||||
}
|
||||
} else if !r {
|
||||
// If the server is not in a running state right now but according to the configuration it
|
||||
// should be, we want to go ahead and restart the instance.
|
||||
if s.State == server.ProcessRunningState || s.State == server.ProcessStartingState {
|
||||
zap.S().Infow(
|
||||
"server state does not match last recorded state in configuration, starting instance now",
|
||||
zap.String("server", s.Uuid),
|
||||
)
|
||||
|
||||
if err := s.Environment.Start(); err != nil {
|
||||
zap.S().Warnw(
|
||||
"failed to put server instance back in running state",
|
||||
zap.String("server", s.Uuid),
|
||||
zap.Error(errors.WithStack(err)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if s.State == "" {
|
||||
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
||||
// track of what the actual server state is.
|
||||
s.SetState(server.ProcessOfflineState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the server is currently running on Docker, mark the process as being in that state.
|
||||
// We never want to stop an instance that is currently running external from Wings since
|
||||
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
|
||||
//
|
||||
// This will also validate that a server process is running if the last tracked state we have
|
||||
// is that it was running, but we see that the container process is not currently running.
|
||||
if r || (!r && (s.State == server.ProcessRunningState || s.State == server.ProcessStartingState)) {
|
||||
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
|
||||
if err := s.Environment.Start(); err != nil {
|
||||
zap.S().Warnw(
|
||||
"failed to properly start server detected as already running",
|
||||
zap.String("server", s.Uuid),
|
||||
zap.Error(errors.WithStack(err)),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
||||
// track of what the actual server state is.
|
||||
s.SetState(server.ProcessOfflineState)
|
||||
}(serv)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user