diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91abe37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Binaries for programs and plugins +artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum diff --git a/README.md b/README.md index 96ea66f..c314cba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,32 @@ -# Testing +# GocustomUrls -With the go-get command +This is a homespun implementation of the practice of [using custom import paths for go modules](https://pkg.go.dev/cmd/go#hdr-Remote_import_paths). + +This package basically returns a html document with a prefilled meta tag + +```html + +``` + +There are ways to do this with [nginx](https://www.nirenjan.com/2019/golang-vanity-urls-in-nginx/) or [hugo](https://blog.jbowen.dev/2020/07/using-go-vanity-urls-with-hugo/) but I wanted to: + +* host on a subdomain +* not muck up my ngnix config + +So I used golang for this project. + +## Testing + +You can test with + +(a) [httpie](https://httpie.io/) + +```sh +$ http --body "https://{domain.name}/{package}?go-get=1" +...truncated output +``` + +(b) With the go-get command ```sh $ go get -v -u jbowen.dev/cereal @@ -8,9 +34,8 @@ get "jbowen.dev/cereal": found meta tag get.metaImport{Prefix:"jbowen.dev/cereal jbowen.dev/cereal (download) ``` -With the httpie command +## TODOs -```sh -$ http --body "https://gopkg.in/yaml.v3?go-get=1" -... -``` +* [x] Fix permission errors around opening the app.log and rules.json. +* [x] Make the flags (config, rules) required instead of optional. +* [ ] Figure how to use logrotate (a linux utility) \ No newline at end of file diff --git a/conf.go b/conf.go index dd1be0a..81973b8 100644 --- a/conf.go +++ b/conf.go @@ -41,8 +41,10 @@ func (c *Config) LoadMappingFile(fp string) error { if len(c.MappingFilePath) == 0 { ok := isFile(mappingFilePath) if !ok { - return fmt.Errorf("%s is not found", mappingFilePath) + return fmt.Errorf("mappingfile %s is not found", mappingFilePath) } + } else { + mappingFilePath = c.MappingFilePath } mappingFile, err := os.Open(mappingFilePath) if err != nil { diff --git a/embed.go b/embed.go index 8a32e90..a99889d 100644 --- a/embed.go +++ b/embed.go @@ -5,15 +5,26 @@ import ( "html/template" ) -// go:embed templates/* +// https://stackoverflow.com/questions/70193820/why-isnt-go-embedding-files +// +//go:embed templates/* var tmpls embed.FS -func GetServeHtml() *template.Template { - data, _ := tmpls.ReadFile("success.html") - return template.Must(template.New("main").Parse(string(data))) +func GetServeHtml() (*template.Template, error) { + var t *template.Template + data, err := tmpls.ReadFile("templates/success.html") + if err != nil { + return t, err + } + return template.New("").Parse(string(data)) } -func GetDefaultHtml() []byte { - data, _ := tmpls.ReadFile("default.html") - return data +func GetDefaultHtml() ([]byte, error) { + var data []byte + var err error + data, err = tmpls.ReadFile("templates/default.html") + if err != nil { + return data, err + } + return data, nil } diff --git a/handlers.go b/handlers.go index 139474c..9b097bc 100644 --- a/handlers.go +++ b/handlers.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "net/http" "strings" ) @@ -14,8 +15,9 @@ func reloadRules(c *Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := c.LoadMappingFile("") if err != nil { - errorLog.Printf("Cannot reload rules: %+v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) + e := fmt.Errorf("annot reload rules: %+v", err) + // errorLog.Printf("Cannot reload rules: %+v", err) + http.Error(w, e.Error(), http.StatusInternalServerError) return } w.Write([]byte("ok")) @@ -31,7 +33,12 @@ func serveRules(c *Config) http.HandlerFunc { // if go-get param is absent, return nothing if r.FormValue("go-get") != "1" { - w.Write(GetDefaultHtml()) + data, err := GetDefaultHtml() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) return } @@ -39,12 +46,9 @@ func serveRules(c *Config) http.HandlerFunc { var vanityUrl, proto, repoUrl string for _, rule := range c.MappingRules.Mappings { - if strings.HasPrefix(strings.ToLower(nameOfPkg), strings.ToLower(rule.VanityUrl+"/")) { - repo := strings.Replace(strings.ToLower(nameOfPkg), strings.ToLower(rule.VanityUrl), "", -1) - repo = strings.Split(repo, "/")[1] - - vanityUrl = rule.VanityUrl + "/" + repo - repoUrl = rule.RealUrl + "/" + repo + if strings.HasPrefix(strings.ToLower(rule.VanityUrl+"/"), strings.Trim(strings.ToLower(nameOfPkg), " ")) { + vanityUrl = rule.VanityUrl + repoUrl = rule.RealUrl proto = rule.Protocol break @@ -56,10 +60,14 @@ func serveRules(c *Config) http.HandlerFunc { Proto: proto, RepoUrl: repoUrl, } - tmpl := GetServeHtml() + tmpl, err := GetServeHtml() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } var buf bytes.Buffer - err := tmpl.Execute(&buf, &d) + err = tmpl.Execute(&buf, &d) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/logger.go b/logger.go index b44afcb..b28fed4 100644 --- a/logger.go +++ b/logger.go @@ -5,7 +5,9 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" + "time" ) // some headers not worth logging @@ -50,7 +52,13 @@ type LogFileRec struct { } func newFileLogger(path string) (*LogFile, error) { - f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) + requestedFile := filepath.Clean(filepath.Join("/", path)) + parentDir := filepath.Dir(requestedFile) + err := os.MkdirAll(parentDir, 0777) + if err != nil { + return nil, err + } + f, err := os.OpenFile(requestedFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { return nil, err } @@ -123,6 +131,11 @@ func shouldLogHeader(s string) bool { return !hdrsToNotLogMap[s] } +func getCurrentDate() string { + dt := time.Now() + return dt.Format(time.RFC3339Nano) +} + func (f *LogFile) WriteLog(r *http.Request) error { if f == nil { return nil @@ -132,6 +145,7 @@ func (f *LogFile) WriteLog(r *http.Request) error { rec["requestUri"] = r.RequestURI rec["Host"] = r.Host rec["ipAddr"] = extractIpAddress(r) + rec["requestDate"] = getCurrentDate() if !canSkipExtraHeaders(r) { for key, val := range r.Header { if shouldLogHeader(key) && len(val) > 0 { diff --git a/main.go b/main.go index 288ca4a..fdb2cc6 100644 --- a/main.go +++ b/main.go @@ -9,16 +9,18 @@ import ( "net/http" "os" "os/signal" + "path/filepath" + "strconv" "syscall" "time" ) var errorLog *log.Logger = log.New(os.Stderr, "", log.LstdFlags) -const ( - DEFAULT_RULES_FILE string = "/var/lib/gocustomurls/rules.json" - DEFAULT_LOG_FILE string = "/var/log/gocustomurls/app.log" -) +// const ( +// DEFAULT_RULES_FILE string = "/var/lib/gocustomurls/rules.json" +// DEFAULT_LOG_FILE string = "/var/log/gocustomurls/app.log" +// ) // flagsSet returns a set of all the flags what were actually set on the // command line. @@ -30,6 +32,35 @@ func flagsSet(flags *flag.FlagSet) map[string]bool { return s } +// generateDefaults - generate the default values for the rules.json and log files +func generateDefaults(rulesfp string, logfp string) (string, string, error) { + var newlogfp, newrulesfp string + var err error + newlogfp = logfp + newrulesfp = rulesfp + if len(newrulesfp) == 0 { + dir, err := os.UserConfigDir() + if err != nil { + return newrulesfp, newlogfp, err + } + newrulesfp = filepath.Join(dir, "gocustomcurls", "rules.json") + } + if len(newlogfp) == 0 { + dir, err := os.UserHomeDir() + if err != nil { + return newrulesfp, newlogfp, err + } + newlogfp = filepath.Join(dir, ".gocustomurls", "logs", "app.log") + } + return newrulesfp, newlogfp, err +} + +// isValidPort returns true if the port is valid +// following the RFC https://datatracker.ietf.org/doc/html/rfc6056#section-2.1 +func isValidPort(port int) bool { + return port > 0 && port < 65535 +} + func main() { programName := os.Args[0] // errorLog = log.New(os.Stderr, "", log.LstdFlags) @@ -43,9 +74,9 @@ func main() { flags.PrintDefaults() } - portFlag := flags.String("port", "7070", "port to listen to") - rulesFileFlag := flags.String("config", DEFAULT_RULES_FILE, "contains go-import mapping") - logFileFlag := flags.String("logfile", DEFAULT_LOG_FILE, "default log file") + portFlag := flags.String("port", "7070", "Optional. Default port is 7070. Port to listen to") + rulesFileFlag := flags.String("config", "", "Optional. Contains go-import mapping") + logFileFlag := flags.String("logfile", "", "Optional. Default log file") flags.Parse(os.Args[1:]) if len(flags.Args()) > 1 { @@ -59,9 +90,23 @@ func main() { var port string if allSetFlags["port"] { port = *portFlag + } else { + port = "7070" + } + + p, err := strconv.Atoi(port) + if err != nil { + errorLog.Println(err) + os.Exit(1) + } + + if !isValidPort(p) { + errorLog.Println(fmt.Errorf("provided port (%d) is not valid", p)) + os.Exit(1) } var rulesFile string + if allSetFlags["config"] { rulesFile = *rulesFileFlag } @@ -71,14 +116,20 @@ func main() { logFile = *logFileFlag } - // load rules mapping - c := &Config{} - err := c.LoadMappingFile(rulesFile) + rFile, lFile, err := generateDefaults(logFile, rulesFile) if err != nil { errorLog.Println(err) os.Exit(1) } - l, err := newFileLogger(logFile) + + // load rules mapping + c := &Config{} + err = c.LoadMappingFile(rFile) + if err != nil { + errorLog.Println(err) + os.Exit(1) + } + l, err := newFileLogger(lFile) if err != nil { errorLog.Println(err) os.Exit(1) @@ -92,12 +143,13 @@ func main() { // For graceful shutdowns go func() { + app.Log.logger.Printf("%s Starting\n", getCurrentDate()) err := srv.ListenAndServe() if !errors.Is(err, http.ErrServerClosed) { errorLog.Printf("HTTP Server error: %+v\n", err) os.Exit(1) } - app.Log.logger.Println("Stopped serving new connections.") + app.Log.logger.Printf("%s Stopped serving new connections.\n", getCurrentDate()) }() sigChan := make(chan os.Signal, 1) @@ -111,5 +163,5 @@ func main() { errorLog.Printf("HTTP shutdown error: %+v\n", err) os.Exit(1) } - app.Log.logger.Println("Graceful shutdown complete.") + app.Log.logger.Printf("%s Graceful shutdown complete.\n", getCurrentDate()) }