/* 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) }) }