diff --git a/document_roots/webhost/assets/js/panelHandlers/publicServer/maintenance.js b/document_roots/webhost/assets/js/panelHandlers/publicServer/maintenance.js new file mode 100644 index 0000000..23734b8 --- /dev/null +++ b/document_roots/webhost/assets/js/panelHandlers/publicServer/maintenance.js @@ -0,0 +1,16 @@ +jQuery('._js_public-server-maintenance-mode').on('click', function(e){ + e.preventDefault(); + + var xhr = new XMLHttpRequest(); + xhr.open('UPDATE', 'api/server/maintenance', true); + xhr.send(); + + xhr.onloadend = function() { + if(xhr.status == 204) { + getPublicServerStatus(); + } + else { + alert('An error has occurred, please contact your webhost administrator.'); + } + } +}); diff --git a/document_roots/webhost/assets/js/panelHandlers/publicServer/restart.js b/document_roots/webhost/assets/js/panelHandlers/publicServer/restart.js new file mode 100644 index 0000000..e468d14 --- /dev/null +++ b/document_roots/webhost/assets/js/panelHandlers/publicServer/restart.js @@ -0,0 +1,30 @@ +jQuery('._js_public-server-restart-graceful, ._js_public-server-restart-forceful').on('click', function(e){ + e.preventDefault(); + var requestData = {}; + + if(jQuery(this).hasClass('_js_public-server-restart-graceful')) { + requestData["graceful"] = true; + } + else { + requestData["graceful"] = false; + } + + var xhr = new XMLHttpRequest(); + xhr.open('UPDATE', 'api/server/restart', true); + xhr.send(JSON.stringify(requestData)); + + xhr.onloadend = function() { + if(xhr.status == 204) { + getPublicServerStatus(); + alert('Server successfully restarted.'); + } + else { + if(xhr.response) { + alert(xhr.response); + } + else { + alert('An error has occurred, please contact your webhost administrator.'); + } + } + } +}); diff --git a/document_roots/webhost/assets/js/panelHandlers/publicServer/shutdown.js b/document_roots/webhost/assets/js/panelHandlers/publicServer/shutdown.js new file mode 100644 index 0000000..81e1fc7 --- /dev/null +++ b/document_roots/webhost/assets/js/panelHandlers/publicServer/shutdown.js @@ -0,0 +1,29 @@ +jQuery('._js_public-server-shutdown-graceful, ._js_public-server-shutdown-forceful').on('click', function(e){ + e.preventDefault(); + var requestData = {}; + + if(jQuery(this).hasClass('_js_public-server-shutdown-graceful')) { + requestData["graceful"] = true; + } + else { + requestData["graceful"] = false; + } + + var xhr = new XMLHttpRequest(); + xhr.open('UPDATE', 'api/server/shutdown', true); + xhr.send(JSON.stringify(requestData)); + + xhr.onloadend = function() { + if(xhr.status == 204) { + getPublicServerStatus(); + } + else { + if(xhr.response) { + alert(xhr.response); + } + else { + alert('An error has occurred, please contact your webhost administrator.'); + } + } + } +}); diff --git a/document_roots/webhost/assets/js/panelHandlers/publicServer/start.js b/document_roots/webhost/assets/js/panelHandlers/publicServer/start.js new file mode 100644 index 0000000..3bb62be --- /dev/null +++ b/document_roots/webhost/assets/js/panelHandlers/publicServer/start.js @@ -0,0 +1,21 @@ +jQuery('._js_public-server-start').on('click', function(e){ + e.preventDefault(); + + var xhr = new XMLHttpRequest(); + xhr.open('UPDATE', 'api/server/start', true); + xhr.send(); + + xhr.onloadend = function() { + if(xhr.status == 204) { + getPublicServerStatus(); + } + else { + if(xhr.response) { + alert(xhr.response); + } + else { + alert('An error has occurred, please contact your webhost administrator.'); + } + } + } +}); diff --git a/document_roots/webhost/assets/js/panelHandlers/publicServer/status.js b/document_roots/webhost/assets/js/panelHandlers/publicServer/status.js new file mode 100644 index 0000000..f44f14d --- /dev/null +++ b/document_roots/webhost/assets/js/panelHandlers/publicServer/status.js @@ -0,0 +1,38 @@ +var statusSpan = jQuery("._js_public-server-status"); + +function getPublicServerStatus() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'api/server/status', true); + xhr.send(); + + xhr.onloadend = function() { + if(xhr.status == 200) { + statusSpan.attr('class', '_js_public-server-status'); + + switch(parseInt(xhr.response)) { + case 0: + statusSpan.addClass('text-danger').html("OFF"); + break; + case 1: + statusSpan.addClass('text-success').html("ON"); + break; + case 2: + statusSpan.addClass('text-warning').html("MAINTENANCE"); + break; + case 3: + statusSpan.addClass('text-info').html("RESTARTING"); + break; + default: + console.log(xhr.response); + statusSpan.addClass('text-danger').html('ERROR'); + break; + } + } + else { + statusSpan.attr('class', '_js_public-server-status').addClass('text-danger').html('ERROR'); + } + } +} + +// Run it once on load +getPublicServerStatus(); diff --git a/document_roots/webhost/gPanel.html b/document_roots/webhost/gPanel.html index 7b58171..d938ccc 100644 --- a/document_roots/webhost/gPanel.html +++ b/document_roots/webhost/gPanel.html @@ -21,7 +21,7 @@ -
+
@@ -34,13 +34,15 @@

