diff --git a/api/server_endpoints.go b/api/server_endpoints.go index 6f6f21c..c0bceda 100644 --- a/api/server_endpoints.go +++ b/api/server_endpoints.go @@ -31,13 +31,16 @@ type ServerConfigurationResponse struct { // and what changes to make to the configuration file for a server. type ProcessConfiguration struct { Startup struct { - Done string `json:"done"` - UserInteraction []string `json:"userInteraction"` + Done []string `json:"done"` + UserInteraction []string `json:"user_interaction"` + StripAnsi bool `json:"strip_ansi"` } `json:"startup"` + Stop struct { Type string `json:"type"` Value string `json:"value"` } `json:"stop"` + ConfigurationFiles []parser.ConfigurationFile `json:"configs"` } @@ -202,4 +205,4 @@ func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) { } return nil, nil -} \ No newline at end of file +} diff --git a/cmd/root.go b/cmd/root.go index f852869..8077adf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "github.com/NYTimes/logrotate" "github.com/apex/log/handlers/multi" "github.com/gammazero/workerpool" + "golang.org/x/crypto/acme" "net/http" "os" "path" @@ -132,14 +133,15 @@ func rootCmdRun(*cobra.Command, []string) { config.SetDebugViaFlag(debug) if err := c.System.ConfigureDirectories(); err != nil { - log.Fatal("failed to configure system directories for pterodactyl") - panic(err) + log.WithError(err).Fatal("failed to configure system directories for pterodactyl") + os.Exit(1) + return } log.WithField("username", c.System.Username).Info("checking for pterodactyl system user") if su, err := c.EnsurePterodactylUser(); err != nil { - log.Error("failed to create pterodactyl system user") - panic(err) + log.WithError(err).Error("failed to create pterodactyl system user") + os.Exit(1) return } else { log.WithFields(log.Fields{ @@ -217,15 +219,19 @@ func rootCmdRun(*cobra.Command, []string) { // 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) + _ = s.SetState(server.ProcessOfflineState) }) } - // Wait until all of the servers are ready to go before we fire up the HTTP server. + // Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers. pool.StopWait() - // Initalize SFTP. - sftp.Initialize(c) + // Initialize the SFTP server. + if err := sftp.Initialize(c); err != nil { + log.WithError(err).Error("failed to initialize the sftp server") + os.Exit(1) + return + } // Ensure the archive directory exists. if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil { @@ -244,9 +250,46 @@ func rootCmdRun(*cobra.Command, []string) { "host_port": c.Api.Port, }).Info("configuring internal webserver") + // Configure the router. r := router.Configure() - addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port) + s := &http.Server{ + Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port), + Handler: r, + + TLSConfig: &tls.Config{ + NextProtos: []string{ + "h2", // enable HTTP/2 + "http/1.1", + }, + + // https://blog.cloudflare.com/exposing-go-on-the-internet + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }, + + PreferServerCipherSuites: true, + + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + + CurvePreferences: []tls.CurveID{ + tls.X25519, + tls.CurveP256, + }, + // END https://blog.cloudflare.com/exposing-go-on-the-internet + }, + } + + // Check if the server should run with TLS but using autocert. if useAutomaticTls && len(tlsHostname) > 0 { m := autocert.Manager{ Prompt: autocert.AcceptTOS, @@ -255,28 +298,43 @@ func rootCmdRun(*cobra.Command, []string) { } log.WithField("hostname", tlsHostname). - Info("webserver is now listening with auto-TLS enabled; certifcates will be automatically generated by Let's Encrypt") + Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt") - // We don't use the autotls runner here since we need to specify a port other than 443 - // to be using for SSL connections for Wings. - s := &http.Server{Addr: addr, TLSConfig: m.TLSConfig(), Handler: r} + // Hook autocert into the main http server. + s.TLSConfig.GetCertificate = m.GetCertificate + s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) // enable tls-alpn ACME challenges - go http.ListenAndServe(":http", m.HTTPHandler(nil)) + // Start the autocert server. + go func() { + if err := http.ListenAndServe(":http", m.HTTPHandler(nil)); err != nil { + log.WithError(err).Error("failed to serve autocert http server") + } + }() + + // Start the main http server with TLS using autocert. if err := s.ListenAndServeTLS("", ""); err != nil { log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}). Fatal("failed to configure HTTP server using auto-tls") os.Exit(1) } - } else if c.Api.Ssl.Enabled { - if err := r.RunTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil { + + return + } + + // Check if main http server should run with TLS. + if c.Api.Ssl.Enabled { + if err := s.ListenAndServeTLS(c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil { log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server") os.Exit(1) } - } else { - if err := r.Run(addr); err != nil { - log.WithField("error", err).Fatal("failed to configure HTTP server") - os.Exit(1) - } + return + } + + // Run the main http server without TLS. + s.TLSConfig = nil + if err := s.ListenAndServe(); err != nil { + log.WithField("error", err).Fatal("failed to configure HTTP server") + os.Exit(1) } } diff --git a/server/environment_docker.go b/server/environment_docker.go index 96e3e16..ee63e88 100644 --- a/server/environment_docker.go +++ b/server/environment_docker.go @@ -201,8 +201,8 @@ func (d *DockerEnvironment) Start() error { // If we don't set it to stopping first, you'll trigger crash detection which // we don't want to do at this point since it'll just immediately try to do the // exact same action that lead to it crashing in the first place... - d.Server.SetState(ProcessStoppingState) - d.Server.SetState(ProcessOfflineState) + _ = d.Server.SetState(ProcessStoppingState) + _ = d.Server.SetState(ProcessOfflineState) } }() @@ -228,7 +228,7 @@ func (d *DockerEnvironment) Start() error { } else { // If the server is running update our internal state and continue on with the attach. if c.State.Running { - d.Server.SetState(ProcessRunningState) + _ = d.Server.SetState(ProcessRunningState) return d.Attach() } @@ -243,7 +243,8 @@ func (d *DockerEnvironment) Start() error { } } - d.Server.SetState(ProcessStartingState) + _ = d.Server.SetState(ProcessStartingState) + // Set this to true for now, we will set it to false once we reach the // end of this chain. sawError = true @@ -289,7 +290,8 @@ func (d *DockerEnvironment) Stop() error { return d.Terminate(os.Kill) } - d.Server.SetState(ProcessStoppingState) + _ = d.Server.SetState(ProcessStoppingState) + // Only attempt to send the stop command to the instance if we are actually attached to // the instance. If we are not for some reason, just send the container stop event. if d.IsAttached() && stop.Type == api.ProcessStopCommand { @@ -304,7 +306,7 @@ func (d *DockerEnvironment) Stop() error { // an error. if client.IsErrNotFound(err) { d.SetStream(nil) - d.Server.SetState(ProcessOfflineState) + _ = d.Server.SetState(ProcessOfflineState) return nil } @@ -333,8 +335,10 @@ func (d *DockerEnvironment) Restart() error { // will be terminated forcefully depending on the value of the second argument. func (d *DockerEnvironment) WaitForStop(seconds int, terminate bool) error { if d.Server.GetState() == ProcessOfflineState { + log.WithField("server", d.Server.Id()).Debug("server is already offline, not waiting for stop.") return nil } + log.WithField("server", d.Server.Id()).Debug("waiting for server to stop") if err := d.Stop(); err != nil { return errors.WithStack(err) @@ -377,7 +381,9 @@ func (d *DockerEnvironment) Terminate(signal os.Signal) error { return nil } - d.Server.SetState(ProcessStoppingState) + // We set it to stopping than offline to prevent crash detection from being triggered. + _ = d.Server.SetState(ProcessStoppingState) + _ = d.Server.SetState(ProcessOfflineState) return d.Client.ContainerKill( context.Background(), d.Server.Id(), strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed"), @@ -387,8 +393,9 @@ func (d *DockerEnvironment) Terminate(signal os.Signal) error { // Remove the Docker container from the machine. If the container is currently running // it will be forcibly stopped by Docker. func (d *DockerEnvironment) Destroy() error { - // Avoid crash detection firing off. - d.Server.SetState(ProcessStoppingState) + // We set it to stopping than offline to prevent crash detection from being triggered. + _ = d.Server.SetState(ProcessStoppingState) + _ = d.Server.SetState(ProcessOfflineState) err := d.Client.ContainerRemove(context.Background(), d.Server.Id(), types.ContainerRemoveOptions{ RemoveVolumes: true, @@ -471,11 +478,11 @@ func (d *DockerEnvironment) Attach() error { go func() { defer d.stream.Close() defer func() { - d.Server.SetState(ProcessOfflineState) + _ = d.Server.SetState(ProcessOfflineState) d.SetStream(nil) }() - io.Copy(console, d.stream.Reader) + _, _ = io.Copy(console, d.stream.Reader) }() return nil @@ -542,14 +549,14 @@ func (d *DockerEnvironment) EnableResourcePolling() error { d.Server.Log().WithField("error", err).Warn("encountered error processing server stats, stopping collection") } - d.DisableResourcePolling() + _ = d.DisableResourcePolling() return } // Disable collection if the server is in an offline state and this process is // still running. if s.GetState() == ProcessOfflineState { - d.DisableResourcePolling() + _ = d.DisableResourcePolling() return } @@ -613,7 +620,7 @@ func (d *DockerEnvironment) ensureImageExists() error { continue } - log.WithField("registry", registry).Debug("using authentication for repository") + log.WithField("registry", registry).Debug("using authentication for registry") registryAuth = &c break } @@ -733,7 +740,7 @@ func (d *DockerEnvironment) Create() error { d.Server.Log().WithFields(log.Fields{ "source_path": m.Source, "target_path": m.Target, - "read_only": m.ReadOnly, + "read_only": m.ReadOnly, }).Debug("attaching custom server mount point to container") } } diff --git a/server/install.go b/server/install.go index 54ebcfb..4d99f20 100644 --- a/server/install.go +++ b/server/install.go @@ -55,6 +55,12 @@ func (s *Server) Install(sync bool) error { l.Warn("failed to notify panel of server install state") } + // Some how these publish events are sent to clients in reverse order, + // this is probably due to channels having the most recently sent item first. + + // Ensure that the server is marked as offline. + s.Events().Publish(StatusEvent, ProcessOfflineState) + // Push an event to the websocket so we can auto-refresh the information in the panel once // the install is completed. s.Events().Publish(InstallCompletedEvent, "") diff --git a/server/listeners.go b/server/listeners.go index bcb4bf7..30c977a 100644 --- a/server/listeners.go +++ b/server/listeners.go @@ -3,7 +3,9 @@ package server import ( "github.com/apex/log" "github.com/pterodactyl/wings/api" + "regexp" "strings" + "sync" ) // Adds all of the internal event listeners we want to use for a server. @@ -21,30 +23,79 @@ func (s *Server) AddEventListeners() { }() } +var ( + stripAnsiRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") + + regexpCacheMx sync.RWMutex + regexpCache map[string]*regexp.Regexp +) + // 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(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. - match := s.ProcessConfiguration().Startup.Done + // Get the server's process configuration. + processConfiguration := s.ProcessConfiguration() - if s.GetState() == ProcessStartingState && strings.Contains(data, match) { - s.Log().WithFields(log.Fields{ - "match": match, - "against": data, - }).Debug("detected server in running state based on console line output") + // Check if the server is currently starting. + if s.GetState() == ProcessStartingState { + // 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. - s.SetState(ProcessRunningState) + // Check if we should strip ansi color codes. + if processConfiguration.Startup.StripAnsi { + // Strip ansi color codes from the data string. + data = stripAnsiRegex.ReplaceAllString(data, "") + } + + // Iterate over all the done lines. + for _, match := range processConfiguration.Startup.Done { + if strings.HasPrefix(match, "regex:") && len(match) > 6 { + match = match[6:] + + regexpCacheMx.RLock() + rxp, ok := regexpCache[match] + regexpCacheMx.RUnlock() + + if !ok { + var err error + + rxp, err = regexp.Compile(match) + if err != nil { + log.WithError(err).Warn("failed to compile regexp") + break + } + + regexpCacheMx.Lock() + regexpCache[match] = rxp + regexpCacheMx.Unlock() + } + + if !rxp.MatchString(data) { + continue + } + } else if !strings.Contains(data, match) { + continue + } + + s.Log().WithFields(log.Fields{ + "match": match, + "against": data, + }).Debug("detected server in running state based on console line output") + + _ = s.SetState(ProcessRunningState) + break + } } // 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.IsRunning() { - stop := s.ProcessConfiguration().Stop + stop := processConfiguration.Stop + if stop.Type == api.ProcessStopCommand && data == stop.Value { - s.SetState(ProcessStoppingState) + _ = s.SetState(ProcessStoppingState) } } }