diff options
author | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2025-08-02 22:23:13 +0530 |
---|---|---|
committer | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2025-08-02 23:02:30 +0530 |
commit | 178e0bfc2d8709ae4d3abd7519b260df09b2d3b5 (patch) | |
tree | 10cbdd013c62586190074d9f088e2269f88c4cab | |
parent | Fixed Broken .gitignore (diff) | |
download | gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar.gz gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar.bz2 gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar.lz gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar.xz gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.tar.zst gopkgserver-178e0bfc2d8709ae4d3abd7519b260df09b2d3b5.zip |
Diffstat (limited to '')
-rw-r--r-- | cmd/gopkgserver/main.go | 26 | ||||
-rw-r--r-- | go.mod | 7 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | logging/handler/handler.go | 75 | ||||
-rw-r--r-- | logging/log.go | 56 | ||||
-rw-r--r-- | main.go | 87 | ||||
-rw-r--r-- | repo/repo.go | 7 | ||||
-rw-r--r-- | repo/repo_test.go | 137 | ||||
-rw-r--r-- | robot.go | 17 |
9 files changed, 189 insertions, 227 deletions
diff --git a/cmd/gopkgserver/main.go b/cmd/gopkgserver/main.go index 9e40b72..7248136 100644 --- a/cmd/gopkgserver/main.go +++ b/cmd/gopkgserver/main.go @@ -1,30 +1,10 @@ package main import ( - "context" - "flag" - "go-pkg-server/logging" - "log/slog" - "os" - "os/signal" - - gopkgserver "go-pkg-server" + "go.sudomsg.com/gopkgserver" + "go.sudomsg.com/kit/runner" ) func main() { - ctx := context.Background() - defer func() { - err := recover() - if err != nil { - logging.RecoverLog(ctx, err) - os.Exit(2) - } - }() - - ctx, stop := signal.NotifyContext(ctx, os.Interrupt) - defer stop() - - if err := gopkgserver.Run(ctx, flag.CommandLine, os.Args[1:]); err != nil { - slog.Log(ctx, slog.LevelError, "Program returned an error", "error", err) - } + runner.Load(gopkgserver.Run) } @@ -2,4 +2,9 @@ module go.sudomsg.com/gopkgserver go 1.24.1 -require github.com/pelletier/go-toml/v2 v2.2.4 +require ( + github.com/pelletier/go-toml/v2 v2.2.4 + go.sudomsg.com/kit v0.1.1 +) + +require golang.org/x/sync v0.16.0 // indirect @@ -1,2 +1,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +go.sudomsg.com/kit v0.1.1 h1:mrwLCE+1jnjaA5aj0AE6cZ9YdRgg/GDsR/OX/5faJ14= +go.sudomsg.com/kit v0.1.1/go.mod h1:rFO8lXUV2Ij1deDS0OrfdZ4xpH5akf0jHtVwC0N8fBQ= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= diff --git a/logging/handler/handler.go b/logging/handler/handler.go deleted file mode 100644 index 1501715..0000000 --- a/logging/handler/handler.go +++ /dev/null @@ -1,75 +0,0 @@ -package handler - -import ( - "go-pkg-server/logging" - "log/slog" - "net/http" - "time" -) - -type traceWriter struct { - http.ResponseWriter - status int - bytes int -} - -var _ http.ResponseWriter = &traceWriter{} - -func (t *traceWriter) Write(b []byte) (int, error) { - if t.status == 0 { - t.WriteHeader(http.StatusOK) - } - c, err := t.ResponseWriter.Write(b) - t.bytes += c - return c, err -} - -func (tw *traceWriter) WriteHeader(statusCode int) { - if tw.status == 0 { - tw.status = http.StatusOK - } - tw.status = statusCode - tw.ResponseWriter.WriteHeader(statusCode) -} - -func New(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - logger := logging.FromContext(ctx) - - defer func() { - err := recover() - if err != nil { - logging.RecoverLog(ctx, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }() - - logger = logger.With("method", r.Method, - "host", r.Host, - "remote_address", r.RemoteAddr, - "path", r.RequestURI, - ) - - ctx = logging.WithLogger(ctx, logger) - - r = r.WithContext(ctx) - - tw := &traceWriter{ResponseWriter: w} - - start := time.Now() - next.ServeHTTP(tw, r) - duration := time.Since(start) - - logger.Log(ctx, slog.LevelInfo, "Request Handled", - "protocol", r.Proto, - "bytes_recieved", r.ContentLength, - "status", tw.status, - "time", duration, - "bytes_sent", tw.bytes, - "user_agent", r.UserAgent(), - "referer", r.Referer(), - ) - - } -} diff --git a/logging/log.go b/logging/log.go deleted file mode 100644 index 8e3078f..0000000 --- a/logging/log.go +++ /dev/null @@ -1,56 +0,0 @@ -package logging - -import ( - "context" - "io" - "log/slog" - "os" - "runtime" -) - -type Config struct { - Level slog.Level `toml:"level"` - File string `toml:"file"` -} - -func SetupLogger(cfg Config) *slog.Logger { - opts := &slog.HandlerOptions{Level: cfg.Level} - var handler slog.Handler - - var writter io.Writer - writter = os.Stdout - if cfg.File != "" { - f, _ := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - writter = f - } - handler = slog.NewJSONHandler(writter, opts) - - logger := slog.New(handler) - slog.SetDefault(logger) - return logger -} - -type contextKeyType struct{} - -var contextKey contextKeyType - -func WithLogger(ctx context.Context, logger *slog.Logger) context.Context { - return context.WithValue(ctx, contextKey, logger) -} - -func FromContext(ctx context.Context) *slog.Logger { - if logger, ok := ctx.Value(contextKey).(*slog.Logger); ok { - return logger - } - return slog.Default() -} - -func RecoverLog(ctx context.Context, err any) { - logger := FromContext(ctx) - - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - - logger.Log(ctx, slog.LevelError, "Program Paniced", "error", err, "stack", string(buf)) -} @@ -3,31 +3,24 @@ package gopkgserver import ( "context" _ "embed" - "errors" "flag" "fmt" - "io" + "log/slog" - "net" "net/http" "os" "github.com/pelletier/go-toml/v2" - - "go-pkg-server/logging" - "go-pkg-server/logging/handler" - "go-pkg-server/repo" + "go.sudomsg.com/gopkgserver/repo" + httpServer "go.sudomsg.com/kit/http" + "go.sudomsg.com/kit/logging" + logHandler "go.sudomsg.com/kit/logging/http" ) -type ServerConfig struct { - Proto string `toml:"proto"` - Address string `toml:"address"` -} - type Config struct { - Server ServerConfig `toml:"server"` - Log logging.Config `toml:"log"` - Repos map[string]repo.Repo `toml:"repo"` + Server []httpServer.ServerConfig `toml:"server"` + Logging logging.LogConfig `toml:"logging"` + Repos map[string]repo.Repo `toml:"repo"` } func Run(ctx context.Context, fs *flag.FlagSet, args []string) error { @@ -40,8 +33,10 @@ func Run(ctx context.Context, fs *flag.FlagSet, args []string) error { return err } - logger := logging.SetupLogger(cfg.Log) - ctx = logging.WithLogger(ctx, logger) + if err := logging.Setup(cfg.Logging); err != nil { + return err + } + ctx = logging.WithLogger(ctx, slog.Default()) h, err := repo.New(cfg.Repos) if err != nil { @@ -52,7 +47,12 @@ func Run(ctx context.Context, fs *flag.FlagSet, args []string) error { mux.Handle("GET /robots.txt", Robot()) mux.Handle("GET /", h) - return RunServer(ctx, handler.New(mux), cfg.Server) + handler := logHandler.New(mux, nil) + + lns, err := httpServer.OpenListeners(ctx, cfg.Server) + defer lns.CloseAll() + + return httpServer.RunHTTPServers(ctx, lns, handler) } func LoadConfig(cfgFile string) (Config, error) { @@ -66,56 +66,5 @@ func LoadConfig(cfgFile string) (Config, error) { return Config{}, fmt.Errorf("parse config: %w", err) } - if cfg.Server.Proto == "" { - cfg.Server.Proto = "tcp" - } - return cfg, nil } - -func RunServer(ctx context.Context, mux http.Handler, config ServerConfig) error { - logger := logging.FromContext(ctx) - - srv := &http.Server{ - Handler: mux, - BaseContext: func(l net.Listener) context.Context { - logger := logger.With("address", l.Addr()) - ctx = logging.WithLogger(ctx, logger) - return ctx - }, - ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), - } - - ln, err := net.Listen(config.Proto, config.Address) - if err != nil { - return err - } - - logger.Log(ctx, slog.LevelError, "Server Started on", "address", ln.Addr()) - - go func() { - <-ctx.Done() - shutdownCtx := context.Background() - // We received an interrupt signal, shut down. - if err := srv.Shutdown(shutdownCtx); err != nil { - slog.Log(shutdownCtx, slog.LevelError, "HTTP server Shutdown", "error", err) - } - }() - - if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { - return err - } - - return nil -} - -func Robot() http.HandlerFunc { - robots := `User-agent: * -Disallow: / -` - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "public, max-age=86400, immutable") - w.Header().Set("Content-Type", "text/plain") - io.WriteString(w, robots) - } -} diff --git a/repo/repo.go b/repo/repo.go index 2c052d6..b3eeef0 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -6,7 +6,7 @@ import ( "fmt" "html/template" "net/http" - "strings" + "path" ) //go:embed meta.html @@ -45,8 +45,9 @@ func New(repo map[string]Repo) (*RepoHandler, error) { } func (h *RepoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - key := fmt.Sprintf("%s%s", r.Host, r.URL.Path) - key = strings.TrimSuffix(key, "/") + host := r.Host + path := path.Clean(r.URL.Path) + key := host + path page, ok := h.Pages[key] if !ok { diff --git a/repo/repo_test.go b/repo/repo_test.go new file mode 100644 index 0000000..57f249c --- /dev/null +++ b/repo/repo_test.go @@ -0,0 +1,137 @@ +package repo_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "go.sudomsg.com/gopkgserver/repo" +) + +func TestMetaExec(t *testing.T) { + t.Parallel() + + meta, err := repo.NewMeta() + if err != nil { + t.Fatalf("failed to create Meta: %v", err) + } + + pkg := "example.com/foo" + r := repo.Repo{ + VCS: "git", + Repository: "https://github.com/user/foo", + Home: "https://github.com/user/foo", + Directory: "https://github.com/user/foo/tree/master{/dir}", + File: "https://github.com/user/foo/blob/master{/dir}/{file}#L{line}", + } + + html, err := meta.Exec(pkg, r) + if err != nil { + t.Fatalf("Exec failed: %v", err) + } + + htmlStr := string(html) + if !strings.Contains(htmlStr, `<meta name="go-import"`) { + t.Fatalf("missing go-import meta tag: %q", htmlStr) + } + if !strings.Contains(htmlStr, `<meta name="go-source"`) { + t.Fatalf("missing go-source meta tag: %q", htmlStr) + } + if !strings.Contains(htmlStr, pkg) { + t.Fatalf("missing package name in output: %q", htmlStr) + } +} + +func TestServeHTTP(t *testing.T) { + t.Parallel() + + repos := map[string]repo.Repo{ + "example.com/foo": { + VCS: "git", + Repository: "https://github.com/user/foo", + Home: "https://github.com/user/foo", + Directory: "https://github.com/user/foo/tree/master{/dir}", + File: "https://github.com/user/foo/blob/master{/dir}/{file}#L{line}", + }, + } + + t.Run("returns meta tag for go-get=1", func(t *testing.T) { + t.Parallel() + + handler, err := repo.New(repos) + if err != nil { + t.Fatalf("failed to create handler: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "http://example.com/foo?go-get=1", nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode) + } + + var buf bytes.Buffer + buf.ReadFrom(resp.Body) + body := buf.String() + + if !strings.Contains(body, `<meta name="go-import"`) { + t.Fatalf("expected go-import tag in body, got: %v", body) + } + }) + + t.Run("redirects to pkg.go.dev otherwise", func(t *testing.T) { + t.Parallel() + + handler, err := repo.New(repos) + if err != nil { + t.Fatalf("failed to create handler: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "http://example.com/foo", nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusFound { + t.Fatalf("expected %v, got %v", http.StatusFound, resp.StatusCode) + } + loc, err := resp.Location() + if err != nil { + t.Fatalf("Invalid redirect location: %v", err) + } + + if !strings.HasPrefix(loc.String(), "https://pkg.go.dev/example.com/foo") { + t.Fatalf("unexpected redirect location: %v", loc) + } + }) + + t.Run("returns 404 for unknown path", func(t *testing.T) { + t.Parallel() + + handler, err := repo.New(repos) + if err != nil { + t.Fatalf("failed to create handler: %v", err) + } + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "http://example.com/bar", nil) + handler.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected %v, got %v", http.StatusNotFound, resp.StatusCode) + } + }) +} diff --git a/robot.go b/robot.go new file mode 100644 index 0000000..7426eda --- /dev/null +++ b/robot.go @@ -0,0 +1,17 @@ +package gopkgserver + +import ( + "io" + "net/http" +) + +func Robot() http.HandlerFunc { + robots := `User-agent: * +Disallow: / +` + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=86400, immutable") + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, robots) + } +} |