Created
October 29, 2020 12:45
-
-
Save bencrowder/45f185b3473c50e9733e1df834c8c0cf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Gate is a small web app for quick text entry along with forwarding entered | |
// text to other endpoints (which I use heavily in my personal workflow). I've | |
// only done a little coding in Go so far, so I'm sure this code is probably | |
// not as idiomatic as I'd like. (I'd also ordinarily split this up into | |
// multiple files. And do several things differently. Hindsight!) | |
package main | |
import ( | |
"crypto/md5" | |
"encoding/hex" | |
"encoding/json" | |
"errors" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"net/url" | |
"os" | |
"strings" | |
"time" | |
"github.com/flosch/pongo2" | |
"gopkg.in/yaml.v2" | |
) | |
const configFilename = "actions.yaml" | |
// Action used in config | |
type Action struct { | |
Slug string | |
Label string | |
Payload string | |
Callback string | |
URL string | |
FullSlug string | |
} | |
// Config is what the YAML gets unmarshalled into | |
type Config struct { | |
Apps map[string]struct { | |
Host string | |
URL string | |
Field string | |
Callback string | |
Actions []Action | |
} | |
Board []struct { | |
Label string | |
App string | |
Groups []struct { | |
Actions []string | |
} | |
} | |
Port string | |
Basepath string | |
Auth struct { | |
Username string | |
Password string | |
Sesame string | |
} | |
} | |
// ConfigChunk is used for the config for a slug/app | |
type ConfigChunk struct { | |
URL string | |
Callback string | |
PayloadTemplate string | |
Field string | |
} | |
// SectionGroup is used for board sections | |
type SectionGroup struct { | |
Actions []Action | |
} | |
// BoardSection is used for the display of the actions | |
type BoardSection struct { | |
Label string | |
Groups []SectionGroup | |
} | |
// BoardSections is the list of sections | |
type BoardSections []BoardSection | |
// Response is for what we respond with | |
type Response struct { | |
Status string | |
Success bool | |
Message string | |
Callback string | |
} | |
var config Config | |
var actions map[string]Action | |
func loadYaml() (data []byte, err error) { | |
data, err = ioutil.ReadFile(configFilename) | |
return data, err | |
} | |
func (config *Config) loadConfigForSlug(target string, app string, slug string) (chunk ConfigChunk, err error) { | |
chunk = ConfigChunk{} | |
appConfig := config.Apps[app] | |
url := appConfig.URL | |
callback := appConfig.Callback | |
chunk.Field = appConfig.Field | |
host := appConfig.Host | |
var selectedAction Action | |
for key, action := range actions { | |
if target == key { | |
selectedAction = action | |
break | |
} | |
} | |
if selectedAction.Slug == "" { | |
// Didn't find the action | |
return ConfigChunk{}, errors.New("Action not found: " + target) | |
} | |
// Get URL if present on action | |
if selectedAction.URL != "" { | |
url = selectedAction.URL | |
} | |
// Get callback if present on action | |
if selectedAction.Callback != "" { | |
callback = selectedAction.Callback | |
} | |
// Replace host | |
chunk.URL = strings.Replace(url, "{host}", host, 1) | |
chunk.Callback = strings.Replace(callback, "{host}", host, 1) | |
chunk.PayloadTemplate = selectedAction.Payload | |
return chunk, nil | |
} | |
// loadConfig loads the configuration YAML into the config object | |
func loadConfig() (config Config) { | |
yamlData, err := loadYaml() | |
if err != nil { | |
log.Fatalf("Error loading YAML: %v", err) | |
} | |
config = Config{} | |
err = yaml.Unmarshal([]byte(yamlData), &config) | |
if err != nil { | |
log.Fatalf("error: %v", err) | |
} | |
// Load things into the map first | |
actions = make(map[string]Action) | |
for appKey, app := range config.Apps { | |
for _, action := range app.Actions { | |
slug := "@" + appKey + "." + action.Slug | |
action.FullSlug = slug | |
actions[slug] = action | |
} | |
} | |
// Update basepath | |
baseURL := "/" | |
if config.Basepath != "/" { | |
baseURL = config.Basepath + "/" | |
} | |
config.Basepath = baseURL | |
return config | |
} | |
func parseSlugLine(slugLine string) (app string, slug string, err error) { | |
// Turns @liszt.projects/next into "liszt", "projects/next" | |
data := strings.Split(slugLine, ".") | |
app = strings.Trim(data[0], "@") | |
slug = data[1] | |
if app == "" || slug == "" { | |
err = errors.New("App or slug incorrectly formed") | |
} else { | |
err = nil | |
} | |
return app, slug, err | |
} | |
func (chunk *ConfigChunk) postPayload(payload string) (Response, error) { | |
// Process payload via template | |
payloadData := strings.Replace(chunk.PayloadTemplate, "{payload}", payload, 1) | |
// POST to the target | |
resp, err := http.PostForm( | |
chunk.URL, | |
url.Values{ | |
chunk.Field: {payloadData}, | |
}, | |
) | |
if err != nil { | |
return Response{}, err | |
} | |
defer resp.Body.Close() | |
body, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return Response{}, err | |
} | |
response := Response{} | |
response.Status = resp.Status | |
if resp.Status == "200 OK" { | |
response.Success = true | |
} else { | |
return Response{}, errors.New(string(body[:])) | |
} | |
response.Callback = chunk.Callback | |
return response, nil | |
} | |
func processPayloadHandler(w http.ResponseWriter, r *http.Request) { | |
var target = "" | |
var payload = "" | |
if !isAuthenticated(r) { | |
// Make sure we're authenticated | |
username, password, ok := r.BasicAuth() | |
if !ok { | |
w.WriteHeader(http.StatusUnauthorized) | |
fmt.Fprintf(w, "{\"error\": \"Not authenticated\"}") | |
return | |
} | |
if username != config.Auth.Username { | |
w.WriteHeader(http.StatusUnauthorized) | |
fmt.Fprintf(w, "{\"error\": \"Bad person\"}") | |
return | |
} | |
if password != config.Auth.Password { | |
w.WriteHeader(http.StatusUnauthorized) | |
fmt.Fprintf(w, "{\"error\": \"Bad secrets\"}") | |
return | |
} | |
} | |
if r.Method == "POST" { | |
target = r.FormValue("target") | |
payload = r.FormValue("payload") | |
} else { | |
return | |
} | |
savePayload(target, payload) | |
if target == "" || payload == "" { | |
w.WriteHeader(http.StatusNotAcceptable) | |
fmt.Fprintf(w, "{\"error\": \"Missing parameters\"}") | |
return | |
} | |
app, slug, err := parseSlugLine(target) | |
if err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
fmt.Fprintf(w, "{\"error\": \"%v\"}", err) | |
return | |
} | |
chunk, err := config.loadConfigForSlug(target, app, slug) | |
if err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
fmt.Fprintf(w, "{\"error\": \"%v\"}", err) | |
return | |
} | |
response, err := chunk.postPayload(payload) | |
if err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
fmt.Fprintf(w, "{\"error\": \"%v\"}", err) | |
return | |
} | |
responseJSON, err := json.Marshal(response) | |
if err != nil { | |
w.WriteHeader(http.StatusInternalServerError) | |
fmt.Fprintf(w, "{\"error\": \"%v\"}", err) | |
return | |
} | |
fmt.Fprintf(w, string(responseJSON)) | |
} | |
func savePayload(target string, payload string) { | |
baseDir := "payloads/" | |
// Make sure the directory exists | |
if _, err := os.Stat(baseDir); os.IsNotExist(err) { | |
os.Mkdir(baseDir, 0700) | |
} | |
// Prep the filename | |
t := time.Now() | |
year := t.Format("2006") | |
yearPath := baseDir + year | |
if _, err := os.Stat(yearPath); os.IsNotExist(err) { | |
os.Mkdir(yearPath, 0700) | |
} | |
month := t.Format("01") | |
monthPath := yearPath + "/" + month | |
if _, err := os.Stat(monthPath); os.IsNotExist(err) { | |
os.Mkdir(monthPath, 0700) | |
} | |
formattedDate := t.Format("2006-01-02-150405.000") | |
filename := formattedDate + ".text" | |
// Concatenate the target and payload | |
output := target + "\n" + payload | |
// Write out the contents of the payload | |
ioutil.WriteFile(monthPath+"/"+filename, []byte(output), 0600) | |
} | |
func indexPageHandler(w http.ResponseWriter, r *http.Request) { | |
if !isAuthenticated(r) { | |
http.Redirect(w, r, config.Basepath+"login", http.StatusFound) | |
return | |
} | |
data, err := ioutil.ReadFile("templates/index.html") | |
if err != nil { | |
fmt.Fprintf(w, "Template error: \"%v\"", err) | |
return | |
} | |
tpl, err := pongo2.FromString(string(data)) | |
if err != nil { | |
fmt.Fprintf(w, "Template error: \"%v\"", err) | |
return | |
} | |
out, err := tpl.Execute(pongo2.Context{"config": config}) | |
if err != nil { | |
fmt.Fprintf(w, "Template error: \"%v\"", err) | |
return | |
} | |
fmt.Fprintf(w, out) | |
} | |
func md5Hash(s string) string { | |
hasher := md5.New() | |
hasher.Write([]byte(s)) | |
return hex.EncodeToString(hasher.Sum(nil)) | |
} | |
func isAuthenticated(r *http.Request) bool { | |
cookie, err := r.Cookie("gatecookie") | |
if err != nil { | |
return false | |
} | |
return cookie.Value == "redacted" | |
} | |
func loginHandler(w http.ResponseWriter, r *http.Request) { | |
var sesame string | |
if r.Method == "POST" { | |
sesame = r.FormValue("sesame") | |
} else { | |
return | |
} | |
if sesame == "" { | |
fmt.Fprintf(w, "{\"error\": \"Missing parameters\"}") | |
return | |
} | |
// Hash all the things | |
hashedKey := md5Hash(config.Auth.Sesame) | |
hashedSesame := md5Hash(sesame) | |
// Invalid sesame | |
if hashedSesame != hashedKey { | |
fmt.Fprintf(w, "{\"error\": \"Sorry, denied\"}") | |
return | |
} | |
// Matches, so set cookie and redirect | |
expiration := time.Now().Add(365 * 24 * time.Hour) | |
cookie := http.Cookie{Name: "gatecookie", Value: "redacted", Path: config.Basepath, Expires: expiration} | |
http.SetCookie(w, &cookie) | |
http.Redirect(w, r, config.Basepath, http.StatusFound) | |
} | |
func loginPageHandler(w http.ResponseWriter, r *http.Request) { | |
data, err := ioutil.ReadFile("templates/login.html") | |
if err != nil { | |
fmt.Fprintf(w, "{\"error\": \"%v\"}", err) | |
return | |
} | |
tpl, err := pongo2.FromString(string(data)) | |
if err != nil { | |
fmt.Fprintf(w, "Template error: \"%v\"", err) | |
return | |
} | |
out, err := tpl.Execute(pongo2.Context{"config": config}) | |
if err != nil { | |
fmt.Fprintf(w, "Template error: \"%v\"", err) | |
return | |
} | |
fmt.Fprintf(w, out) | |
} | |
func main() { | |
config = loadConfig() | |
http.HandleFunc(config.Basepath+"api/process", processPayloadHandler) | |
http.HandleFunc(config.Basepath+"api/login", loginHandler) | |
http.HandleFunc(config.Basepath+"login", loginPageHandler) | |
http.HandleFunc(config.Basepath, indexPageHandler) | |
http.Handle(config.Basepath+"static/", http.StripPrefix(config.Basepath+"static/", http.FileServer(http.Dir("static")))) | |
log.Fatal(http.ListenAndServe(":"+config.Port, nil)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment