Compare commits

...

2 Commits

7 changed files with 172 additions and 40 deletions

20
.gitignore vendored Normal file
View File

@ -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

View File

@ -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
<meta name="go-import" content="{package} git {url location of the package}">
```
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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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 {

78
main.go
View File

@ -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())
}