From 80e369836b0901b9febe74ad19fa6c2b84b4f8ed Mon Sep 17 00:00:00 2001 From: George Shaw Date: Mon, 8 Jan 2018 14:42:58 -0600 Subject: [PATCH 1/3] changes on work computer --- pkg/system/user.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 pkg/system/user.go diff --git a/pkg/system/user.go b/pkg/system/user.go new file mode 100644 index 0000000..d81d11d --- /dev/null +++ b/pkg/system/user.go @@ -0,0 +1,5 @@ +package system + +func Create(username string) { + +} From fe00595edf095b6a776238ae34ceb4620fae5eb1 Mon Sep 17 00:00:00 2001 From: George Shaw Date: Mon, 8 Jan 2018 16:49:15 -0600 Subject: [PATCH 2/3] ssh process is in the works --- main.go => gpanel.go | 0 pkg/api/bundle/create.go | 8 ++++ pkg/system/user.go | 98 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 2 deletions(-) rename main.go => gpanel.go (100%) diff --git a/main.go b/gpanel.go similarity index 100% rename from main.go rename to gpanel.go diff --git a/pkg/api/bundle/create.go b/pkg/api/bundle/create.go index 258af71..23f3f81 100644 --- a/pkg/api/bundle/create.go +++ b/pkg/api/bundle/create.go @@ -13,6 +13,7 @@ import ( "github.com/Ennovar/gPanel/pkg/encryption" "github.com/Ennovar/gPanel/pkg/file" "github.com/Ennovar/gPanel/pkg/gpaccount" + "github.com/Ennovar/gPanel/pkg/system" ) func Create(res http.ResponseWriter, req *http.Request, logger *log.Logger, bundles map[string]*gpaccount.Controller) bool { @@ -123,6 +124,13 @@ func Create(res http.ResponseWriter, req *http.Request, logger *log.Logger, bund } ds.Close() + err, err2 := system.CreateBundleUser(createBundleRequestData.Name) + if err != nil { + logger.Println(req.URL.Path + "::" + err.Error() + " AND " + err2.Error()) + http.Error(res, err2.Error(), http.StatusInternalServerError) + return false + } + bundles[createBundleRequestData.Name] = gpaccount.New(newBundle+"/", databaseBundlePorts.Account, databaseBundlePorts.Public) _ = bundles[createBundleRequestData.Name].Start() _ = bundles[createBundleRequestData.Name].Public.Start() diff --git a/pkg/system/user.go b/pkg/system/user.go index d81d11d..d2b5b46 100644 --- a/pkg/system/user.go +++ b/pkg/system/user.go @@ -1,5 +1,99 @@ package system -func Create(username string) { - +import ( + "bytes" + "errors" + "os/exec" +) + +func CreateBundleUser(username string) (error, error) { + var cerr bytes.Buffer + var err error + + adduserArgs := []string{ + "--disabled-password", + "--gecos", + "", + username, + } + + keygenArgs := []string{ + "-t", + "rsa", + "-N", + "", + "-f", + "/home/" + username + "/.ssh/id_rsa", + } + + // Add the user + cmd := exec.Command("adduser", adduserArgs...) + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Create the .ssh folder for said user + cmd = exec.Command("mkdir", "/home/"+username+"/.ssh") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Create authorized_keys file + cmd = exec.Command("touch", "/home/"+username+"/.ssh/authorized_keys") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Put root public key into authorized_keys file + cmd = exec.Command("cp", "/root/.ssh/id_rsa.pub", "/home/"+username+"/.ssh/authorized_keys") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Correct permissions on authorized_keys file + cmd = exec.Command("chmod", "600", "/home/"+username+"/.ssh/authorized_keys") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Create the host key-pair for said user + cmd = exec.Command("ssh-keygen", keygenArgs...) + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Create document root for said user + cmd = exec.Command("mkdir", "/home/"+username+"/document_root") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + return nil, nil +} + +func DeleteBundleUser(username string) (error, error) { + var cerr bytes.Buffer + + cmd := exec.Command("deluser", "--remove-all-files", username) + cmd.Stderr = &cerr + + if err := cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + return nil, nil } From b01536869a40123689a9e4d596e4508eb5e7af8b Mon Sep 17 00:00:00 2001 From: George Shaw Date: Tue, 9 Jan 2018 12:33:45 -0600 Subject: [PATCH 3/3] SSH is almost done, just needs an interface for adding/removing authorized keys. Also updated contributing docs and readme --- CONTRIBUTING.md | 16 ++-- README.md | 9 ++- .../assets/js/panelHandlers/users/addkey.js | 6 ++ .../js/panelHandlers/users/deletekey.js | 0 account/gPanel.html | 25 +++++- pkg/api/ssh/addkey.go | 39 +++++++++ pkg/api/ssh/deletekey.go | 39 +++++++++ pkg/gpaccount/apihandler.go | 9 ++- pkg/system/keys.go | 49 +++++++++++ pkg/system/user.go | 81 ++++++++++++++++--- 10 files changed, 249 insertions(+), 24 deletions(-) create mode 100644 account/assets/js/panelHandlers/users/addkey.js create mode 100644 account/assets/js/panelHandlers/users/deletekey.js create mode 100644 pkg/api/ssh/addkey.go create mode 100644 pkg/api/ssh/deletekey.go create mode 100644 pkg/system/keys.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c42a3ce..1704710 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,7 @@ Key: __Implemented__ | __*Implemented, but needs work*__ | *Unimplemented* | ~~R * Clients * __Multi-client support__ * __Configuration of new clients__ - * __*Configuration of existing hosts*__ + * __Configuration of existing hosts__ * gPanel Account (Upper level of bundles) * Accessibility * __Using ports defined during configuration__ @@ -89,12 +89,12 @@ Key: __Implemented__ | __*Implemented, but needs work*__ | *Unimplemented* | ~~R * *Alert system for fatal errors* * Mail Servers * Domains - * *Configuration of domains* + * __Configuration of domains__ * *Configuration of sub-domains* * *TLS/SSL certificate support* - * *Multi-domain support* + * __Multi-domain support__ * Remote Access - * *SSH Access* + * __*SSH Access*__ * File Manager * *CRUD* * *Inline Editor* @@ -107,15 +107,15 @@ Key: __Implemented__ | __*Implemented, but needs work*__ | *Unimplemented* | ~~R * Supported content types * __All of the obvious ones (.jpg/.html/.css/etc)__ * *.go* - * *.php* + * __*.php*__ * General * Deployment - * *Binary* + * __Binary__ * *GUI Installation Helper* * Multi-infrastructure Support - * __Windows__ + * ~~Windows~~ (No native solution for SSH exists within Windows OS) * __Linux__ - * __Mac__ + * *Mac* (This is possible, but OS detection and refactoring within the system package needs done) * *Amazon Web Services* * *DigitalOcean* * *Google Cloud Platform* diff --git a/README.md b/README.md index 83147fd..e1d3ad6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A web-hosting control panel written in Go. ## Stack -Backend: __[Go](https://golang.org/)__ +Backend: __[Go (1.8+)](https://golang.org/)__ Database: __[Bolt](https://github.com/boltdb/bolt)__ CSS Toolkit(s): __[Bootstrap 4](http://getbootstrap.com/) & [Font Awesome](http://fontawesome.io/)__ JS Toolkit(s): __[jQuery](https://jquery.com/)__ @@ -35,6 +35,9 @@ git remote rename fork origin To deploy... ```shell -# Starting gPanel -go run main.go +# Build the binary +go build gpanel.go + +# Execute binary as root (root access is needed for functions within the system package) +sudo ./gpanel ``` diff --git a/account/assets/js/panelHandlers/users/addkey.js b/account/assets/js/panelHandlers/users/addkey.js new file mode 100644 index 0000000..69383f7 --- /dev/null +++ b/account/assets/js/panelHandlers/users/addkey.js @@ -0,0 +1,6 @@ +var sshModal = jQuery(".manage-ssh-modal"); + +jQuery('._js_manage-ssh').on('click', function(e){ + e.preventDefault(); + sshModal.modal('show'); +}); diff --git a/account/assets/js/panelHandlers/users/deletekey.js b/account/assets/js/panelHandlers/users/deletekey.js new file mode 100644 index 0000000..e69de29 diff --git a/account/gPanel.html b/account/gPanel.html index 7f47857..3571ac6 100644 --- a/account/gPanel.html +++ b/account/gPanel.html @@ -226,6 +226,26 @@ + + +
@@ -306,9 +326,10 @@

Account Users

-
View, edit, update, and remove users that can access this gPanel Account
+
View, edit, update, and remove users that can access the gPanel Account and utilize SSH.
+
@@ -370,6 +391,8 @@ + + diff --git a/pkg/api/ssh/addkey.go b/pkg/api/ssh/addkey.go new file mode 100644 index 0000000..792cd9b --- /dev/null +++ b/pkg/api/ssh/addkey.go @@ -0,0 +1,39 @@ +package ssh + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/Ennovar/gPanel/pkg/system" +) + +func AddKey(res http.ResponseWriter, req *http.Request, logger *log.Logger) bool { + if req.Method != "UPDATE" { + logger.Println(req.URL.Path + "::" + req.Method + "::" + strconv.Itoa(http.StatusMethodNotAllowed) + "::" + http.StatusText(http.StatusMethodNotAllowed)) + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + var requestData struct { + Username string `json:"username"` + PublicKey string `json:"publickey"` + } + + err := json.NewDecoder(req.Body).Decode(&requestData) + if err != nil { + logger.Println(req.URL.Path + "::" + err.Error()) + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + if err = system.AddAuthorizedKey(requestData.Username, requestData.PublicKey); err != nil { + logger.Println(req.URL.Path + "::" + err.Error()) + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + + res.WriteHeader(http.StatusNoContent) + return true +} diff --git a/pkg/api/ssh/deletekey.go b/pkg/api/ssh/deletekey.go new file mode 100644 index 0000000..865278f --- /dev/null +++ b/pkg/api/ssh/deletekey.go @@ -0,0 +1,39 @@ +package ssh + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/Ennovar/gPanel/pkg/system" +) + +func DeleteKey(res http.ResponseWriter, req *http.Request, logger *log.Logger) bool { + if req.Method != "UPDATE" { + logger.Println(req.URL.Path + "::" + req.Method + "::" + strconv.Itoa(http.StatusMethodNotAllowed) + "::" + http.StatusText(http.StatusMethodNotAllowed)) + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + var requestData struct { + Username string `json:"username"` + PublicKey string `json:"publickey"` + } + + err := json.NewDecoder(req.Body).Decode(&requestData) + if err != nil { + logger.Println(req.URL.Path + "::" + err.Error()) + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + if err = system.DeleteAuthorizedKey(requestData.Username, requestData.PublicKey); err != nil { + logger.Println(req.URL.Path + "::" + err.Error()) + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + + res.WriteHeader(http.StatusNoContent) + return true +} diff --git a/pkg/gpaccount/apihandler.go b/pkg/gpaccount/apihandler.go index e59fe46..e16218c 100644 --- a/pkg/gpaccount/apihandler.go +++ b/pkg/gpaccount/apihandler.go @@ -5,12 +5,13 @@ import ( "net/http" "strings" + "github.com/Ennovar/gPanel/pkg/api/domain" "github.com/Ennovar/gPanel/pkg/api/ip" logapi "github.com/Ennovar/gPanel/pkg/api/log" "github.com/Ennovar/gPanel/pkg/api/server" - "github.com/Ennovar/gPanel/pkg/api/user" - "github.com/Ennovar/gPanel/pkg/api/domain" "github.com/Ennovar/gPanel/pkg/api/settings" + "github.com/Ennovar/gPanel/pkg/api/ssh" + "github.com/Ennovar/gPanel/pkg/api/user" ) func (con *Controller) apiHandler(res http.ResponseWriter, req *http.Request) (bool, bool) { @@ -67,6 +68,10 @@ func (con *Controller) apiHandler(res http.ResponseWriter, req *http.Request) (b return true, domain.Unlink(res, req, con.APILogger) case "/settings/get_nameservers": return true, settings.GetNameservers(res, req, con.APILogger) + case "/ssh/addkey": + return true, ssh.AddKey(res, req, con.APILogger) + case "/ssh/deletekey": + return true, ssh.DeleteKey(res, req, con.APILogger) default: return false, false } diff --git a/pkg/system/keys.go b/pkg/system/keys.go new file mode 100644 index 0000000..15b1294 --- /dev/null +++ b/pkg/system/keys.go @@ -0,0 +1,49 @@ +package system + +import ( + "io/ioutil" + "os" + "strings" +) + +func AddAuthorizedKey(username, key string) error { + f, err := os.OpenFile("/home/"+username+"/.ssh/authorized_keys", os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + + if _, err = f.WriteString(key); err != nil { + return err + } + + return nil +} + +func DeleteAuthorizedKey(username, key string) error { + old, err := ioutil.ReadFile("/home/" + username + "/.ssh/authorized_keys") + if err != nil { + return err + } + + lines := strings.Split(string(old), "\n") + + for i, line := range lines { + if strings.Contains(line, key) { + lines = append(lines[:i], lines[i+1:]...) + break + } + } + + new := strings.Join(lines, "\n") + + f, err := os.OpenFile("/home/"+username+"/.ssh/authorized_keys", os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return err + } + + if _, err = f.WriteString(new); err != nil { + return err + } + + return nil +} diff --git a/pkg/system/user.go b/pkg/system/user.go index d2b5b46..9d90518 100644 --- a/pkg/system/user.go +++ b/pkg/system/user.go @@ -58,14 +58,6 @@ func CreateBundleUser(username string) (error, error) { return err, errors.New(cerr.String()) } - // Correct permissions on authorized_keys file - cmd = exec.Command("chmod", "600", "/home/"+username+"/.ssh/authorized_keys") - cmd.Stderr = &cerr - - if err = cmd.Run(); err != nil { - return err, errors.New(cerr.String()) - } - // Create the host key-pair for said user cmd = exec.Command("ssh-keygen", keygenArgs...) cmd.Stderr = &cerr @@ -82,16 +74,85 @@ func CreateBundleUser(username string) (error, error) { return err, errors.New(cerr.String()) } + /* OWNERSHIP AND FILE PERMISSIONS START */ + cmd = exec.Command("chmod", "700", "/home/"+username+"/.ssh") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chmod", "600", "/home/"+username+"/.ssh/id_rsa") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chmod", "644", "/home/"+username+"/.ssh/id_rsa.pub") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chmod", "644", "/home/"+username+"/.ssh/authorized_keys") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chown", username+":", "/home/"+username+"/.ssh") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chown", username+":", "/home/"+username+"/.ssh/id_rsa") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chown", username+":", "/home/"+username+"/.ssh/id_rsa.pub") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + cmd = exec.Command("chown", username+":", "/home/"+username+"/.ssh/authorized_keys") + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + /* OWNERSHIP AND FILE PERMISSIONS END */ + return nil, nil } func DeleteBundleUser(username string) (error, error) { var cerr bytes.Buffer + var err error - cmd := exec.Command("deluser", "--remove-all-files", username) + // Delete the user and try to remove all files associated + cmd := exec.Command("deluser", "--quiet", username) cmd.Stderr = &cerr - if err := cmd.Run(); err != nil { + if err = cmd.Run(); err != nil { + return err, errors.New(cerr.String()) + } + + // Forcefully remove the users home directory if it has not already been done + // (sometimes deluser doesn't do its job even with the flag) + cmd = exec.Command("rm", "-rf", "/home/"+username) + cmd.Stderr = &cerr + + if err = cmd.Run(); err != nil { return err, errors.New(cerr.String()) }