diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..3018bff --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,36 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 5c0f819..dda6ba1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This allows for the required meta headers to be outputted in order for the GO package system to find the source files of the package. -The middleware can be configured in runtime, the server has a YAML configuration. +The outputter can be configured in runtime, the server has a YAML configuration. +The outputter can be used to add the extra meta tags to the head of the HTML document. Maintainer: [Captain ALM](https://code.mrmelon54.xyz/alfred) diff --git a/cmd/gopkghsrv/main.go b/cmd/gopkghsrv/main.go index ad4a711..4e11be1 100644 --- a/cmd/gopkghsrv/main.go +++ b/cmd/gopkghsrv/main.go @@ -1,6 +1,19 @@ package main -import "log" +import ( + "fmt" + "github.com/joho/godotenv" + "golang.captainalm.com/GOPackageHeaderServer/conf" + "golang.captainalm.com/GOPackageHeaderServer/web" + "gopkg.in/yaml.v3" + "log" + "os" + "os/signal" + "path" + "sync" + "syscall" + "time" +) var ( buildVersion = "develop" @@ -9,4 +22,84 @@ var ( func main() { log.Printf("[Main] Starting up GO Package Header Server #%s (%s)\n", buildVersion, buildDate) + y := time.Now() + + //Hold main thread till safe shutdown exit: + wg := &sync.WaitGroup{} + wg.Add(1) + + //Get working directory: + cwdDir, err := os.Getwd() + if err != nil { + log.Println(err) + } + + //Load environment file: + err = godotenv.Load() + if err != nil { + log.Fatalln("Error loading .env file") + } + + //Data directory processing: + dataDir := os.Getenv("DIR_DATA") + if dataDir == "" { + dataDir = path.Join(cwdDir, ".data") + } + + check(os.MkdirAll(dataDir, 0777)) + + //Config loading: + configFile, err := os.Open(path.Join(dataDir, "config.yml")) + if err != nil { + log.Fatalln("Failed to open config.yml") + } + + var configYml conf.ConfigYaml + groupsDecoder := yaml.NewDecoder(configFile) + err = groupsDecoder.Decode(&configYml) + if err != nil { + log.Fatalln("Failed to parse config.yml:", err) + } + + //Server definitions: + log.Printf("[Main] Starting up HTTP server on %s...\n", configYml.Listen.Web) + webServer, _ := web.New(configYml) + + //===================== + // Safe shutdown + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + //Startup complete: + z := time.Now().Sub(y) + log.Printf("[Main] Took '%s' to fully initialize modules\n", z.String()) + + go func() { + <-sigs + fmt.Printf("\n") + + log.Printf("[Main] Attempting safe shutdown\n") + a := time.Now() + + log.Printf("[Main] Shutting down HTTP server...\n") + err := webServer.Close() + if err != nil { + log.Println(err) + } + + log.Printf("[Main] Signalling program exit...\n") + b := time.Now().Sub(a) + log.Printf("[Main] Took '%s' to fully shutdown modules\n", b.String()) + wg.Done() + }() + // + //===================== + wg.Wait() + log.Println("[Main] Goodbye") +} + +func check(err error) { + if err != nil { + panic(err) + } } diff --git a/conf/config.go b/conf/config.go new file mode 100644 index 0000000..c9f133b --- /dev/null +++ b/conf/config.go @@ -0,0 +1,6 @@ +package conf + +type ConfigYaml struct { + Listen ListenYaml `yaml:"listen"` + Zones []ZoneYaml `yaml:"zones"` +} diff --git a/conf/listen.go b/conf/listen.go new file mode 100644 index 0000000..07eb258 --- /dev/null +++ b/conf/listen.go @@ -0,0 +1,26 @@ +package conf + +import "time" + +type ListenYaml struct { + Web string `yaml:"web"` + ReadTimeout time.Duration `yaml:"readTimeout"` + WriteTimeout time.Duration `yaml:"writeTimeout"` + Identify bool `yaml:"identify"` +} + +func (ly ListenYaml) GetReadTimeout() time.Duration { + if ly.ReadTimeout.Seconds() < 1 { + return 1 * time.Second + } else { + return ly.ReadTimeout + } +} + +func (ly ListenYaml) GetWriteTimeout() time.Duration { + if ly.WriteTimeout.Seconds() < 1 { + return 1 * time.Second + } else { + return ly.WriteTimeout + } +} diff --git a/conf/zone.go b/conf/zone.go new file mode 100644 index 0000000..381d759 --- /dev/null +++ b/conf/zone.go @@ -0,0 +1,29 @@ +package conf + +import "golang.captainalm.com/GOPackageHeaderServer/outputMeta" + +type ZoneYaml struct { + Name string `yaml:"name"` + Domains []string `yaml:"domains"` + HavePageContents bool `yaml:"havePageContents"` + BasePath string `yaml:"basePath"` + UsernameProvided bool `yaml:"usernameProvided"` + Username string `yaml:"username"` + BasePrefixURL string `yaml:"basePrefixURL"` + SuffixDirectoryURL string `yaml:"suffixDirectoryURL"` + SuffixFileURL string `yaml:"suffixFileURL"` +} + +func (zy ZoneYaml) GetPackageMetaTagOutputter() *outputMeta.PackageMetaTagOutputter { + var theUsername string + if !zy.UsernameProvided { + theUsername = zy.Username + } + return &outputMeta.PackageMetaTagOutputter{ + BasePath: zy.BasePath, + Username: theUsername, + BasePrefixURL: zy.BasePrefixURL, + SuffixDirectoryURL: zy.SuffixDirectoryURL, + SuffixFileURL: zy.SuffixFileURL, + } +} diff --git a/go.mod b/go.mod index dc1c1f0..629cb13 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ -module GOPackageHeaderServer +module golang.captainalm.com/GOPackageHeaderServer go 1.18 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/joho/godotenv v1.4.0 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..27d4d9a --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/outputMeta/packagemetatagoutputter.go b/outputMeta/packagemetatagoutputter.go new file mode 100644 index 0000000..a1f4c09 --- /dev/null +++ b/outputMeta/packagemetatagoutputter.go @@ -0,0 +1,77 @@ +package outputMeta + +import ( + "path" + "strings" +) + +type PackageMetaTagOutputter struct { + BasePath string + Username string //If set, the outputter will do /{repo}/ for repos rather than /{user}/{repo}/ + BasePrefixURL string + SuffixDirectoryURL string + SuffixFileURL string +} + +func (pkgMTO *PackageMetaTagOutputter) GetMetaTags(pathIn string) string { + return "\r\n" + + "" +} + +func (pkgMTO *PackageMetaTagOutputter) assureBasePrefixURL() (failed bool) { + if pkgMTO.BasePrefixURL == "" { + if pkgMTO.BasePath == "" { + return true + } + pkgMTO.BasePrefixURL = "http://" + pkgMTO.BasePath + } + return false +} + +func (pkgMTO *PackageMetaTagOutputter) getPrefix(pathIn string) string { + if pkgMTO.BasePath == "" { + return "_" + } + if pkgMTO.Username == "" { + return path.Join(pkgMTO.BasePath, pathIn) + } else { + return path.Join(pkgMTO.BasePath, pkgMTO.Username, pathIn) + } +} + +func (pkgMTO *PackageMetaTagOutputter) getHomeURL(pathIn string) string { + if pkgMTO.assureBasePrefixURL() { + return "_" + } + + if pkgMTO.Username == "" { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Clean(pathIn), "/") + } else { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn), "/") + } +} + +func (pkgMTO *PackageMetaTagOutputter) getDirectoryURL(pathIn string) string { + if pkgMTO.assureBasePrefixURL() || pkgMTO.SuffixDirectoryURL == "" { + return "_" + } + + if pkgMTO.Username == "" { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixDirectoryURL), "/") + } else { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixDirectoryURL), "/") + } +} + +func (pkgMTO *PackageMetaTagOutputter) getFileURL(pathIn string) string { + if pkgMTO.assureBasePrefixURL() || pkgMTO.SuffixFileURL == "" { + return "_" + } + + if pkgMTO.Username == "" { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pathIn, pkgMTO.SuffixFileURL), "/") + } else { + return pkgMTO.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pkgMTO.Username, pathIn, pkgMTO.SuffixFileURL), "/") + } +} diff --git a/web/pagehandler.go b/web/pagehandler.go new file mode 100644 index 0000000..a8dfb3b --- /dev/null +++ b/web/pagehandler.go @@ -0,0 +1,50 @@ +package web + +import ( + "golang.captainalm.com/GOPackageHeaderServer/outputMeta" + "net/http" + "path" + "strconv" + "strings" +) + +type PageHandler struct { + Name string + OutputPage bool + MetaOutput *outputMeta.PackageMetaTagOutputter +} + +func (pgh *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + if request.Method == http.MethodGet || request.Method == http.MethodHead { + thePage := "\r\n\r\n\r\n" + if pgh.OutputPage && pgh.Name != "" { + thePage += "Go Package: " + pgh.Name + "\r\n" + } + thePage += pgh.MetaOutput.GetMetaTags(request.URL.Path) + "\r\n\r\n\r\n" + if pgh.OutputPage { + if pgh.Name != "" { + thePage += "

Go Package: " + pgh.Name + "

\r\n" + } + var theLink string + if pgh.MetaOutput.Username == "" { + theLink = pgh.MetaOutput.BasePrefixURL + "/" + strings.TrimLeft(path.Clean(request.URL.Path), "/") + } else { + theLink = pgh.MetaOutput.BasePrefixURL + "/" + strings.TrimLeft(path.Join(pgh.MetaOutput.Username, request.URL.Path), "/") + } + thePage += "" + theLink + "\r\n" + } + thePage += "\r\n\r\n" + writer.Header().Set("Content-Length", strconv.Itoa(len([]byte(thePage)))) + writer.Header().Set("Content-Type", "text/html; charset=utf-8") + if writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "") { + _, _ = writer.Write([]byte(thePage)) + } + } else { + writer.Header().Set("Allow", http.MethodOptions+", "+http.MethodGet+", "+http.MethodHead) + if request.Method == http.MethodOptions { + writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "") + } else { + writeResponseHeaderCanWriteBody(request.Method, writer, http.StatusMethodNotAllowed, "") + } + } +} diff --git a/web/utils.go b/web/utils.go new file mode 100644 index 0000000..382bb95 --- /dev/null +++ b/web/utils.go @@ -0,0 +1,24 @@ +package web + +import ( + "net/http" + "strconv" +) + +func writeResponseHeaderCanWriteBody(method string, rw http.ResponseWriter, statusCode int, message string) bool { + hasBody := method != http.MethodHead && method != http.MethodOptions + if hasBody && message != "" { + rw.Header().Set("Content-Type", "text/plain; charset=utf-8") + rw.Header().Set("X-Content-Type-Options", "nosniff") + rw.Header().Set("Content-Length", strconv.Itoa(len(message)+2)) + } + rw.WriteHeader(statusCode) + if hasBody { + if message != "" { + _, _ = rw.Write([]byte(message + "\r\n")) + return false + } + return true + } + return false +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..26cecbd --- /dev/null +++ b/web/web.go @@ -0,0 +1,62 @@ +package web + +import ( + "github.com/gorilla/mux" + "golang.captainalm.com/GOPackageHeaderServer/conf" + "log" + "net/http" + "strings" +) + +func New(yaml conf.ConfigYaml) (*http.Server, map[string]*PageHandler) { + router := mux.NewRouter() + var pages = make(map[string]*PageHandler) + for _, zc := range yaml.Zones { + currentPage := &PageHandler{ + Name: zc.Name, + OutputPage: zc.HavePageContents, + MetaOutput: zc.GetPackageMetaTagOutputter(), + } + for _, d := range zc.Domains { + ld := strings.ToLower(d) + if _, exists := pages[ld]; !exists { + pages[ld] = currentPage + router.Host(ld).HandlerFunc(currentPage.ServeHTTP) + } + } + } + if yaml.Listen.Identify { + router.Use(headerMiddleware) + } + if yaml.Listen.Web == "" { + log.Fatalf("[Http] Invalid Listening Address") + } + s := &http.Server{ + Addr: yaml.Listen.Web, + Handler: router, + ReadTimeout: yaml.Listen.GetReadTimeout(), + WriteTimeout: yaml.Listen.GetWriteTimeout(), + } + go runBackgroundHttp(s) + return s, pages +} + +func runBackgroundHttp(s *http.Server) { + err := s.ListenAndServe() + if err != nil { + if err == http.ErrServerClosed { + log.Println("The http server shutdown successfully") + } else { + log.Fatalf("[Http] Error trying to host the http server: %s\n", err.Error()) + } + } +} + +func headerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", "Clerie Gilbert") + w.Header().Set("X-Powered-By", "Love") + w.Header().Set("X-Friendly", "True") + next.ServeHTTP(w, r) + }) +}