From 0b585725b9dc6563f7c50f347b08ff90dde07c1b Mon Sep 17 00:00:00 2001 From: Joshua Taylor Date: Fri, 8 Nov 2019 15:41:29 -0600 Subject: [PATCH] Start setting up to merge 2 projects --- authorization.go | 65 +++++++++++++++ cmd/run.go | 172 ++------------------------------------- config.go | 22 +---- entry/entry.go | 140 +++++++++++++++++++++++++++++++ httphandlers/oauth2.go | 41 ++-------- oauth2/shared.go | 22 ----- oauth2/ssh.go | 5 +- pufferd_windows.syso | Bin 36090 -> 0 bytes routing/root.go | 5 +- routing/server/server.go | 5 +- sftp/server.go | 13 ++- 11 files changed, 244 insertions(+), 246 deletions(-) create mode 100644 authorization.go create mode 100644 entry/entry.go delete mode 100644 pufferd_windows.syso 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 0656bb362c8d7121d26f8ca1463e86519aa3aec4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36090 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 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)) }, }