Compare commits
No commits in common. "f17afbd227770b2f7d01b96a55b28e949f67d0cb" and "9f30ec897e15ab4c5d615d6da66c61e5f628b8eb" have entirely different histories.
f17afbd227
...
9f30ec897e
16
README.md
16
README.md
|
@ -1,16 +0,0 @@
|
||||||
# Testing
|
|
||||||
|
|
||||||
With the go-get command
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go get -v -u jbowen.dev/cereal
|
|
||||||
get "jbowen.dev/cereal": found meta tag get.metaImport{Prefix:"jbowen.dev/cereal", VCS:"git", RepoRoot:"https://github.com/jamesbo13/cereal"} at //jbowen.dev/cereal?go-get=1
|
|
||||||
jbowen.dev/cereal (download)
|
|
||||||
```
|
|
||||||
|
|
||||||
With the httpie command
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ http --body "https://gopkg.in/yaml.v3?go-get=1"
|
|
||||||
...
|
|
||||||
```
|
|
30
app.go
30
app.go
|
@ -1,30 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Application struct {
|
|
||||||
Config *Config
|
|
||||||
Mux *http.ServeMux
|
|
||||||
Log *LogFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) routes() {
|
|
||||||
m := http.NewServeMux()
|
|
||||||
|
|
||||||
m.HandleFunc("/healthcheck", healthcheck)
|
|
||||||
m.HandleFunc("/reloadRules", reloadRules(app.Config))
|
|
||||||
m.HandleFunc("/", serveLogger(app.Log)(serveRules(app.Config)))
|
|
||||||
|
|
||||||
app.Mux = m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) Setup(port string) *http.Server {
|
|
||||||
app.routes()
|
|
||||||
return &http.Server{
|
|
||||||
Addr: fmt.Sprintf(":%s", port),
|
|
||||||
Handler: app.Mux,
|
|
||||||
}
|
|
||||||
}
|
|
62
conf.go
62
conf.go
|
@ -1,62 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
MappingFilePath string
|
|
||||||
MappingRules ImportRulesMappings
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImportRulesMappings struct {
|
|
||||||
Mappings []struct {
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
VanityUrl string `json:"vanity_url"`
|
|
||||||
RealUrl string `json:"real_url"`
|
|
||||||
} `json:"mappings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImportRuleStruct struct {
|
|
||||||
VanityUrl string
|
|
||||||
Proto string
|
|
||||||
RepoUrl string
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFile - check if fp is a valid file
|
|
||||||
func isFile(fp string) bool {
|
|
||||||
info, err := os.Stat(fp)
|
|
||||||
if os.IsNotExist(err) || !info.Mode().IsRegular() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// load mapping file
|
|
||||||
func (c *Config) LoadMappingFile(fp string) error {
|
|
||||||
var mapping ImportRulesMappings
|
|
||||||
mappingFilePath := fp
|
|
||||||
if len(c.MappingFilePath) == 0 {
|
|
||||||
ok := isFile(mappingFilePath)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%s is not found", mappingFilePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mappingFile, err := os.Open(mappingFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer mappingFile.Close()
|
|
||||||
|
|
||||||
err = json.NewDecoder(mappingFile).Decode(&mapping)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.MappingRules = mapping
|
|
||||||
if len(c.MappingFilePath) == 0 {
|
|
||||||
c.MappingFilePath = mappingFilePath
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
19
embed.go
19
embed.go
|
@ -1,19 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"html/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 GetDefaultHtml() []byte {
|
|
||||||
data, _ := tmpls.ReadFile("default.html")
|
|
||||||
return data
|
|
||||||
}
|
|
71
handlers.go
71
handlers.go
|
@ -1,71 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func healthcheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveRules(c *Config) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if go-get param is absent, return nothing
|
|
||||||
if r.FormValue("go-get") != "1" {
|
|
||||||
w.Write(GetDefaultHtml())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nameOfPkg := r.Host + r.URL.Path
|
|
||||||
|
|
||||||
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
|
|
||||||
proto = rule.Protocol
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d := ImportRuleStruct{
|
|
||||||
VanityUrl: vanityUrl,
|
|
||||||
Proto: proto,
|
|
||||||
RepoUrl: repoUrl,
|
|
||||||
}
|
|
||||||
tmpl := GetServeHtml()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := tmpl.Execute(&buf, &d)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "public, max-age=500")
|
|
||||||
w.Write(buf.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
148
logger.go
148
logger.go
|
@ -1,148 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// some headers not worth logging
|
|
||||||
var (
|
|
||||||
hdrsToNotLog = []string{
|
|
||||||
"Accept-Language",
|
|
||||||
"Cache-Control",
|
|
||||||
"Cf-Ray",
|
|
||||||
"CF-Visitor",
|
|
||||||
"CF-Connecting-IP",
|
|
||||||
"Cdn-Loop",
|
|
||||||
"Cookie",
|
|
||||||
"Connection",
|
|
||||||
"Dnt",
|
|
||||||
"If-Modified-Since",
|
|
||||||
"Sec-Fetch-Dest",
|
|
||||||
"Sec-Ch-Ua-Mobile",
|
|
||||||
// "Sec-Ch-Ua",
|
|
||||||
"Sec-Ch-Ua-Platform",
|
|
||||||
"Sec-Fetch-Site",
|
|
||||||
"Sec-Fetch-Mode",
|
|
||||||
"Sec-Fetch-User",
|
|
||||||
"Upgrade-Insecure-Requests",
|
|
||||||
"X-Request-Start",
|
|
||||||
"X-Forwarded-For",
|
|
||||||
"X-Forwarded-Proto",
|
|
||||||
"X-Forwarded-Host",
|
|
||||||
}
|
|
||||||
hdrsToNotLogMap map[string]bool
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogFile struct {
|
|
||||||
handle *os.File
|
|
||||||
logger *log.Logger
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogFileRec struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
IpAddr string `json:"ipAddr"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFileLogger(path string) (*LogFile, error) {
|
|
||||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &LogFile{
|
|
||||||
handle: f,
|
|
||||||
logger: log.New(f, "", 0),
|
|
||||||
path: path,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *LogFile) Close() error {
|
|
||||||
if f == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := f.handle.Close()
|
|
||||||
f.handle = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFirstFragment(header *http.Header, headerName string) string {
|
|
||||||
s := header.Get(headerName)
|
|
||||||
if len(strings.TrimSpace(s)) == 0 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
fragments := strings.Split(s, ",")
|
|
||||||
return strings.TrimSpace(fragments[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Ip Address of the client
|
|
||||||
func extractIpAddress(r *http.Request) string {
|
|
||||||
var ipAddr string
|
|
||||||
if r == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
possibleIpHeaders := []string{"CF-Connecting-IP", "X-Real-Ip", "X-Forwarded-For"}
|
|
||||||
for _, header := range possibleIpHeaders {
|
|
||||||
ipAddr = extractFirstFragment(&r.Header, header)
|
|
||||||
if len(strings.TrimSpace(ipAddr)) != 0 {
|
|
||||||
return ipAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// pull ip from Request.RemoteAddr
|
|
||||||
if len(strings.TrimSpace(r.RemoteAddr)) != 0 {
|
|
||||||
index := strings.LastIndex(r.RemoteAddr, ";")
|
|
||||||
if index == -1 {
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
ipAddr = r.RemoteAddr[:index]
|
|
||||||
}
|
|
||||||
return ipAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func canSkipExtraHeaders(r *http.Request) bool {
|
|
||||||
ref := r.Header.Get("Referer")
|
|
||||||
if len(strings.TrimSpace(ref)) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(ref, r.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldLogHeader(s string) bool {
|
|
||||||
if hdrsToNotLogMap == nil {
|
|
||||||
hdrsToNotLogMap = map[string]bool{}
|
|
||||||
for _, h := range hdrsToNotLog {
|
|
||||||
h = strings.ToLower(h)
|
|
||||||
hdrsToNotLogMap[h] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s = strings.ToLower(s)
|
|
||||||
return !hdrsToNotLogMap[s]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *LogFile) WriteLog(r *http.Request) error {
|
|
||||||
if f == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var rec = make(map[string]string)
|
|
||||||
rec["method"] = r.Method
|
|
||||||
rec["requestUri"] = r.RequestURI
|
|
||||||
rec["Host"] = r.Host
|
|
||||||
rec["ipAddr"] = extractIpAddress(r)
|
|
||||||
if !canSkipExtraHeaders(r) {
|
|
||||||
for key, val := range r.Header {
|
|
||||||
if shouldLogHeader(key) && len(val) > 0 {
|
|
||||||
rec[key] = val[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(rec)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.logger.Println(string(b))
|
|
||||||
return nil
|
|
||||||
}
|
|
110
main.go
110
main.go
|
@ -1,115 +1,5 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flagsSet returns a set of all the flags what were actually set on the
|
|
||||||
// command line.
|
|
||||||
func flagsSet(flags *flag.FlagSet) map[string]bool {
|
|
||||||
s := make(map[string]bool)
|
|
||||||
flags.Visit(func(f *flag.Flag) {
|
|
||||||
s[f.Name] = true
|
|
||||||
})
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
programName := os.Args[0]
|
|
||||||
// errorLog = log.New(os.Stderr, "", log.LstdFlags)
|
|
||||||
|
|
||||||
flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
|
||||||
flags.Usage = func() {
|
|
||||||
out := flags.Output()
|
|
||||||
fmt.Fprintf(out, "Usage: %v [flags]\n\n", programName)
|
|
||||||
fmt.Fprint(out, " This utility serves vanity urls for the go get/install command.\n")
|
|
||||||
fmt.Fprint(out, " By default, the server listens on localhost:7070.\n")
|
|
||||||
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")
|
|
||||||
flags.Parse(os.Args[1:])
|
|
||||||
|
|
||||||
if len(flags.Args()) > 1 {
|
|
||||||
errorLog.Println("Error: too many command-line arguments")
|
|
||||||
flags.Usage()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
allSetFlags := flagsSet(flags)
|
|
||||||
|
|
||||||
var port string
|
|
||||||
if allSetFlags["port"] {
|
|
||||||
port = *portFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
var rulesFile string
|
|
||||||
if allSetFlags["config"] {
|
|
||||||
rulesFile = *rulesFileFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
var logFile string
|
|
||||||
if allSetFlags["logFile"] {
|
|
||||||
logFile = *logFileFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
// load rules mapping
|
|
||||||
c := &Config{}
|
|
||||||
err := c.LoadMappingFile(rulesFile)
|
|
||||||
if err != nil {
|
|
||||||
errorLog.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
l, err := newFileLogger(logFile)
|
|
||||||
if err != nil {
|
|
||||||
errorLog.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
app := &Application{
|
|
||||||
Config: c,
|
|
||||||
Log: l,
|
|
||||||
}
|
|
||||||
srv := app.Setup(port)
|
|
||||||
|
|
||||||
// For graceful shutdowns
|
|
||||||
go func() {
|
|
||||||
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.")
|
|
||||||
}()
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigChan
|
|
||||||
|
|
||||||
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer shutdownRelease()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
||||||
errorLog.Printf("HTTP shutdown error: %+v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
app.Log.logger.Println("Graceful shutdown complete.")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// serveLogger is a logging middleware for serving. It generates logs for
|
|
||||||
// requests sent to the server.
|
|
||||||
func serveLogger(l *LogFile) func(http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
l.WriteLog(r)
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func serveLogger(logger *LogFile, next http.Handler) http.Handler {
|
|
||||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// logger.WriteLog(r)
|
|
||||||
// next.ServeHTTP(w, r)
|
|
||||||
// })
|
|
||||||
// }
|
|
|
@ -1,6 +0,0 @@
|
||||||
<html>
|
|
||||||
<head></head>
|
|
||||||
<body>
|
|
||||||
<h5>Nothing here. Move along.</h5>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<meta name="go-import" content="{{.VanityUrl}} {{.Proto}} {{.RepoUrl}}">
|
|
||||||
<meta name="go-source" content="{{.VanityUrl}} {{.RepoUrl}} {{.RepoUrl}}/tree/main{/dir} {{.RepoUrl}}/blob/main{/dir}/{file}#L{line}">
|
|
||||||
</head>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue