2020-04-10 13:12:18 +00:00
package cmd
import (
"crypto/tls"
"fmt"
2020-08-04 03:35:48 +00:00
"github.com/NYTimes/logrotate"
"github.com/apex/log/handlers/multi"
2020-09-13 05:08:50 +00:00
"github.com/docker/docker/client"
2020-08-01 04:56:44 +00:00
"github.com/gammazero/workerpool"
2020-08-04 23:19:04 +00:00
"golang.org/x/crypto/acme"
2020-04-10 13:12:18 +00:00
"net/http"
"os"
2020-04-17 21:27:06 +00:00
"path"
2020-08-04 03:35:48 +00:00
"path/filepath"
2020-04-17 21:27:06 +00:00
"strings"
2020-04-10 13:12:18 +00:00
2020-07-04 22:18:29 +00:00
"github.com/apex/log"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/loggers/cli"
"golang.org/x/crypto/acme/autocert"
2020-04-10 13:12:18 +00:00
"github.com/pkg/errors"
2020-04-10 21:39:07 +00:00
"github.com/pkg/profile"
2020-04-10 13:12:18 +00:00
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/router"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/sftp"
"github.com/pterodactyl/wings/system"
"github.com/spf13/cobra"
)
2020-04-17 21:27:06 +00:00
var configPath = config . DefaultLocation
2020-04-10 13:12:18 +00:00
var debug = false
2020-04-10 22:33:30 +00:00
var shouldRunProfiler = false
2020-07-01 04:34:47 +00:00
var useAutomaticTls = false
var tlsHostname = ""
2020-07-04 22:18:29 +00:00
var showVersion = false
2020-04-10 13:12:18 +00:00
var root = & cobra . Command {
Use : "wings" ,
2020-04-10 18:11:37 +00:00
Short : "The wings of the pterodactyl game management panel" ,
2020-04-10 13:12:18 +00:00
Long : ` ` ,
2020-07-01 04:34:47 +00:00
PreRun : func ( cmd * cobra . Command , args [ ] string ) {
if useAutomaticTls && len ( tlsHostname ) == 0 {
fmt . Println ( "A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com" )
os . Exit ( 1 )
}
} ,
2020-07-04 22:18:29 +00:00
Run : rootCmdRun ,
2020-04-10 13:12:18 +00:00
}
func init ( ) {
2020-07-04 22:18:29 +00:00
root . PersistentFlags ( ) . BoolVar ( & showVersion , "version" , false , "show the version and exit" )
2020-04-17 21:27:06 +00:00
root . PersistentFlags ( ) . StringVar ( & configPath , "config" , config . DefaultLocation , "set the location for the configuration file" )
2020-04-10 13:12:18 +00:00
root . PersistentFlags ( ) . BoolVar ( & debug , "debug" , false , "pass in order to run wings in debug mode" )
2020-04-10 22:33:30 +00:00
root . PersistentFlags ( ) . BoolVar ( & shouldRunProfiler , "profile" , false , "pass in order to profile wings" )
2020-07-01 04:34:47 +00:00
root . PersistentFlags ( ) . BoolVar ( & useAutomaticTls , "auto-tls" , false , "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt" )
root . PersistentFlags ( ) . StringVar ( & tlsHostname , "tls-hostname" , "" , "required with --auto-tls, the FQDN for the generated SSL certificate" )
2020-04-10 15:22:57 +00:00
root . AddCommand ( configureCmd )
2020-07-04 20:57:54 +00:00
root . AddCommand ( diagnosticsCmd )
2020-04-10 13:12:18 +00:00
}
2020-04-17 21:27:06 +00:00
// Get the configuration path based on the arguments provided.
func readConfiguration ( ) ( * config . Configuration , error ) {
var p = configPath
if ! strings . HasPrefix ( p , "/" ) {
d , err := os . Getwd ( )
if err != nil {
return nil , err
}
p = path . Clean ( path . Join ( d , configPath ) )
}
if s , err := os . Stat ( p ) ; err != nil {
return nil , errors . WithStack ( err )
} else if s . IsDir ( ) {
return nil , errors . New ( "cannot use directory as configuration file path" )
}
return config . ReadConfiguration ( p )
}
2020-04-10 22:33:30 +00:00
func rootCmdRun ( * cobra . Command , [ ] string ) {
2020-07-04 22:18:29 +00:00
if showVersion {
fmt . Println ( system . Version )
os . Exit ( 0 )
}
2020-04-10 22:33:30 +00:00
if shouldRunProfiler {
defer profile . Start ( ) . Stop ( )
}
2020-04-10 21:39:07 +00:00
2020-05-09 22:37:49 +00:00
// Only attempt configuration file relocation if a custom location has not
// been specified in the command startup.
if configPath == config . DefaultLocation {
if err := RelocateConfiguration ( ) ; err != nil {
if os . IsNotExist ( err ) {
exitWithConfigurationNotice ( )
}
panic ( err )
}
}
2020-04-17 21:27:06 +00:00
c , err := readConfiguration ( )
2020-04-10 13:12:18 +00:00
if err != nil {
panic ( err )
}
if debug {
c . Debug = true
}
printLogo ( )
2020-08-04 03:35:48 +00:00
if err := configureLogging ( c . System . LogDirectory , c . Debug ) ; err != nil {
2020-04-10 13:12:18 +00:00
panic ( err )
}
2020-05-29 05:07:53 +00:00
log . WithField ( "path" , c . GetPath ( ) ) . Info ( "loading configuration from path" )
2020-04-10 13:12:18 +00:00
if c . Debug {
2020-05-29 05:07:53 +00:00
log . Debug ( "running in debug mode" )
2020-08-04 03:35:48 +00:00
log . Warn ( "certificate checking is disabled" )
2020-04-10 13:12:18 +00:00
http . DefaultTransport . ( * http . Transport ) . TLSClientConfig = & tls . Config {
InsecureSkipVerify : true ,
}
}
config . Set ( c )
config . SetDebugViaFlag ( debug )
2020-10-17 18:35:20 +00:00
if err := c . System . ConfigureTimezone ( ) ; err != nil {
log . WithField ( "error" , err ) . Fatal ( "failed to detect system timezone or use supplied configuration value" )
return
}
log . WithField ( "timezone" , c . System . Timezone ) . Info ( "configured wings with system timezone" )
2020-04-17 21:27:06 +00:00
if err := c . System . ConfigureDirectories ( ) ; err != nil {
2020-09-04 03:13:51 +00:00
log . WithField ( "error" , err ) . Fatal ( "failed to configure system directories for pterodactyl" )
return
}
if err := c . System . EnableLogRotation ( ) ; err != nil {
log . WithField ( "error" , err ) . Fatal ( "failed to configure log rotation on the system" )
2020-08-04 23:19:04 +00:00
return
2020-04-17 21:27:06 +00:00
}
2020-05-29 05:07:53 +00:00
log . WithField ( "username" , c . System . Username ) . Info ( "checking for pterodactyl system user" )
2020-04-10 13:12:18 +00:00
if su , err := c . EnsurePterodactylUser ( ) ; err != nil {
2020-09-04 03:29:53 +00:00
log . WithField ( "error" , err ) . Fatal ( "failed to create pterodactyl system user" )
2020-04-10 13:12:18 +00:00
return
} else {
2020-05-29 05:07:53 +00:00
log . WithFields ( log . Fields {
"username" : su . Username ,
"uid" : su . Uid ,
"gid" : su . Gid ,
} ) . Info ( "configured system user successfully" )
2020-04-10 13:12:18 +00:00
}
2020-04-11 01:03:35 +00:00
if err := server . LoadDirectory ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Fatal ( "failed to load server configurations" )
2020-04-10 13:12:18 +00:00
return
}
if err := environment . ConfigureDocker ( & c . Docker ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Fatal ( "failed to configure docker environment" )
2020-09-04 03:29:53 +00:00
return
2020-04-10 13:12:18 +00:00
}
2020-04-10 23:55:36 +00:00
if err := c . WriteToDisk ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Error ( "failed to save configuration to disk" )
2020-04-10 23:55:36 +00:00
}
2020-04-10 13:12:18 +00:00
// Just for some nice log output.
for _ , s := range server . GetServers ( ) . All ( ) {
2020-07-20 00:53:41 +00:00
log . WithField ( "server" , s . Id ( ) ) . Info ( "loaded configuration for server" )
2020-04-10 13:12:18 +00:00
}
2020-09-13 05:08:50 +00:00
states , err := server . CachedServerStates ( )
if err != nil {
log . WithField ( "error" , errors . WithStack ( err ) ) . Error ( "failed to retrieve locally cached server states from disk, assuming all servers in offline state" )
}
2020-08-01 04:56:44 +00:00
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time
2020-04-10 13:12:18 +00:00
// on Wings. This allows us to ensure the environment exists, write configurations,
// and reboot processes without causing a slow-down due to sequential booting.
2020-08-01 04:56:44 +00:00
pool := workerpool . New ( 4 )
2020-04-10 13:12:18 +00:00
for _ , serv := range server . GetServers ( ) . All ( ) {
2020-08-01 04:56:44 +00:00
s := serv
2020-05-29 05:07:53 +00:00
2020-08-01 04:56:44 +00:00
pool . Submit ( func ( ) {
2020-09-13 05:08:50 +00:00
s . Log ( ) . Info ( "configuring server environment and restoring to previous state" )
var st string
if state , exists := states [ s . Id ( ) ] ; exists {
st = state
2020-04-10 13:12:18 +00:00
}
r , err := s . Environment . IsRunning ( )
2020-09-13 05:08:50 +00:00
// We ignore missing containers because we don't want to actually block booting of wings at this
// point. If we didn't do this and you pruned all of the images and then started wings you could
// end up waiting a long period of time for all of the images to be re-pulled on Wings boot rather
// than when the server itself is started.
if err != nil && ! client . IsErrNotFound ( err ) {
2020-05-29 05:07:53 +00:00
s . Log ( ) . WithField ( "error" , err ) . Error ( "error checking server environment status" )
2020-04-10 13:12:18 +00:00
}
2020-09-13 05:08:50 +00:00
// Check if the server was previously running. If so, attempt to start the server now so that Wings
// can pick up where it left off. If the environment does not exist at all, just create it and then allow
// the normal flow to execute.
2020-04-10 13:12:18 +00:00
//
2020-09-13 05:08:50 +00:00
// This does mean that booting wings after a catastrophic machine crash and wiping out the Docker images
// as a result will result in a slow boot.
if ! r && ( st == environment . ProcessRunningState || st == environment . ProcessStartingState ) {
if err := s . HandlePowerAction ( server . PowerActionStart ) ; err != nil {
s . Log ( ) . WithField ( "error" , errors . WithStack ( err ) ) . Warn ( "failed to return server to running state" )
}
} else if r || ( ! r && s . IsRunning ( ) ) {
// 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.
2020-05-29 05:07:53 +00:00
s . Log ( ) . Info ( "detected server is running, re-attaching to process..." )
2020-08-20 02:20:46 +00:00
s . SetState ( environment . ProcessRunningState )
if err := s . Environment . Attach ( ) ; err != nil {
s . Log ( ) . WithField ( "error" , errors . WithStack ( err ) ) . Warn ( "failed to attach to running server environment" )
2020-04-10 13:12:18 +00:00
}
return
}
// Addresses potentially invalid data in the stored file that can cause Wings to lose
// track of what the actual server state is.
2020-08-20 01:58:48 +00:00
_ = s . SetState ( environment . ProcessOfflineState )
2020-08-01 04:56:44 +00:00
} )
2020-04-10 13:12:18 +00:00
}
2020-08-04 23:19:04 +00:00
// Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers.
2020-08-01 04:56:44 +00:00
pool . StopWait ( )
2020-04-10 13:12:18 +00:00
2020-08-04 23:19:04 +00:00
// Initialize the SFTP server.
2020-09-01 03:14:04 +00:00
if err := sftp . Initialize ( c . System ) ; err != nil {
log . WithError ( err ) . Fatal ( "failed to initialize the sftp server" )
2020-08-04 23:19:04 +00:00
return
}
2020-04-10 13:12:18 +00:00
// Ensure the archive directory exists.
if err := os . MkdirAll ( c . System . ArchiveDirectory , 0755 ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Error ( "failed to create archive directory" )
2020-04-10 13:12:18 +00:00
}
2020-04-10 22:33:30 +00:00
// Ensure the backup directory exists.
if err := os . MkdirAll ( c . System . BackupDirectory , 0755 ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Error ( "failed to create backup directory" )
2020-04-10 22:33:30 +00:00
}
2020-05-29 05:07:53 +00:00
log . WithFields ( log . Fields {
2020-07-04 22:18:29 +00:00
"use_ssl" : c . Api . Ssl . Enabled ,
2020-07-01 04:34:47 +00:00
"use_auto_tls" : useAutomaticTls && len ( tlsHostname ) > 0 ,
"host_address" : c . Api . Host ,
2020-07-04 22:18:29 +00:00
"host_port" : c . Api . Port ,
2020-07-01 04:34:47 +00:00
} ) . Info ( "configuring internal webserver" )
2020-04-10 13:12:18 +00:00
2020-08-04 23:19:04 +00:00
// Configure the router.
2020-04-10 13:12:18 +00:00
r := router . Configure ( )
2020-08-04 23:19:04 +00:00
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.
2020-07-01 04:34:47 +00:00
if useAutomaticTls && len ( tlsHostname ) > 0 {
m := autocert . Manager {
2020-07-04 22:18:29 +00:00
Prompt : autocert . AcceptTOS ,
Cache : autocert . DirCache ( path . Join ( c . System . RootDirectory , "/.tls-cache" ) ) ,
HostPolicy : autocert . HostWhitelist ( tlsHostname ) ,
2020-07-01 04:34:47 +00:00
}
log . WithField ( "hostname" , tlsHostname ) .
2020-08-04 23:19:04 +00:00
Info ( "webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt" )
2020-07-01 04:34:47 +00:00
2020-08-04 23:19:04 +00:00
// 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
// 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" )
}
} ( )
2020-07-01 04:34:47 +00:00
2020-08-04 23:19:04 +00:00
// Start the main http server with TLS using autocert.
2020-07-01 04:34:47 +00:00
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 )
}
2020-08-04 23:19:04 +00:00
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 {
2020-07-01 04:34:47 +00:00
log . WithFields ( log . Fields { "auto_tls" : false , "error" : err } ) . Fatal ( "failed to configure HTTPS server" )
2020-05-29 05:07:53 +00:00
os . Exit ( 1 )
2020-04-10 13:12:18 +00:00
}
2020-08-04 23:19:04 +00:00
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 )
2020-04-10 13:12:18 +00:00
}
}
// Execute calls cobra to handle cli commands
func Execute ( ) error {
return root . Execute ( )
}
// Configures the global logger for Zap so that we can call it from any location
// in the code without having to pass around a logger instance.
2020-08-04 03:35:48 +00:00
func configureLogging ( logDir string , debug bool ) error {
2020-08-25 02:22:19 +00:00
if err := os . MkdirAll ( path . Join ( logDir , "/install" ) , 0700 ) ; err != nil {
2020-08-24 17:29:40 +00:00
return errors . WithStack ( err )
}
2020-08-04 03:35:48 +00:00
p := filepath . Join ( logDir , "/wings.log" )
w , err := logrotate . NewFile ( p )
if err != nil {
2020-08-24 00:18:40 +00:00
panic ( errors . Wrap ( err , "failed to open process log file" ) )
2020-08-04 03:35:48 +00:00
}
2020-09-04 03:29:53 +00:00
if debug {
log . SetLevel ( log . DebugLevel )
} else {
log . SetLevel ( log . InfoLevel )
}
2020-08-04 03:35:48 +00:00
log . SetHandler ( multi . New (
cli . Default ,
cli . New ( w . File , false ) ,
) )
log . WithField ( "path" , p ) . Info ( "writing log files to disk" )
2020-05-29 05:07:53 +00:00
2020-04-10 13:12:18 +00:00
return nil
}
// Prints the wings logo, nothing special here!
func printLogo ( ) {
2020-08-04 03:35:48 +00:00
fmt . Printf ( colorstring . Color ( `
____
__ [ blue ] [ bold ] Pterodactyl [ reset ] _____ / ___ / _______ _______ ______
\ _____ \ \ / \ / / / / __ / ___ /
\ ___ \ / / / / / _ / / ___ /
\ ___ / \ ___ / ___ / ___ / ___ / ___ / ______ /
/ _______ / [ bold ] v % s [ reset ]
Copyright © 2018 - 2020 Dane Everitt & Contributors
Website : https : //pterodactyl.io
Source : https : //github.com/pterodactyl/wings
License : https : //github.com/pterodactyl/wings/blob/develop/LICENSE
This software is made available under the terms of the MIT license .
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software . % s ` ) , system . Version , "\n\n" )
2020-04-10 13:12:18 +00:00
}
2020-05-09 22:37:49 +00:00
func exitWithConfigurationNotice ( ) {
fmt . Print ( colorstring . Color ( `
[ _red_ ] [ white ] [ bold ] Error : Configuration File Not Found [ reset ]
Wings was not able to locate your configuration file , and therefore is not
able to complete its boot process .
Please ensure you have copied your instance configuration file into
the default location , or have provided the -- config flag to use a
custom location .
Default Location : / etc / pterodactyl / config . yml
[ yellow ] This is not a bug with this software . Please do not make a bug report
for this issue , it will be closed . [ reset ]
` ) )
os . Exit ( 1 )
}