Simple and fast dashboard with a go backend and a mostly HTML+CSS frontend with a small amount of JS.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

267 lines
9.5 KiB

/*
Copyright 2021 Arsen Musayelyan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/base64"
"encoding/json"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"time"
)
// Create struct to store template context
type TemplateData struct {
Username string
Config Conf
User User
Error string
}
func registerRoutes(app App) {
// Root endpoint, home page
app.Route.Path("/").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// If HEAD request received
if req.Method == http.MethodHead {
// Reply with code 200 OK
res.WriteHeader(http.StatusOK)
// Return from function
return
}
// Get session by name from config
session, _ := app.Session.Get(req, app.Config.Session.Name)
// Attempt to get loggedInAs from session
loggedInAs, ok := session.Values["loggedInAs"].(string)
// If user not logged in and login is required
if !ok && app.Config.LoginRequired {
// Redirect to login page
http.Redirect(res, req, "/login", http.StatusFound)
// Return from function
return
} else if !ok && !app.Config.LoginRequired {
// If not logged in and login is not required
// Set logged in to public user
loggedInAs = "_public_"
// Set logged in user to session
session.Values["loggedInAs"] = loggedInAs
// Save session
_ = session.Save(req, res)
}
// Create template context
tmplData := TemplateData{Username: loggedInAs, Config: app.Config, User: app.Config.Users[loggedInAs]}
// Execute home template with provided context
err := app.Templates["home"].Execute(res, tmplData)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error executing home template")
Log.Warn().Err(err).Msg("Error executing home template")
return
}
})
// /login endpoint, login page
app.Route.Path("/login").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// Check request method
switch req.Method {
// If GET request
case http.MethodGet:
// Get session by name from config
session, _ := app.Session.Get(req, app.Config.Session.Name)
// Attempt to get loggedInAs from session
loggedInAs, ok := session.Values["loggedInAs"].(string)
// If logged in and not as public user
if ok && loggedInAs != "_public_" {
// Redirect back to home page
http.Redirect(res, req, "/", http.StatusFound)
}
// Get query parameter error
urlErr := req.URL.Query().Get("error")
// Execute login template with provided context
err := app.Templates["login"].Execute(res, TemplateData{Config: app.Config, Error: urlErr, Username: loggedInAs})
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error executing login template")
Log.Warn().Err(err).Msg("Error executing login template")
return
}
// If POST request
case http.MethodPost:
// Parse form in POST request body
_ = req.ParseForm()
// Get password from form
password := req.PostForm.Get("password")
// Get username from form
username := req.PostForm.Get("username")
// Get user from config by username
user, ok := app.Config.Users[username]
// If user not found
if !ok {
// Redirect to login page with error parameter set to usr
http.Redirect(res, req, "/login?error=usr", http.StatusFound)
return
}
// Compare hash stored in config to password in form
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
// If password was incorrect
if err != nil {
// Redirect to login page with error parameter set to pwd
http.Redirect(res, req, "/login?error=pwd", http.StatusFound)
return
}
// Get session by name from config
session, _ := app.Session.Get(req, app.Config.Session.Name)
// Set loggedInAs value in session to username from form
session.Values["loggedInAs"] = username
// Save session
err = session.Save(req, res)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error saving session")
Log.Warn().Err(err).Msg("Error saving session")
return
}
// Redirect to homepage
http.Redirect(res, req, "/", http.StatusFound)
}
})
// /logout endpoint, logout and redirect
app.Route.Path("/logout").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// Get session by name from config
session, _ := app.Session.Get(req, app.Config.Session.Name)
// Remove loggedInAs value from session
delete(session.Values, "loggedInAs")
// Save session
err := session.Save(req, res)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error saving session")
Log.Warn().Err(err).Msg("Error while handling error")
return
}
// Redirect to login page
http.Redirect(res, req, "/login", http.StatusFound)
})
// /status/:b64url endpoint, return status of base64 encoded URL as JSON
app.Route.Path("/status/{b64url}").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// Get path variables
vars := mux.Vars(req)
// Create JSON encoder writing to response
enc := json.NewEncoder(res)
// Decode base64 URL string
url, _ := base64.StdEncoding.DecodeString(vars["b64url"])
// Create new HEAD request to check status without downloading whole page
headReq, err := http.NewRequest(http.MethodHead, string(url), nil)
if err != nil {
// Encode down status and write to response
_ = enc.Encode(map[string]interface{}{"code": 0, "down": true})
Log.Warn().Err(err).Msg("Error creating HEAD request for status check")
return
}
// Create new HTTP client with 5 second timeout
client := http.Client{Timeout: 5 * time.Second}
// Use client to do request created above
headRes, err := client.Do(headReq)
if err != nil {
// Encode down status and write to response
_ = enc.Encode(map[string]interface{}{"code": 0, "down": true})
Log.Warn().Err(err).Msg("Error executing HEAD request for status check")
return
}
// Encode returned status and write to response
_ = enc.Encode(map[string]interface{}{"code": headRes.StatusCode, "down": false})
})
// /proxy/:b64url endpoint, proxy HTTP connection bypassing CORS
app.Route.Path("/proxy/{b64url}").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// Get path variables
vars := mux.Vars(req)
// Decode base64 URL string
url, _ := base64.StdEncoding.DecodeString(vars["b64url"])
// If URL is allowed for proxy
if strSlcContains(app.Config.AllowProxy, string(url)) {
// Create new HTTP request with the same parameters as sent to endpoint
proxyReq, err := http.NewRequest(req.Method, string(url), req.Body)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Proxying connection failed")
Log.Warn().Err(err).Msg("Error creating request for proxy")
return
}
// Create new HTTP client with 5 second timeout
client := http.Client{Timeout: 5 * time.Second}
proxyReq.Header = req.Header
// Use client to do request created above
proxyRes, err := client.Do(proxyReq)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Proxying connection failed")
Log.Warn().Err(err).Msg("Error executing request for proxy")
return
}
// Close proxy response body at end of function
defer proxyRes.Body.Close()
/*
var proxyReader io.Reader
switch proxyRes.Header.Get("Content-Type") {
case "application/x-gzip":
gzReader, err := gzip.NewReader(proxyRes.Body)
if err != nil {
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Decompressing response failed")
Log.Warn().Err(err).Msg("Error executing request for proxy")
return
}
proxyReader = gzReader
} else {
proxyReader = proxyRes.Body
}
*/
for key := range proxyRes.Header {
res.Header().Set(key, proxyRes.Header.Get(key))
}
// Copy data from proxy response to response
io.Copy(res, proxyRes.Body)
} else {
httpError(res, app.Templates["error"], app.Config, http.StatusBadRequest, "This url is not in allowedProxy")
Log.Warn().Str("url", string(url)).Msg("URL is disallowed for proxy")
return
}
})
// Catch-all route, gzip-compressing file server
app.Route.PathPrefix("/").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
// Create OS-specific path to requested file
filePath := filepath.Join("resources", "public", req.URL.Path)
// Open requested file
file, err := os.Open(filePath)
if err != nil {
Log.Warn().Str("file", filePath).Msg("File not found")
httpError(res, app.Templates["error"], app.Config, http.StatusNotFound, "This file was not found")
return
}
// Set Content-Type header based on file extension
res.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(filePath)))
// Close file at end of function
defer file.Close()
// Compress response
gzRes := compressRes(res)
// Close compressed response at end of function
defer gzRes.Close()
// Copy file contents to compressed response
io.Copy(gzRes, file)
})
}