diff --git a/authorization.go b/authorization.go new file mode 100644 index 0000000..796f2fe --- /dev/null +++ b/authorization.go @@ -0,0 +1,65 @@ +package pufferd + +import ( + "bytes" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "github.com/pufferpanel/apufferi/v4" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" + "io" + "os" + "sync" +) + +type SFTPAuthorization interface { + Validate(username, password string) (perms *ssh.Permissions, err error) +} + +var publicKey *ecdsa.PublicKey + +var atLocker = &sync.RWMutex{} + +func SetPublicKey(key *ecdsa.PublicKey) { + atLocker.Lock() + defer atLocker.Unlock() + publicKey = key +} + +func GetPublicKey() *ecdsa.PublicKey { + atLocker.RLock() + defer atLocker.RUnlock() + return publicKey +} + +func LoadPublicKey() (*ecdsa.PublicKey, error) { + publicKey := GetPublicKey() + if publicKey != nil { + return publicKey, nil + } + + f, err := os.OpenFile(viper.GetString("auth.publicKey"), os.O_RDONLY, 660) + defer apufferi.Close(f) + + var buf bytes.Buffer + + _, _ = io.Copy(&buf, f) + + block, _ := pem.Decode(buf.Bytes()) + if block == nil { + return nil, ErrKeyNotPEM + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + publicKey, ok := pub.(*ecdsa.PublicKey) + if !ok { + return nil, ErrKeyNotECDSA + } + + SetPublicKey(publicKey) + return publicKey, nil +} diff --git a/cmd/run.go b/cmd/run.go index f7a0647..a3c1ee4 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,186 +18,28 @@ package main import ( "fmt" - "github.com/braintree/manners" "github.com/pufferpanel/apufferi/v4/logging" "github.com/pufferpanel/pufferd/v2" - "github.com/pufferpanel/pufferd/v2/environments" - "github.com/pufferpanel/pufferd/v2/programs" - "github.com/pufferpanel/pufferd/v2/routing" - "github.com/pufferpanel/pufferd/v2/sftp" - "github.com/pufferpanel/pufferd/v2/shutdown" + "github.com/pufferpanel/pufferd/v2/entry" "github.com/spf13/cobra" "github.com/spf13/viper" - "os" - "os/signal" - "runtime/debug" - "syscall" ) var RunCmd = &cobra.Command{ Use: "run", Short: "Runs the daemon", - Run: func(cmd *cobra.Command, args []string) { - err := runRun() - if err != nil { - logging.Exception("error running", err) - } - }, + Run: runRun, } -var runService = true - -func runRun() error { +func runRun(cmd *cobra.Command, args []string) { + pufferd.SetDefaults() _ = pufferd.LoadConfig() var logPath = viper.GetString("data.logs") _ = logging.WithLogDirectory(logPath, logging.DEBUG, nil) - logging.Info(pufferd.Display) - - environments.LoadModules() - programs.Initialize() - - var err error - - if _, err = os.Stat(programs.ServerFolder); os.IsNotExist(err) { - logging.Info("No server directory found, creating") - err = os.MkdirAll(programs.ServerFolder, 0755) - if err != nil && !os.IsExist(err) { - return err - } - } - - programs.LoadFromFolder() - - programs.InitService() - - for _, element := range programs.GetAll() { - if element.IsEnabled() { - element.GetEnvironment().DisplayToConsole(true, "Daemon has been started\n") - if element.IsAutoStart() { - logging.Info("Queued server %s", element.Id()) - element.GetEnvironment().DisplayToConsole(true, "Server has been queued to start\n") - programs.StartViaService(element) - } - } - } - - defer recoverPanic() - - createHook() - - for runService && err == nil { - err = runServices() - } - - shutdown.Shutdown() - - return err -} - -func runServices() error { - router := routing.ConfigureWeb() - - useHttps := false - - httpsPem := viper.GetString("listen.webCert") - httpsKey := viper.GetString("listen.webKey") - - if _, err := os.Stat(httpsPem); os.IsNotExist(err) { - logging.Warn("No HTTPS.PEM found in data folder, will use http instead") - } else if _, err := os.Stat(httpsKey); os.IsNotExist(err) { - logging.Warn("No HTTPS.KEY found in data folder, will use http instead") - } else { - useHttps = true - } - - sftp.Run() - - web := viper.GetString("listen.web") - - logging.Info("Starting web access on %s", web) - var err error - if useHttps { - err = manners.ListenAndServeTLS(web, httpsPem, httpsKey, router) - } else { - err = manners.ListenAndServe(web, router) - } - - /*if runtime.GOOS != "windows" { - go func() { - file := viper.GetString("listen.socket") - - if file == "" || !strings.HasPrefix(file, "unix:") { - return - } - - file = strings.TrimPrefix(file, "unix:") - - err := os.Remove(file) - if err != nil && !os.IsNotExist(err) { - logging.Exception(fmt.Sprintf("Error deleting %s", file), err) - return - } - - listener, err := net.Listen("unix", file) - defer apufferi.Close(listener) - if err != nil { - logging.Exception(fmt.Sprintf("Error listening on %s", file), err) - return - } - - err = os.Chmod(file, 0777) - if err != nil { - logging.Exception(fmt.Sprintf("Error listening on %s", file), err) - return - } - - logging.Info("Listening for socket requests") - err = http.Serve(listener, router) - if err != nil { - logging.Exception(fmt.Sprintf("Error listening on %s", file), err) - return - } - }() - }*/ - - return err -} - -func createHook() { - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) - go func() { - defer func() { - if err := recover(); err != nil { - logging.Error("%+v\n%s", err, debug.Stack()) - } - }() - - var sig os.Signal - - for sig != syscall.SIGTERM { - sig = <-c - switch sig { - case syscall.SIGHUP: - //manners.Close() - //sftp.Stop() - _ = pufferd.LoadConfig() - case syscall.SIGPIPE: - //ignore SIGPIPEs for now, we're somehow getting them and it's causing issues - } - } - - runService = false - shutdown.CompleteShutdown() - }() -} - -func recoverPanic() { - if rec := recover(); rec != nil { - err := rec.(error) - fmt.Printf("CRITICAL: %s", err.Error()) - logging.Critical("Unhandled error: %s", err.Error()) + err := <-entry.Start() + if err != nil { + fmt.Printf("Error running: %s", err.Error()) } } diff --git a/config.go b/config.go index 49d99f9..3606327 100644 --- a/config.go +++ b/config.go @@ -7,10 +7,12 @@ import ( func init() { //env configuration - viper.SetEnvPrefix("PUFFERD") + viper.SetEnvPrefix("PUFFERPANEL") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() +} +func SetDefaults() { //defaults we can set at this point in time viper.SetDefault("console.buffer", 50) viper.SetDefault("console.forward", false) @@ -25,24 +27,6 @@ func init() { viper.SetDefault("auth.publicKey", "panel.pem") viper.SetDefault("auth.url", "http://localhost:8080") - /*if runtime.GOOS == "windows" { - viper.SetDefault("auth.url", "http://localhost:8080") - } else { - pufferpanelConfig := viper.New() - pufferpanelConfig.SetEnvPrefix("PUFFERPANEL") - pufferpanelConfig.AutomaticEnv() - pufferpanelConfig.SetConfigName("config") - pufferpanelConfig.AddConfigPath("/etc/pufferpanel/") - pufferpanelConfig.AddConfigPath("/pufferpanel/") - - _ = pufferpanelConfig.ReadInConfig() - panelUrl := pufferpanelConfig.GetString("web.socket") - if panelUrl != "" { - viper.SetDefault("auth.url", "unix:"+panelUrl) - } else { - viper.SetDefault("auth.url", "unix:/var/run/pufferpanel.sock") - } - }*/ viper.SetDefault("auth.clientId", "") viper.SetDefault("auth.clientSecret", "") diff --git a/entry/entry.go b/entry/entry.go new file mode 100644 index 0000000..686b26e --- /dev/null +++ b/entry/entry.go @@ -0,0 +1,140 @@ +package entry + +import ( + "fmt" + "github.com/braintree/manners" + "github.com/pufferpanel/apufferi/v4/logging" + "github.com/pufferpanel/pufferd/v2" + "github.com/pufferpanel/pufferd/v2/environments" + "github.com/pufferpanel/pufferd/v2/programs" + "github.com/pufferpanel/pufferd/v2/routing" + "github.com/pufferpanel/pufferd/v2/sftp" + "github.com/pufferpanel/pufferd/v2/shutdown" + "github.com/spf13/viper" + "os" + "os/signal" + "runtime/debug" + "syscall" +) + +var runService = true + +func Start() chan error { + errChan := make(chan error) + go entry(errChan) + return errChan +} + +func entry(errChan chan error) { + logging.Info(pufferd.Display) + + environments.LoadModules() + programs.Initialize() + + var err error + + if _, err = os.Stat(programs.ServerFolder); os.IsNotExist(err) { + logging.Info("No server directory found, creating") + err = os.MkdirAll(programs.ServerFolder, 0755) + if err != nil && !os.IsExist(err) { + errChan <- err + return + } + } + + programs.LoadFromFolder() + + programs.InitService() + + for _, element := range programs.GetAll() { + if element.IsEnabled() { + element.GetEnvironment().DisplayToConsole(true, "Daemon has been started\n") + if element.IsAutoStart() { + logging.Info("Queued server %s", element.Id()) + element.GetEnvironment().DisplayToConsole(true, "Server has been queued to start\n") + programs.StartViaService(element) + } + } + } + + createHook() + + for runService && err == nil { + err = runServices() + } + + shutdown.Shutdown() + + errChan <- err + return +} + +func runServices() error { + defer recoverPanic() + + router := routing.ConfigureWeb() + + useHttps := false + + httpsPem := viper.GetString("listen.webCert") + httpsKey := viper.GetString("listen.webKey") + + if _, err := os.Stat(httpsPem); os.IsNotExist(err) { + logging.Warn("No HTTPS.PEM found in data folder, will use http instead") + } else if _, err := os.Stat(httpsKey); os.IsNotExist(err) { + logging.Warn("No HTTPS.KEY found in data folder, will use http instead") + } else { + useHttps = true + } + + sftp.Run() + + web := viper.GetString("listen.web") + + logging.Info("Starting web access on %s", web) + var err error + if useHttps { + err = manners.ListenAndServeTLS(web, httpsPem, httpsKey, router) + } else { + err = manners.ListenAndServe(web, router) + } + + return err +} + +func createHook() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) + go func() { + defer func() { + if err := recover(); err != nil { + logging.Error("%+v\n%s", err, debug.Stack()) + } + }() + + var sig os.Signal + + for sig != syscall.SIGTERM { + sig = <-c + switch sig { + case syscall.SIGHUP: + //manners.Close() + //sftp.Stop() + _ = pufferd.LoadConfig() + case syscall.SIGPIPE: + //ignore SIGPIPEs for now, we're somehow getting them and it's causing issues + } + } + + runService = false + shutdown.CompleteShutdown() + }() +} + +func recoverPanic() { + if rec := recover(); rec != nil { + err := rec.(error) + fmt.Printf("CRITICAL: %s", err.Error()) + logging.Critical("Unhandled error: %s", err.Error()) + } +} diff --git a/httphandlers/oauth2.go b/httphandlers/oauth2.go index 58ba797..192e4b9 100644 --- a/httphandlers/oauth2.go +++ b/httphandlers/oauth2.go @@ -17,20 +17,13 @@ package httphandlers import ( - "bytes" - "crypto/ecdsa" - "crypto/x509" - "encoding/pem" "github.com/gin-gonic/gin" "github.com/pufferpanel/apufferi/v4" "github.com/pufferpanel/apufferi/v4/response" "github.com/pufferpanel/apufferi/v4/scope" "github.com/pufferpanel/pufferd/v2" "github.com/pufferpanel/pufferd/v2/programs" - "github.com/spf13/viper" - "io" "net/http" - "os" "strings" ) @@ -42,6 +35,7 @@ func OAuth2Handler(requiredScope scope.Scope, requireServer bool) gin.HandlerFun c.Abort() } }() + authHeader := c.Request.Header.Get("Authorization") var authToken string if authHeader == "" { @@ -59,33 +53,16 @@ func OAuth2Handler(requiredScope scope.Scope, requireServer bool) gin.HandlerFun authToken = authArr[1] } - f, err := os.OpenFile(viper.GetString("auth.publicKey"), os.O_RDONLY, 660) - defer apufferi.Close(f) - if response.HandleError(c, err, http.StatusInternalServerError) { - return - } - - var buf bytes.Buffer - - _, _ = io.Copy(&buf, f) - - block, _ := pem.Decode(buf.Bytes()) - if block == nil { - response.HandleError(c, pufferd.ErrKeyNotPEM, http.StatusInternalServerError) - return - } - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if response.HandleError(c, err, http.StatusInternalServerError) { - return - } - - pubKey, ok := pub.(*ecdsa.PublicKey) - if !ok { - response.HandleError(c, pufferd.ErrKeyNotECDSA, http.StatusInternalServerError) - return + var err error + key := pufferd.GetPublicKey() + if key == nil { + key, err = pufferd.LoadPublicKey() + if response.HandleError(c, err, http.StatusInternalServerError) { + return + } } - token, err := apufferi.ParseToken(pubKey, authToken) + token, err := apufferi.ParseToken(key, authToken) if response.HandleError(c, err, http.StatusForbidden) { return } diff --git a/oauth2/shared.go b/oauth2/shared.go index 6135f8e..441951e 100644 --- a/oauth2/shared.go +++ b/oauth2/shared.go @@ -18,17 +18,14 @@ package oauth2 import ( "bytes" - "context" "encoding/json" "errors" "github.com/pufferpanel/apufferi/v4/logging" "github.com/pufferpanel/pufferd/v2/commons" "github.com/spf13/viper" - "net" "net/http" "net/url" "strconv" - "strings" "sync" "time" ) @@ -106,15 +103,6 @@ func RefreshIfStale() { func createRequest(encodedData string) (request *http.Request) { authUrl := viper.GetString("auth.url") - /*if strings.HasPrefix(authUrl, "unix:") && client.Transport != unixTransport { - logging.Debug("Configured to use unix socket") - client.Transport = unixTransport - request, _ = http.NewRequest("POST", "http://unix/oauth2/token", bytes.NewBufferString(encodedData)) - } else { - logging.Debug("Using standard http connection") - request, _ = http.NewRequest("POST", authUrl+"/oauth2/token", bytes.NewBufferString(encodedData)) - }*/ - logging.Debug("Using standard http connection") request, _ = http.NewRequest("POST", authUrl+"/oauth2/token", bytes.NewBufferString(encodedData)) return @@ -125,13 +113,3 @@ type requestResponse struct { ExpiresIn time.Duration `json:"expires_in"` Error string `json:"error"` } - -var unixTransport = &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - dialer := net.Dialer{} - authUrl := viper.GetString("auth.url") - socketPath := strings.TrimPrefix(authUrl, "unix:") - logging.Debug("Using %s as socket path", socketPath) - return dialer.DialContext(ctx, "unix", socketPath) - }, -} \ No newline at end of file diff --git a/oauth2/ssh.go b/oauth2/ssh.go index 0005908..a8c6a34 100644 --- a/oauth2/ssh.go +++ b/oauth2/ssh.go @@ -28,7 +28,10 @@ import ( "strings" ) -func ValidateSSH(username string, password string) (*ssh.Permissions, error) { +type WebSSHAuthorization struct { +} + +func (ws *WebSSHAuthorization) Validate(username string, password string) (*ssh.Permissions, error) { return validateSSH(username, password, true) } diff --git a/pufferd_windows.syso b/pufferd_windows.syso deleted file mode 100644 index 0656bb3..0000000 Binary files a/pufferd_windows.syso and /dev/null differ diff --git a/routing/root.go b/routing/root.go index 12d3618..c54fa89 100644 --- a/routing/root.go +++ b/routing/root.go @@ -23,7 +23,6 @@ import ( "github.com/pufferpanel/apufferi/v4/response" "github.com/pufferpanel/pufferd/v2" _ "github.com/pufferpanel/pufferd/v2/docs" - "github.com/pufferpanel/pufferd/v2/oauth2" "github.com/pufferpanel/pufferd/v2/routing/server" "github.com/pufferpanel/pufferd/v2/routing/swagger" "net/http" @@ -52,12 +51,10 @@ func ConfigureWeb() *gin.Engine { middleware.ResponseAndRecover(c) }) RegisterRoutes(r) - server.RegisterRoutes(r) + server.RegisterRoutes(r.Group("/")) swagger.Load(r) } - oauth2.RefreshToken() - return r } diff --git a/routing/server/server.go b/routing/server/server.go index 2f95e22..18941a4 100644 --- a/routing/server/server.go +++ b/routing/server/server.go @@ -49,7 +49,7 @@ var wsupgrader = websocket.Upgrader{ }, } -func RegisterRoutes(e *gin.Engine) { +func RegisterRoutes(e *gin.RouterGroup) { l := e.Group("/server") { l.PUT("/:id", httphandlers.OAuth2Handler(scope.ServersCreate, false), CreateServer) @@ -80,7 +80,8 @@ func RegisterRoutes(e *gin.Engine) { l.GET("/:id/file/*filename", httphandlers.OAuth2Handler(scope.ServersFilesGet, true), GetFile) l.PUT("/:id/file/*filename", httphandlers.OAuth2Handler(scope.ServersFilesPut, true), PutFile) l.DELETE("/:id/file/*filename", httphandlers.OAuth2Handler(scope.ServersFilesPut, true), DeleteFile) - l.OPTIONS("/:id/file/*filename", response.CreateOptions("GET", "PUT", "DELETE")) + l.POST("/:id/file/*filename", httphandlers.OAuth2Handler(scope.ServersFilesPut, true), response.NotImplemented) + l.OPTIONS("/:id/file/*filename", response.CreateOptions("GET", "PUT", "DELETE", "POST")) l.GET("/:id/console", httphandlers.OAuth2Handler(scope.ServersConsole, true), GetLogs) l.POST("/:id/console", httphandlers.OAuth2Handler(scope.ServersConsoleSend, true), PostConsole) diff --git a/sftp/server.go b/sftp/server.go index 476cc21..b30676a 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -24,6 +24,7 @@ import ( "github.com/pkg/sftp" "github.com/pufferpanel/apufferi/v4" "github.com/pufferpanel/apufferi/v4/logging" + "github.com/pufferpanel/pufferd/v2" "github.com/pufferpanel/pufferd/v2/oauth2" "github.com/pufferpanel/pufferd/v2/programs" "github.com/spf13/viper" @@ -36,6 +37,8 @@ import ( var sftpServer net.Listener +var auth pufferd.SFTPAuthorization + func Run() { err := runServer() if err != nil { @@ -43,14 +46,22 @@ func Run() { } } +func SetAuthorization(service pufferd.SFTPAuthorization) { + auth = service +} + func Stop() { _ = sftpServer.Close() } func runServer() error { + if auth == nil { + auth = &oauth2.WebSSHAuthorization{} + } + config := &ssh.ServerConfig{ PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - return oauth2.ValidateSSH(c.User(), string(pass)) + return auth.Validate(c.User(), string(pass)) }, }