aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--archlinux/PKGBUILD1
-rw-r--r--archlinux/go-import-redirect.conf5
-rw-r--r--main.go19
-rw-r--r--main_aws.go27
-rw-r--r--pkg/redirector/redirector.go75
-rw-r--r--pkg/redirector/redirector_test.go130
-rw-r--r--pkg/redirector/template.tpl (renamed from resp.html)0
-rw-r--r--resp.go54
-rw-r--r--resp_test.go70
10 files changed, 255 insertions, 134 deletions
diff --git a/README.md b/README.md
index 5412c96..61b90fd 100644
--- a/README.md
+++ b/README.md
@@ -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"
+}]
diff --git a/main.go b/main.go
index aa2ce53..dd7e262 100644
--- a/main.go
+++ b/main.go
@@ -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)
- }
- })
-}