Public Server

Handle operations for the public serving server
- - - - + + + + + +
@@ -64,8 +66,15 @@ + + + + + + + diff --git a/document_roots/webhost/index.html b/document_roots/webhost/index.html index 2fa7516..283302e 100644 --- a/document_roots/webhost/index.html +++ b/document_roots/webhost/index.html @@ -32,7 +32,7 @@
-
+
@@ -52,7 +52,7 @@
- +
diff --git a/main.go b/main.go index 568315a..f262ee4 100644 --- a/main.go +++ b/main.go @@ -4,22 +4,20 @@ import ( "log" "net/http" - "github.com/Ennovar/gPanel/pkg/public" "github.com/Ennovar/gPanel/pkg/webhost" "github.com/gorilla/context" ) func main() { - webhost := webhost.NewPrivateHost() - public := public.NewPublicWeb() + webhost := webhost.New() log.Printf("To Exit: CTRL+C") go func() { - log.Print("Listening (private) on localhost:2082, serving out of the document_roots/webhost/ directory...") - http.ListenAndServe("localhost:2082", context.ClearHandler(&webhost)) + log.Print("Listening (public) on localhost:3000, serving out of the document_roots/public/ directory...") + _ = webhost.Public.Start() }() - log.Print("Listening (public) on localhost:3000, serving out of the document_roots/public/ directory...") - http.ListenAndServe("localhost:3000", &public) + log.Print("Listening (private) on localhost:2082, serving out of the document_roots/webhost/ directory...") + http.ListenAndServe("localhost:2082", context.ClearHandler(&webhost)) } diff --git a/pkg/api/api_handler.go b/pkg/api/api_handler.go index 99ea2fc..7ad8146 100644 --- a/pkg/api/api_handler.go +++ b/pkg/api/api_handler.go @@ -5,23 +5,40 @@ import ( "net/http" "strings" + "github.com/Ennovar/gPanel/pkg/api/server" "github.com/Ennovar/gPanel/pkg/api/user" + "github.com/Ennovar/gPanel/pkg/public" ) // HandleAPI function takes a path and determines if it is an API call, if it is it will // call the specified API. It returns two booleans, the first being if it is an API call and // the second is the response of the API call's function. -func HandleAPI(path string, res http.ResponseWriter, req *http.Request) (bool, bool) { - splitUrl := strings.Split(path, "/") +// +// The path of an API is formatted as such: document_roots/webhost/api/some/thing. The path is split +// by the sequence of characters "api", returning the first half of the URL which is discarded, and the +// second half which will look like /some/thing. The second half is processed to see whether or not it is +// a valid API, and then handled accordingly from there. +func HandleAPI(res http.ResponseWriter, req *http.Request, path string, publicServer *public.Controller) (bool, bool) { + splitUrl := strings.SplitN(path, "api", 2) suspectApi := strings.ToLower(splitUrl[len(splitUrl)-1]) switch suspectApi { - case "api/user/auth": + case "/user/auth": return true, user.Auth(res, req) - case "api/user/register": + case "/user/register": return true, user.Register(res, req) - case "api/user/logout": + case "/user/logout": return true, user.Logout(res, req) + case "/server/status": + return true, server.Status(res, req, publicServer) + case "/server/start": + return true, server.Start(res, req, publicServer) + case "/server/shutdown": + return true, server.Shutdown(res, req, publicServer) + case "/server/restart": + return true, server.Restart(res, req, publicServer) + case "/server/maintenance": + return true, server.Maintenance(res, req, publicServer) default: return false, false } diff --git a/pkg/api/server/maintenance.go b/pkg/api/server/maintenance.go index 6eb08ff..40eb7f8 100644 --- a/pkg/api/server/maintenance.go +++ b/pkg/api/server/maintenance.go @@ -1,8 +1,20 @@ // Package server is a child of package api to handle api calls concerning the server package server -import "net/http" +import ( + "net/http" + + "github.com/Ennovar/gPanel/pkg/public" +) + +func Maintenance(res http.ResponseWriter, req *http.Request, publicServer *public.Controller) bool { + if req.Method != "UPDATE" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + publicServer.Maintenance() + res.WriteHeader(http.StatusNoContent) -func Maintenance(res http.ResponseWriter, req *http.Request) bool { return true } diff --git a/pkg/api/server/restart.go b/pkg/api/server/restart.go index a010e93..9098c07 100644 --- a/pkg/api/server/restart.go +++ b/pkg/api/server/restart.go @@ -1,8 +1,36 @@ // Package server is a child of package api to handle api calls concerning the server package server -import "net/http" +import ( + "encoding/json" + "net/http" + + "github.com/Ennovar/gPanel/pkg/public" +) + +func Restart(res http.ResponseWriter, req *http.Request, publicServer *public.Controller) bool { + if req.Method != "UPDATE" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + var restartRequestData struct { + graceful bool `json:"graceful"` + } + + err := json.NewDecoder(req.Body).Decode(&restartRequestData) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + err = publicServer.Restart(restartRequestData.graceful) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + res.WriteHeader(http.StatusNoContent) -func Restart(res http.ResponseWriter, req *http.Request) bool { return true } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go deleted file mode 100644 index 9d01ff0..0000000 --- a/pkg/api/server/server.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package server is a child of package api to handle api calls concerning the server -package server diff --git a/pkg/api/server/shutdown.go b/pkg/api/server/shutdown.go index 76d5566..178b91d 100644 --- a/pkg/api/server/shutdown.go +++ b/pkg/api/server/shutdown.go @@ -1,8 +1,36 @@ // Package server is a child of package api to handle api calls concerning the server package server -import "net/http" +import ( + "encoding/json" + "net/http" + + "github.com/Ennovar/gPanel/pkg/public" +) + +func Shutdown(res http.ResponseWriter, req *http.Request, publicServer *public.Controller) bool { + if req.Method != "UPDATE" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + var shutdownRequestData struct { + graceful bool `json:"graceful"` + } + + err := json.NewDecoder(req.Body).Decode(&shutdownRequestData) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + err = publicServer.Stop(shutdownRequestData.graceful) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + res.WriteHeader(http.StatusNoContent) -func Shutdown(res http.ResponseWriter, req *http.Request) bool { return true } diff --git a/pkg/api/server/start.go b/pkg/api/server/start.go index 2b9b035..1b5c1b4 100644 --- a/pkg/api/server/start.go +++ b/pkg/api/server/start.go @@ -1,8 +1,24 @@ // Package server is a child of package api to handle api calls concerning the server package server -import "net/http" +import ( + "net/http" -func Start(res http.ResponseWriter, req *http.Request) bool { + "github.com/Ennovar/gPanel/pkg/public" +) + +func Start(res http.ResponseWriter, req *http.Request, publicServer *public.Controller) bool { + if req.Method != "UPDATE" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + err := publicServer.Start() + if err != nil { + http.Error(res, err.Error(), http.StatusConflict) + return false + } + + res.WriteHeader(http.StatusNoContent) return true } diff --git a/pkg/api/server/status.go b/pkg/api/server/status.go index 7b4014d..fba7927 100644 --- a/pkg/api/server/status.go +++ b/pkg/api/server/status.go @@ -1,8 +1,21 @@ // Package server is a child of package api to handle api calls concerning the server package server -import "net/http" +import ( + "net/http" + "strconv" + + "github.com/Ennovar/gPanel/pkg/public" +) + +func Status(res http.ResponseWriter, req *http.Request, publicServer *public.Controller) bool { + if req.Method != "GET" { + http.Error(res, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return false + } + + res.WriteHeader(http.StatusOK) + res.Write([]byte(strconv.Itoa(publicServer.Status))) -func Status(res http.ResponseWriter, req *http.Request) bool { return true } diff --git a/pkg/api/user/get_secret.go b/pkg/api/user/get_secret.go index 951bced..649e979 100644 --- a/pkg/api/user/get_secret.go +++ b/pkg/api/user/get_secret.go @@ -11,6 +11,7 @@ func GetSecret(user string) (string, error) { if err != nil { return "", err } + defer ds.Close() err = ds.Get(database.BUCKET_USERS, []byte(user), &userDatabaseData) if err != nil { diff --git a/pkg/public/public.go b/pkg/public/public.go index 685e4e1..d7b4fb3 100644 --- a/pkg/public/public.go +++ b/pkg/public/public.go @@ -2,40 +2,139 @@ package public import ( + "context" + "errors" + "fmt" "io" "net/http" "os" + "time" "github.com/Ennovar/gPanel/pkg/logging" "github.com/Ennovar/gPanel/pkg/routing" ) -type PublicWeb struct { - Directory string - MaintenanceMode int +type Controller struct { + Directory string + GracefulShutdownTimeout time.Duration + Status int } -// NewPublicWeb returns a new PublicWeb type. -func NewPublicWeb() PublicWeb { - return PublicWeb{ - Directory: "document_roots/public/", - MaintenanceMode: 0, +var controller Controller +var server http.Server + +// New function returns a new PublicWeb type. +func New() *Controller { + controller = Controller{ + Directory: "document_roots/public/", + GracefulShutdownTimeout: 5 * time.Second, + Status: 0, } + + server = http.Server{ + Addr: "localhost:3000", + Handler: &controller, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + MaxHeaderBytes: 0, + } + + return &controller +} + +// Start function starts listening on the public server +func (con *Controller) Start() error { + if con.Status == 1 { + return errors.New("Public server is already on.") + } + + con.Status = 1 + go server.ListenAndServe() + return nil +} + +// Stop function stops the server gracefully or forceful, depending on the boolean input +func (con *Controller) Stop(graceful bool) error { + if graceful { + context, cancel := context.WithTimeout(context.Background(), con.GracefulShutdownTimeout) + defer cancel() + + err := server.Shutdown(context) + if err != nil { + fmt.Printf("Graceful shutdown failed attempting forced: %v\n", err) + + err = server.Close() + if err != nil { + return err + } + } + } + + err := server.Close() + if err != nil { + return err + } + + con.Status = 0 + return nil +} + +// Restart function combines both the start and stop function, using different +// status codes, as it is restarting. +func (con *Controller) Restart(graceful bool) error { + con.Status = 3 + + if graceful { + context, cancel := context.WithTimeout(context.Background(), con.GracefulShutdownTimeout) + defer cancel() + + err := server.Shutdown(context) + if err != nil { + fmt.Printf("Graceful shutdown failed attempting forced: %v\n", err) + + err = server.Close() + if err != nil { + return err + } + } + } + + err := server.Close() + if err != nil { + return err + } + + con.Status = 1 + go server.ListenAndServe() + return nil +} + +func (con *Controller) Maintenance() { + con.Status = 2 } // ServeHTTP function routes all requests for the public web server. It is used in the main // function inside of the http.ListenAndServe() function for the public host. -func (pub *PublicWeb) ServeHTTP(res http.ResponseWriter, req *http.Request) { - if pub.MaintenanceMode == 1 { - res.WriteHeader(http.StatusServiceUnavailable) - res.Write([]byte("We are currently in maintenance mode, please check back later.")) +func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) { + switch con.Status { + case 0: // This will actually never show because this function won't run if the server is off + http.Error(res, "The server is currently down and not serving requests.", http.StatusServiceUnavailable) + return + case 1: // Normal + break + case 2: // Maintenance mode + http.Error(res, "The server is currently maintenance mode and not serving requests.", http.StatusServiceUnavailable) + return + case 3: // This will actually never show because this function won't run if the server is off + http.Error(res, "The server is currently restarting.", http.StatusServiceUnavailable) + return } path := req.URL.Path[1:] if len(path) == 0 { - path = (pub.Directory + "index.html") + path = (con.Directory + "index.html") } else { - path = (pub.Directory + path) + path = (con.Directory + path) } f, err := os.Open(path) diff --git a/pkg/webhost/webhost.go b/pkg/webhost/webhost.go index a62c872..32f001c 100644 --- a/pkg/webhost/webhost.go +++ b/pkg/webhost/webhost.go @@ -8,28 +8,31 @@ import ( "github.com/Ennovar/gPanel/pkg/api" "github.com/Ennovar/gPanel/pkg/logging" + "github.com/Ennovar/gPanel/pkg/public" "github.com/Ennovar/gPanel/pkg/routing" ) -type PrivateHost struct { +type Controller struct { Directory string + Public *public.Controller } -// NewPrivateHost returns a new PrivateHost type. -func NewPrivateHost() PrivateHost { - return PrivateHost{ +// New returns a new PrivateHost type. +func New() Controller { + return Controller{ Directory: "document_roots/webhost/", + Public: public.New(), } } // ServeHTTP function routes all requests for the private webhost server. It is used in the main // function inside of the http.ListenAndServe() function for the private webhost host. -func (priv *PrivateHost) ServeHTTP(res http.ResponseWriter, req *http.Request) { +func (con *Controller) ServeHTTP(res http.ResponseWriter, req *http.Request) { path := req.URL.Path[1:] if len(path) == 0 { - path = (priv.Directory + "index.html") + path = (con.Directory + "index.html") } else { - path = (priv.Directory + path) + path = (con.Directory + path) } if reqAuth(path) { @@ -39,7 +42,7 @@ func (priv *PrivateHost) ServeHTTP(res http.ResponseWriter, req *http.Request) { } } - isApi, _ := api.HandleAPI(path, res, req) + isApi, _ := api.HandleAPI(res, req, path, con.Public) if isApi { // API methods handle HTTP logic from here