diff options
| author | Grégoire Duchêne <gduchene@awhk.org> | 2022-06-19 13:31:49 +0100 |
|---|---|---|
| committer | Grégoire Duchêne <gduchene@awhk.org> | 2022-06-19 13:40:39 +0100 |
| commit | 41d23c22df853b0bdf35e2d0988c8d4c4281d42f (patch) | |
| tree | 48ddc49039d6ceded97914374171157515b004be | |
| parent | 21456154ef3172490cef72a3b69bf59bb9795e43 (diff) | |
Move redirection logic to a separate packagev0.4.0
Also, add an optional ‘-c’ flag to pass the path to a configuration file
that can be used to specify several matching patterns and replacements.
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | archlinux/PKGBUILD | 1 | ||||
| -rw-r--r-- | archlinux/go-import-redirect.conf | 5 | ||||
| -rw-r--r-- | main.go | 19 | ||||
| -rw-r--r-- | main_aws.go | 27 | ||||
| -rw-r--r-- | pkg/redirector/redirector.go | 75 | ||||
| -rw-r--r-- | pkg/redirector/redirector_test.go | 130 | ||||
| -rw-r--r-- | pkg/redirector/template.tpl (renamed from resp.html) | 0 | ||||
| -rw-r--r-- | resp.go | 54 | ||||
| -rw-r--r-- | resp_test.go | 70 |
10 files changed, 255 insertions, 134 deletions
@@ -11,12 +11,16 @@ to https://godoc.org. It can either be a normal `IP:PORT` address or an absolute path to a UNIX socket that will be created. Defaults to `localhost:8080`. See https://golang.org/pkg/net/#Dial for more details. -* `-from` for the prefix that must be removed from your package name, - e.g. `golang.org/x/` for `golang.org/x/image`. +* `-from` for the regular expression that the import path must match, + including any capturing group, e.g. `go\\.example\\.com/(.+)`. * `-to` for the URL that will be used to build the repository URL. + Capturing groups can be used, e.g. `https://git.example.com/$1`. * `-vcs` for the type of VCS you are using, e.g. `git`. Defaults to `git`. +Additionally, a configuration file can be passed with `-c`. See +`archlinux/go-import-redirect.conf` for an example. + It is recommended to enable the companion systemd socket and customize it so systemd can start the service when needed and pass the socket to `go-import-redirect`. diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index fc5bb65..096d272 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -24,5 +24,6 @@ package() { install -Dm644 systemd/${pkgname}.service ${pkgdir}/usr/lib/systemd/system/${pkgname}.service install -Dm644 systemd/${pkgname}.socket ${pkgdir}/usr/lib/systemd/system/${pkgname}.socket install -Dm644 README.md ${pkgdir}/usr/share/doc/${pkgname}/README.md + install -Dm644 archlinux/${pkgname}.conf ${pkgdir}/usr/share/doc/${pkgname}/examples/${pkgname}.conf install -Dm644 LICENSE ${pkgdir}/usr/share/licenses/${pkgname}/LICENSE } diff --git a/archlinux/go-import-redirect.conf b/archlinux/go-import-redirect.conf new file mode 100644 index 0000000..6bb195a --- /dev/null +++ b/archlinux/go-import-redirect.conf @@ -0,0 +1,5 @@ +[{ + "pattern": "go\\.example\\.com/(.+)", + "replacement": "https://git.example.com/$1", + "vcs": "git" +}] @@ -7,20 +7,24 @@ package main import ( "context" + "encoding/json" "flag" "log" "net/http" "os" "os/signal" "regexp" + "strings" "syscall" "time" "go.awhk.org/core" + "go.awhk.org/go-import-redirect/pkg/redirector" ) var ( addr = flag.String("addr", "localhost:8080", "address to listen on") + cfg = flag.String("c", "", "path to a configuration file") from = flag.String("from", "", "package prefix to remove") to = flag.String("to", "", "repository prefix to add") vcs = flag.String("vcs", "git", "version control system to signal") @@ -32,7 +36,20 @@ func main() { done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGTERM) - srv := http.Server{Handler: &redirector{regexp.MustCompile(*from), *to, *vcs}} + h := &redirector.Redirector{Template: redirector.DefaultTemplate} + if *cfg != "" { + if err := json.NewDecoder(core.Must(os.Open(*cfg))).Decode(&h.Transformers); err != nil { + log.Fatalln(err) + } + } else { + h.Transformers = append(h.Transformers, redirector.Transformer{ + Pattern: &redirector.Pattern{regexp.MustCompile(strings.ReplaceAll(*from, `\\`, `\`))}, + Replacement: *to, + VCS: *vcs, + }) + } + + srv := http.Server{Handler: h} go func() { if err := srv.Serve(core.Must(core.Listen(*addr))); err != nil && err != http.ErrServerClosed { log.Fatalln("server.Serve:", err) diff --git a/main_aws.go b/main_aws.go index 4f6f16e..76cd5b9 100644 --- a/main_aws.go +++ b/main_aws.go @@ -10,29 +10,42 @@ import ( "net/http" "os" "path" + "regexp" "strings" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" -) -var ( - from = os.Getenv("FROM") - to = os.Getenv("TO") - vcs = os.Getenv("VCS") - redir = &redirector{from, to, vcs} + "go.awhk.org/go-import-redirect/pkg/redirector" ) +var transf = redirector.Transformer{ + Pattern: &redirector.Pattern{regexp.MustCompile(strings.ReplaceAll(os.Getenv("FROM"), `\\`, `\`))}, + Replacement: os.Getenv("TO"), + VCS: os.Getenv("VCS"), +} + func redirect(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { pkg := path.Join(req.Headers["Host"], req.Path) + + if !transf.Pattern.MatchString(pkg) { + return events.APIGatewayProxyResponse{StatusCode: http.StatusNotFound}, nil + } + if v, ok := req.QueryStringParameters["go-get"]; !ok || v != "1" { return events.APIGatewayProxyResponse{ Headers: map[string]string{"Location": "https://pkg.go.dev/" + pkg}, StatusCode: http.StatusFound, }, nil } + + data := redirector.TemplateData{ + Package: pkg, + Repository: transf.Pattern.ReplaceAllString(pkg, transf.Replacement), + VCS: transf.VCS, + } var buf strings.Builder - if err := body.Execute(&buf, bodyData{pkg, redir.getRepo(pkg), vcs}); err != nil { + if err := redirector.DefaultTemplate.Execute(&buf, data); err != nil { return events.APIGatewayProxyResponse{}, err } return events.APIGatewayProxyResponse{ diff --git a/pkg/redirector/redirector.go b/pkg/redirector/redirector.go new file mode 100644 index 0000000..96becb2 --- /dev/null +++ b/pkg/redirector/redirector.go @@ -0,0 +1,75 @@ +package redirector + +import ( + "embed" + "encoding/json" + "log" + "net/http" + "path" + "regexp" + "strings" + "text/template" + + "go.awhk.org/core" +) + +var DefaultTemplate = template.Must(template.ParseFS(fs, "*.tpl")) + +var ( + filter = core.FilterHTTPMethod(http.MethodGet) + + //go:embed *.tpl + fs embed.FS +) + +type Redirector struct { + Template *template.Template + Transformers []Transformer +} + +func (h *Redirector) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if filter(w, req) { + return + } + + pkg := path.Join(req.Host, req.URL.Path) + for _, t := range h.Transformers { + if !t.Pattern.MatchString(pkg) { + continue + } + + if req.URL.Query().Get("go-get") != "1" { + w.Header().Set("Location", "https://pkg.go.dev/"+pkg) + w.WriteHeader(http.StatusFound) + return + } + + repo := t.Pattern.ReplaceAllString(pkg, t.Replacement) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + if err := h.Template.Execute(w, TemplateData{pkg, repo, t.VCS}); err != nil { + log.Println("Failed to execute template:", err) + } + return + } + w.WriteHeader(http.StatusNotFound) +} + +type Pattern struct{ *regexp.Regexp } + +func (pp *Pattern) UnmarshalJSON(data []byte) (err error) { + var s string + if err = json.Unmarshal(data, &s); err != nil { + return + } + pp.Regexp, err = regexp.Compile(strings.ReplaceAll(s, `\\`, `\`)) + return +} + +type TemplateData struct{ Package, Repository, VCS string } + +type Transformer struct { + Pattern *Pattern `json:"pattern"` + Replacement string `json:"replacement"` + VCS string `json:"vcs"` +} diff --git a/pkg/redirector/redirector_test.go b/pkg/redirector/redirector_test.go new file mode 100644 index 0000000..8761185 --- /dev/null +++ b/pkg/redirector/redirector_test.go @@ -0,0 +1,130 @@ +package redirector_test + +import ( + "io" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "go.awhk.org/core" + "go.awhk.org/go-import-redirect/pkg/redirector" +) + +func TestRedirector_ServeHTTP(s *testing.T) { + t := core.T{T: s} + + meta := regexp.MustCompile(`<meta name="go-import" content="(.+?)">`) + redir := &redirector.Redirector{ + Template: redirector.DefaultTemplate, + Transformers: []redirector.Transformer{ + { + Pattern: &redirector.Pattern{regexp.MustCompile("go.example.com/x/baz(?:/.+)?")}, + Replacement: "https://git.example.net/elsewhere/baz", + VCS: "git", + }, + { + Pattern: &redirector.Pattern{regexp.MustCompile("go.example.com/x/([^/]+)(?:/.+)?")}, + Replacement: "https://git.example.net/y/$1", + VCS: "git", + }, + { + Pattern: &redirector.Pattern{regexp.MustCompile("go.example.com/x/qux(?:/.+)?")}, + Replacement: "https://git.example.net/elsewhere/qux", + VCS: "git", + }, + }, + } + + for _, tc := range []struct { + name string + method string + url string + + expGetStatusCode int + expGetGoImport string + expVisitStatusCode int + expVisitLocation string + }{ + { + name: "Match", + method: http.MethodGet, + url: "https://go.example.com/x/foo", + + expGetStatusCode: http.StatusOK, + expGetGoImport: "go.example.com/x/foo git https://git.example.net/y/foo", + expVisitStatusCode: http.StatusFound, + expVisitLocation: "https://pkg.go.dev/go.example.com/x/foo", + }, + { + name: "MatchDirectory", + method: http.MethodGet, + url: "https://go.example.com/x/foo/bar", + + expGetStatusCode: http.StatusOK, + expGetGoImport: "go.example.com/x/foo/bar git https://git.example.net/y/foo", + expVisitStatusCode: http.StatusFound, + expVisitLocation: "https://pkg.go.dev/go.example.com/x/foo/bar", + }, + { + name: "MatchIgnored", + method: http.MethodGet, + url: "https://go.example.com/x/qux", + + expGetStatusCode: http.StatusOK, + expGetGoImport: "go.example.com/x/qux git https://git.example.net/y/qux", + expVisitStatusCode: http.StatusFound, + expVisitLocation: "https://pkg.go.dev/go.example.com/x/qux", + }, + { + name: "MatchSpecific", + method: http.MethodGet, + url: "https://go.example.com/x/baz", + + expGetStatusCode: http.StatusOK, + expGetGoImport: "go.example.com/x/baz git https://git.example.net/elsewhere/baz", + expVisitStatusCode: http.StatusFound, + expVisitLocation: "https://pkg.go.dev/go.example.com/x/baz", + }, + { + name: "BadMethod", + method: http.MethodPost, + url: "https://go.example.com/x/baz", + + expGetStatusCode: http.StatusMethodNotAllowed, + expVisitStatusCode: http.StatusMethodNotAllowed, + }, + } { + t.Run("Get"+tc.name, func(t *core.T) { + var ( + req = httptest.NewRequest(tc.method, tc.url+"?go-get=1", nil) + w = httptest.NewRecorder() + ) + redir.ServeHTTP(w, req) + + resp := w.Result() + t.AssertEqual(tc.expGetStatusCode, resp.StatusCode) + t.AssertEqual("", resp.Header.Get("Location")) + + match := meta.FindSubmatch(core.Must(io.ReadAll(resp.Body))) + if tc.expGetGoImport == "" { + t.AssertEqual(0, len(match)) + return + } + if t.AssertEqual(2, len(match)) { + t.AssertEqual(tc.expGetGoImport, string(match[1])) + } + }) + t.Run("Visit"+tc.name, func(t *core.T) { + var ( + req = httptest.NewRequest(tc.method, tc.url, nil) + w = httptest.NewRecorder() + ) + redir.ServeHTTP(w, req) + + resp := w.Result() + t.AssertEqual(tc.expVisitStatusCode, resp.StatusCode) + t.AssertEqual(tc.expVisitLocation, resp.Header.Get("Location")) + }) + } +} diff --git a/resp.html b/pkg/redirector/template.tpl index 422024a..422024a 100644 --- a/resp.html +++ b/pkg/redirector/template.tpl diff --git a/resp.go b/resp.go deleted file mode 100644 index deae50a..0000000 --- a/resp.go +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: © 2019 Grégoire Duchêne <gduchene@awhk.org> -// SPDX-License-Identifier: ISC - -package main - -import ( - _ "embed" - "log" - "net/http" - "path" - "regexp" - "text/template" -) - -var ( - body = template.Must(template.New("").Parse(tmpl)) - - //go:embed resp.html - tmpl string -) - -type bodyData struct{ Package, Repository, VCS string } - -type redirector struct { - re *regexp.Regexp - repl string - vcs string -} - -func (h *redirector) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - w.Header().Set("Allow", http.MethodGet) - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - pkg := path.Join(req.Host, req.URL.Path) - if !h.re.MatchString(pkg) { - w.WriteHeader(http.StatusNotFound) - return - } - if req.URL.Query().Get("go-get") != "1" { - w.Header().Set("Location", "https://pkg.go.dev/"+pkg) - w.WriteHeader(http.StatusFound) - return - } - - dest := h.re.ReplaceAllString(pkg, h.repl) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(http.StatusOK) - if err := body.Execute(w, bodyData{pkg, dest, h.vcs}); err != nil { - log.Println(err) - } -} diff --git a/resp_test.go b/resp_test.go deleted file mode 100644 index 7e01946..0000000 --- a/resp_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: © 2019 Grégoire Duchêne <gduchene@awhk.org> -// SPDX-License-Identifier: ISC - -package main - -import ( - "io" - "net/http" - "net/http/httptest" - "regexp" - "testing" -) - -func TestRedirector_ServeHTTP(t *testing.T) { - r := &redirector{regexp.MustCompile(`src\.example\.com/x`), "https://example.com/git", "git"} - - t.Run("NotFound", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "https://example.com/foo", nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - resp := w.Result() - if http.StatusNotFound != resp.StatusCode { - t.Errorf("expected %d, got %d", http.StatusNotFound, resp.StatusCode) - } - }) - - t.Run("GoVisit", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "https://src.example.com/x/foo?go-get=1", nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - resp := w.Result() - if http.StatusOK != resp.StatusCode { - t.Errorf("expected %d, got %d", http.StatusFound, resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Error(err) - t.FailNow() - } - expected := `<!doctype html> -<meta name="go-import" content="src.example.com/x/foo git https://example.com/git/foo"> -<title>go-import-redirect</title> -` - if string(body) != expected { - t.Errorf("expected\n---\n%s\n---\ngot\n---\n%s\n---", expected, string(body)) - } - if hdr := resp.Header.Get("Location"); hdr != "" { - t.Error("expected empty Location header") - } - }) - - t.Run("UserVisit", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "https://src.example.com/x/foo", nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - resp := w.Result() - if http.StatusFound != resp.StatusCode { - t.Errorf("expected %d, got %d", http.StatusFound, resp.StatusCode) - } - if resp.ContentLength > 0 { - t.Error("expected empty body") - } - if hdr := resp.Header.Get("Location"); hdr != "https://pkg.go.dev/src.example.com/x/foo" { - t.Errorf("expected %q, got %q", "https://pkg.go.dev/src.example.com/x/foo", hdr) - } - }) -} |
