2020-04-10 13:12:18 +00:00
package cmd
import (
"crypto/tls"
"fmt"
"net/http"
"os"
2020-04-17 21:27:06 +00:00
"path"
"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/remeh/sizedwaitgroup"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
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 ( )
if err := configureLogging ( c . Debug ) ; err != nil {
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" )
log . Info ( "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-04-17 21:27:06 +00:00
if err := c . System . ConfigureDirectories ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . Fatal ( "failed to configure system directories for pterodactyl" )
panic ( err )
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-05-29 05:07:53 +00:00
log . Error ( "failed to create pterodactyl system user" )
panic ( err )
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-05-29 05:07:53 +00:00
log . Info ( "beginning file permission setting on server data directories" )
2020-04-10 13:12:18 +00:00
if err := c . EnsureFilePermissions ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
log . WithField ( "error" , err ) . Error ( "failed to properly chown data directories" )
2020-04-10 13:12:18 +00:00
} else {
2020-05-29 05:07:53 +00:00
log . Info ( "finished ensuring file permissions" )
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-04-10 13:12:18 +00:00
os . Exit ( 1 )
}
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-05-29 05:07:53 +00:00
log . WithField ( "server" , s . Uuid ) . Info ( "loaded configuration for server" )
2020-04-10 13:12:18 +00:00
}
// Create a new WaitGroup that limits us to 4 servers being bootstrapped at a time
// on Wings. This allows us to ensure the environment exists, write configurations,
// and reboot processes without causing a slow-down due to sequential booting.
wg := sizedwaitgroup . New ( 4 )
for _ , serv := range server . GetServers ( ) . All ( ) {
wg . Add ( )
go func ( s * server . Server ) {
2020-07-19 23:27:55 +00:00
defer wg . Done ( )
2020-05-29 05:07:53 +00:00
2020-07-19 23:27:55 +00:00
s . Log ( ) . Info ( "ensuring server environment exists" )
2020-04-10 13:12:18 +00:00
// Create a server environment if none exists currently. This allows us to recover from Docker
// being reinstalled on the host system for example.
2020-07-19 23:27:55 +00:00
if err := s . Environment . Create ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
s . Log ( ) . WithField ( "error" , err ) . Error ( "failed to process environment" )
2020-04-10 13:12:18 +00:00
}
r , err := s . Environment . IsRunning ( )
if err != nil {
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
}
// 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-04-11 01:22:18 +00:00
if r || ( ! r && s . IsRunning ( ) ) {
2020-05-29 05:07:53 +00:00
s . Log ( ) . Info ( "detected server is running, re-attaching to process..." )
2020-04-10 13:12:18 +00:00
if err := s . Environment . Start ( ) ; err != nil {
2020-05-29 05:07:53 +00:00
s . Log ( ) . WithField ( "error" , errors . WithStack ( err ) ) . Warn ( "failed to properly start server detected as already running" )
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.
s . SetState ( server . ProcessOfflineState )
} ( serv )
}
// Wait until all of the servers are ready to go before we fire up the HTTP server.
wg . Wait ( )
2020-05-25 22:51:36 +00:00
// Initalize SFTP.
sftp . Initialize ( c )
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
r := router . Configure ( )
addr := fmt . Sprintf ( "%s:%d" , c . Api . Host , c . Api . Port )
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 ) .
Info ( "webserver is now listening with auto-TLS enabled; certifcates 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 }
go http . ListenAndServe ( ":http" , m . HTTPHandler ( nil ) )
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 {
2020-04-10 13:12:18 +00:00
if err := r . RunTLS ( addr , 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
}
} else {
if err := r . Run ( addr ) ; err != nil {
2020-05-29 05:07:53 +00:00
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.
func configureLogging ( debug bool ) error {
cfg := zap . NewProductionConfig ( )
if debug {
cfg = zap . NewDevelopmentConfig ( )
}
cfg . Encoding = "console"
cfg . OutputPaths = [ ] string {
"stdout" ,
}
logger , err := cfg . Build ( )
if err != nil {
return err
}
zap . ReplaceGlobals ( logger )
2020-05-29 05:07:53 +00:00
log . SetHandler ( cli . Default )
log . SetLevel ( log . DebugLevel )
2020-04-10 13:12:18 +00:00
return nil
}
// Prints the wings logo, nothing special here!
func printLogo ( ) {
fmt . Println ( )
fmt . Println ( ` ____ ` )
fmt . Println ( ` __ Pterodactyl _____/___/_______ _______ ______ ` )
fmt . Println ( ` \_____\ \/\/ / / / __ / ___/ ` )
fmt . Println ( ` \___\ / / / / /_/ /___ / ` )
fmt . Println ( ` \___/\___/___/___/___/___ /______/ ` )
fmt . Println ( ` /_______/ v ` + system . Version )
fmt . Println ( )
2020-04-17 21:32:09 +00:00
fmt . Println ( ` Website: https://pterodactyl.io ` )
fmt . Println ( ` Source: https://github.com/pterodactyl/wings ` )
fmt . Println ( )
fmt . Println ( ` Copyright © 2018 - 2020 Dane Everitt & Contributors ` )
fmt . Println ( )
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 )
}