diff --git a/document_roots/webhost/api_testing.html b/document_roots/webhost/api_testing.html deleted file mode 100644 index 6058327..0000000 --- a/document_roots/webhost/api_testing.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - Private Test - - - -
- - - - - - - - - - - - - - - - - - - -
gPanel Login
Username:
Password:
-
- -
- - - - - - - - - - - - - - - - - - - -
gPanel Registration
Username:
Password:
-
- -
- - - - - - - - - - - -
gPanel Logout
-
- - - - diff --git a/document_roots/webhost/gPanel.html b/document_roots/webhost/gPanel.html index 599f395..7b58171 100644 --- a/document_roots/webhost/gPanel.html +++ b/document_roots/webhost/gPanel.html @@ -39,6 +39,9 @@ + diff --git a/pkg/api/api_handler.go b/pkg/api/api_handler.go index 11a48d6..920e836 100644 --- a/pkg/api/api_handler.go +++ b/pkg/api/api_handler.go @@ -4,6 +4,8 @@ package api import ( "net/http" "strings" + + "github.com/Ennovar/gPanel/pkg/api/user" ) // HandleAPI function takes a path and determines if it is an API call, if it is it will @@ -15,11 +17,11 @@ func HandleAPI(path string, res http.ResponseWriter, req *http.Request) (bool, b switch suspectApi { case "user_auth": - return true, UserAuthentication(res, req) + return true, user.UserAuthentication(res, req) case "user_register": - return true, UserRegistration(res, req) + return true, user.UserRegistration(res, req) case "user_logout": - return true, UserLogout(res, req) + return true, user.UserLogout(res, req) default: return false, false } diff --git a/pkg/api/user.go b/pkg/api/user.go deleted file mode 100644 index 3435966..0000000 --- a/pkg/api/user.go +++ /dev/null @@ -1,139 +0,0 @@ -// Package api handles all API calls -package api - -import ( - "encoding/json" - "net/http" - - "github.com/Ennovar/gPanel/pkg/database" - "github.com/Ennovar/gPanel/pkg/encryption" - "github.com/Ennovar/gPanel/pkg/networking" -) - -// userRequestData struct is the structure of the JSON data to be -// retrieved from the authentication API request -var userRequestData struct { - User string `json:"user"` - Pass string `json:"pass"` -} - -// userDatabaseData struct is the structure of the JSON data to be retrieved from -// the bolt database inside of the user bucket, using the username as the key. -var userDatabaseData struct { - Pass string `json:"pass"` -} - -// UserAuthentication function is accessed by an API call from the webhost root -// by accessing /user_auth and sending it a post request with userRequestData -// struct in JSON format. -func UserAuthentication(res http.ResponseWriter, req *http.Request) bool { - if req.Method != "POST" { - http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) - return false - } - - err := json.NewDecoder(req.Body).Decode(&userRequestData) - if err != nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } - - ds, err := database.Open(database.DBLOC_MAIN) - if err != nil || ds == nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } - defer ds.Close() - - err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData) - - if err == database.ErrKeyNotExist { - http.Error(res, "User does not exist.", http.StatusUnauthorized) - return false - } - - err = encryption.CheckPassword([]byte(userDatabaseData.Pass), []byte(userRequestData.Pass)) - if err != nil { - http.Error(res, err.Error(), http.StatusUnauthorized) - return false - } - - store := networking.GetStore(networking.COOKIES_USER_AUTH) - err = store.Set(res, req, "auth", true, (60 * 60 * 24)) - - if err != nil { - http.Error(res, http.StatusText(500), http.StatusInternalServerError) - return false - } - - res.WriteHeader(http.StatusNoContent) - return true -} - -// UserRegistration function is accessed by an API call from the webhost root -// by accessing /user_register and sending it a post request with userRequestData -// struct in JSON format. -func UserRegistration(res http.ResponseWriter, req *http.Request) bool { - if req.Method != "POST" { - http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) - return false - } - - err := json.NewDecoder(req.Body).Decode(&userRequestData) - if err != nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } else if len(userRequestData.User) == 0 || len(userRequestData.Pass) == 0 { - http.Error(res, "Username or password field cannot be blank", http.StatusBadRequest) - return false - } - - ds, err := database.Open(database.DBLOC_MAIN) - if err != nil || ds == nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } - defer ds.Close() - - err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData) - if err != database.ErrKeyNotExist { - http.Error(res, "Username already exists in the database", http.StatusBadRequest) - return false - } - - userDatabaseData.Pass, err = encryption.HashPassword(userRequestData.Pass) - if err != nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } - - err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData) - if err != nil { - http.Error(res, err.Error(), http.StatusBadRequest) - return false - } - - res.WriteHeader(http.StatusNoContent) - return true -} - -// UserRegistration function is accessed by an API call from the webhost root -// by accessing /user_logout and sending it an empty POST request. This function will -// delete the user-auth cookie session store -func UserLogout(res http.ResponseWriter, req *http.Request) bool { - if req.Method != "POST" { - http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) - return false - } - - store := networking.GetStore(networking.COOKIES_USER_AUTH) - err := store.Delete(res, req) - - if err != nil { - http.Error(res, http.StatusText(500), http.StatusInternalServerError) - return false - } - - res.WriteHeader(http.StatusNoContent) - return true -} diff --git a/pkg/api/user/user.go b/pkg/api/user/user.go new file mode 100644 index 0000000..2e48fd2 --- /dev/null +++ b/pkg/api/user/user.go @@ -0,0 +1,16 @@ +// Package user is a child of package api to handle api calls concerning users +package user + +// userRequestData struct is the structure of the JSON data to be +// retrieved from the authentication API request +var userRequestData struct { + User string `json:"user"` + Pass string `json:"pass"` +} + +// userDatabaseData struct is the structure of the JSON data to be retrieved from +// the bolt database inside of the user bucket, using the username as the key. +var userDatabaseData struct { + Pass string `json:"pass"` + Secret string `json:"secret"` +} diff --git a/pkg/api/user/user_auth.go b/pkg/api/user/user_auth.go new file mode 100644 index 0000000..328811f --- /dev/null +++ b/pkg/api/user/user_auth.go @@ -0,0 +1,87 @@ +// Package user is a child of package api to handle api calls concerning users +package user + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/Ennovar/gPanel/pkg/database" + "github.com/Ennovar/gPanel/pkg/encryption" + "github.com/Ennovar/gPanel/pkg/networking" + jwt "github.com/dgrijalva/jwt-go" +) + +// UserAuthentication function is accessed by an API call from the webhost root +// by accessing /user_auth and sending it a post request with userRequestData +// struct in JSON format. +func UserAuthentication(res http.ResponseWriter, req *http.Request) bool { + if req.Method != "POST" { + http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) + return false + } + + err := json.NewDecoder(req.Body).Decode(&userRequestData) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + ds, err := database.Open(database.DBLOC_MAIN) + if err != nil || ds == nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + defer ds.Close() + + err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData) + + if err == database.ErrKeyNotExist { + http.Error(res, "User does not exist.", http.StatusUnauthorized) + return false + } + + err = encryption.CheckPassword([]byte(userDatabaseData.Pass), []byte(userRequestData.Pass)) + if err != nil { + http.Error(res, err.Error(), http.StatusUnauthorized) + return false + } + + secret, err := encryption.RandomString() + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + userDatabaseData.Secret = secret + + err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } + + now := time.Now().UTC() + claims := jwt.StandardClaims{ + Subject: userRequestData.User, + NotBefore: now.Unix(), + IssuedAt: now.Unix(), + ExpiresAt: now.Add(24 * time.Hour).Unix(), + } + + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + + store := networking.GetStore(networking.COOKIES_USER_AUTH) + err = store.Set(res, req, "token", token, (60 * 60 * 24)) + err2 := store.Set(res, req, "user", userRequestData.User, (60 * 60 * 24)) + if err != nil || err2 != nil { + http.Error(res, http.StatusText(500), http.StatusInternalServerError) + return false + } + + res.WriteHeader(http.StatusNoContent) + return true +} diff --git a/pkg/api/user/user_logout.go b/pkg/api/user/user_logout.go new file mode 100644 index 0000000..b147747 --- /dev/null +++ b/pkg/api/user/user_logout.go @@ -0,0 +1,29 @@ +// Package user is a child of package api to handle api calls concerning users +package user + +import ( + "net/http" + + "github.com/Ennovar/gPanel/pkg/networking" +) + +// UserLogout function is accessed by an API call from the webhost root +// by accessing /user_logout and sending it an empty POST request. This function will +// delete the user-auth cookie session store +func UserLogout(res http.ResponseWriter, req *http.Request) bool { + if req.Method != "POST" { + http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) + return false + } + + store := networking.GetStore(networking.COOKIES_USER_AUTH) + err := store.Delete(res, req) + + if err != nil { + http.Error(res, http.StatusText(500), http.StatusInternalServerError) + return false + } + + res.WriteHeader(http.StatusNoContent) + return true +} diff --git a/pkg/api/user/user_register.go b/pkg/api/user/user_register.go new file mode 100644 index 0000000..da00c7a --- /dev/null +++ b/pkg/api/user/user_register.go @@ -0,0 +1,59 @@ +// Package user is a child of package api to handle api calls concerning users +package user + +import ( + "encoding/json" + "net/http" + + "github.com/Ennovar/gPanel/pkg/database" + "github.com/Ennovar/gPanel/pkg/encryption" +) + +// UserRegistration function is accessed by an API call from the webhost root +// by accessing /user_register and sending it a post request with userRequestData +// struct in JSON format. +func UserRegistration(res http.ResponseWriter, req *http.Request) bool { + if req.Method != "POST" { + http.Error(res, req.Method+" HTTP method is unsupported for this API.", http.StatusMethodNotAllowed) + return false + } + + err := json.NewDecoder(req.Body).Decode(&userRequestData) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return false + } else if len(userRequestData.User) == 0 || len(userRequestData.Pass) == 0 { + http.Error(res, "Username or password field cannot be blank", http.StatusBadRequest) + return false + } + + ds, err := database.Open(database.DBLOC_MAIN) + if err != nil || ds == nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + defer ds.Close() + + err = ds.Get(database.BUCKET_USERS, []byte(userRequestData.User), &userDatabaseData) + if err != database.ErrKeyNotExist { + http.Error(res, "Username already exists in the database", http.StatusBadRequest) + return false + } + + userDatabaseData.Pass, err = encryption.HashPassword(userRequestData.Pass) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + + userDatabaseData.Secret = "" + + err = ds.Put(database.BUCKET_USERS, []byte(userRequestData.User), userDatabaseData) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return false + } + + res.WriteHeader(http.StatusNoContent) + return true +} diff --git a/pkg/api/user/user_token.go b/pkg/api/user/user_token.go new file mode 100644 index 0000000..deb4526 --- /dev/null +++ b/pkg/api/user/user_token.go @@ -0,0 +1,21 @@ +// Package user is a child of package api to handle api calls concerning users +package user + +import "github.com/Ennovar/gPanel/pkg/database" + +// UserSecret is not accessible from the any client side request. It is +// only used on the server side to help verify users are who they say they +// are. +func UserSecret(user string) (string, error) { + ds, err := database.Open(database.DBLOC_MAIN) + if err != nil { + return "", err + } + + err = ds.Get(database.BUCKET_USERS, []byte(user), &userDatabaseData) + if err != nil { + return "", err + } + + return userDatabaseData.Secret, nil +} diff --git a/pkg/encryption/random_string.go b/pkg/encryption/random_string.go new file mode 100644 index 0000000..199bcfc --- /dev/null +++ b/pkg/encryption/random_string.go @@ -0,0 +1,19 @@ +// Encryption package has functions inside of it that utilize various encypting and hashing techniques +package encryption + +import ( + "crypto/rand" + "fmt" +) + +func RandomString() (string, error) { + n := 5 + b := make([]byte, n) + + if _, err := rand.Read(b); err != nil { + return "", err + } + + s := fmt.Sprintf("%X", b) + return s, nil +} diff --git a/pkg/public/public.go b/pkg/public/public.go index e241057..35e6831 100644 --- a/pkg/public/public.go +++ b/pkg/public/public.go @@ -11,19 +11,26 @@ import ( ) type PublicWeb struct { - Directory string + Directory string + MaintenanceMode int } // NewPublicWeb returns a new PublicWeb type. func NewPublicWeb() PublicWeb { return PublicWeb{ - Directory: "document_roots/public/", + Directory: "document_roots/public/", + MaintenanceMode: 0, } } // 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(w http.ResponseWriter, req *http.Request) { + if pub.MaintenanceMode == 1 { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("We are currently in maintenance mode, please check back later.")) + } + path := req.URL.Path[1:] if len(path) == 0 { path = (pub.Directory + "index.html") diff --git a/pkg/webhost/webhost.go b/pkg/webhost/webhost.go index 2232fd9..66b4224 100644 --- a/pkg/webhost/webhost.go +++ b/pkg/webhost/webhost.go @@ -8,9 +8,11 @@ import ( "strings" "github.com/Ennovar/gPanel/pkg/api" + "github.com/Ennovar/gPanel/pkg/api/user" "github.com/Ennovar/gPanel/pkg/logging" "github.com/Ennovar/gPanel/pkg/networking" "github.com/Ennovar/gPanel/pkg/routing" + jwt "github.com/dgrijalva/jwt-go" ) type PrivateHost struct { @@ -36,11 +38,10 @@ func reqAuth(path string) bool { } dismissibleFiles := []string{ - "api_testing.html", "index.html", "user_auth", "user_register", - "user_logut", + "user_logout", } for _, f := range dismissibleFiles { if strings.HasSuffix(path, f) { @@ -64,18 +65,81 @@ func (priv *PrivateHost) ServeHTTP(w http.ResponseWriter, req *http.Request) { if reqAuth(path) { store := networking.GetStore(networking.COOKIES_USER_AUTH) - session_value, err := store.Read(w, req, "auth") + session_value, err := store.Read(w, req, "user") if err != nil { + logging.Console("DEBUG::", logging.NORMAL_LOG, "1") http.Error(w, err.Error(), http.StatusInternalServerError) return } if session_value == nil { + logging.Console("DEBUG::", logging.NORMAL_LOG, "2") http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } - if auth, ok := session_value.(bool); !ok || !auth { + username, ok := session_value.(string) + if !ok { + logging.Console("DEBUG::", logging.NORMAL_LOG, "3") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + stored_secret, err := user.UserSecret(username) + if stored_secret == "" { + logging.Console("DEBUG::", logging.NORMAL_LOG, "4") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + session_value, err = store.Read(w, req, "token") + if err != nil { + logging.Console("DEBUG::", logging.NORMAL_LOG, "5") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if session_value == nil { + logging.Console("DEBUG::", logging.NORMAL_LOG, "6") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + tokenString, ok := session_value.(string) + if !ok { + logging.Console("DEBUG::", logging.NORMAL_LOG, "7") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + // if len(tokenString) < 7 { + // logging.Console("DEBUG::", logging.NORMAL_LOG, "8") + // http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + // return + // } + // tokenString = tokenString[7:] + + keyfunc := func(t *jwt.Token) (interface{}, error) { + return []byte(stored_secret), nil + } + + p := jwt.Parser{ + ValidMethods: []string{"HS256", "HS384", "HS512"}, + } + t, err := p.ParseWithClaims(tokenString, &jwt.StandardClaims{}, keyfunc) + + if err != nil { + logging.Console("DEBUG::", logging.NORMAL_LOG, username) + logging.Console("DEBUG::", logging.NORMAL_LOG, tokenString) + logging.Console("DEBUG::", logging.NORMAL_LOG, stored_secret) + logging.Console("DEBUG::", logging.NORMAL_LOG, err.Error()) + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + claims := t.Claims.(*jwt.StandardClaims) + if claims.Subject != username { + logging.Console("DEBUG::", logging.NORMAL_LOG, "10") http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return }