Skip to content

Commit

Permalink
Vanity URL Handler
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort committed Dec 30, 2024
1 parent d5c35e8 commit 53fb814
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 12 deletions.
18 changes: 10 additions & 8 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const Prefix = "vanity"

// Configures the vanityd server from the environment.
type Config struct {
Maintenance bool `default:"false" desc:"if true, the server will start in maintenance mode"`
LogLevel logger.LevelDecoder `split_words:"true" default:"info" desc:"specify the verbosity of logging (trace, debug, info, warn, error, fatal panic)"`
ConsoleLog bool `split_words:"true" default:"false" desc:"if true logs colorized human readable output instead of json"`
BindAddr string `split_words:"true" default:":3264" desc:"the ip address and port to bind the server on"`
ReadTimeout time.Duration `split_words:"true" default:"20s" desc:"amount of time allowed to read request headers before server decides the request is too slow"`
WriteTimeout time.Duration `split_words:"true" default:"20s" desc:"maximum amount of time before timing out a write to a response"`
IdleTimeout time.Duration `split_words:"true" default:"10m" desc:"maximum amount of time to wait for the next request while keep alives are enabled"`
processed bool
Maintenance bool `default:"false" desc:"if true, the server will start in maintenance mode"`
Domain string `default:"" desc:"specify the domain of the vanity URLs, otherwise the request host will be used"`
DefaultBranch string `split_words:"true" default:"main" desc:"specify the default branch to use for repository references"`
LogLevel logger.LevelDecoder `split_words:"true" default:"info" desc:"specify the verbosity of logging (trace, debug, info, warn, error, fatal panic)"`
ConsoleLog bool `split_words:"true" default:"false" desc:"if true logs colorized human readable output instead of json"`
BindAddr string `split_words:"true" default:":3264" desc:"the ip address and port to bind the server on"`
ReadTimeout time.Duration `split_words:"true" default:"20s" desc:"amount of time allowed to read request headers before server decides the request is too slow"`
WriteTimeout time.Duration `split_words:"true" default:"20s" desc:"maximum amount of time before timing out a write to a response"`
IdleTimeout time.Duration `split_words:"true" default:"10m" desc:"maximum amount of time to wait for the next request while keep alives are enabled"`
processed bool
}

func New() (conf Config, err error) {
Expand Down
9 changes: 9 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package vanity

import "errors"

var (
ErrNoRepository = errors.New("a repository url is required")
ErrInvalidRepository = errors.New("expected repository url in the form vcsScheme://vcsHost/user/repo")
ErrInvalidProtocol = errors.New("protocol must be git, github, or gogs")
)
4 changes: 1 addition & 3 deletions logger/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import (
"time"

"github.com/julienschmidt/httprouter"
"github.com/rotationalio/vanity"
"github.com/rotationalio/vanity/server/middleware"
"github.com/rs/zerolog/log"
)

func HTTPLogger(server string) middleware.Middleware {
version := vanity.Version()
func HTTPLogger(server, version string) middleware.Middleware {
return func(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Before the request
Expand Down
7 changes: 6 additions & 1 deletion server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import (
"net/http"

"github.com/julienschmidt/httprouter"
"github.com/rotationalio/vanity"
"github.com/rotationalio/vanity/logger"
"github.com/rotationalio/vanity/server/middleware"
)

// Sets up the server's middleware and routes.
func (s *Server) setupRoutes() (err error) {
middleware := []middleware.Middleware{
logger.HTTPLogger("vanity"),
logger.HTTPLogger("vanity", vanity.Version()),
s.Maintenance(),
}

Expand All @@ -31,6 +32,10 @@ func (s *Server) setupRoutes() (err error) {
s.addRoute(http.MethodGet, "/", s.HomePage(), middleware...)

// Golang Vanity Handling
pkg := &vanity.GoPackage{Domain: "go.rotational.io", Repository: "https://github.com/rotationalio/confire"}
pkg.Resolve(nil)
s.addRoute(http.MethodGet, "/rotationalio/confire", Vanity(pkg), middleware...)
s.addRoute(http.MethodGet, "/rotationalio/confire/*filepath", Vanity(pkg), middleware...)

return nil
}
Expand Down
4 changes: 4 additions & 0 deletions server/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ header div {
padding: 24px;
margin: 0;
font-size: 24px;
}

.redirect {
padding: 32px 0;
}
23 changes: 23 additions & 0 deletions server/templates/vanity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="{{ .GoImportMeta }}" />
<meta name="go-source" content="{{ .GoSourceMeta }}" />
<meta http-equiv="refresh" content="0; url={{ .Redirect }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,follow">

<title>Rotational Go Packages</title>

<link rel="icon" type="image/ico" href="/static/img/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<header >
<div class="redirect text-center">
If not automatically redirected please click: <a href="{{ .Redirect }}">{{ .Redirect }}</a>.
</div>
</header>
</body>
</html>
33 changes: 33 additions & 0 deletions server/vanity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package server

import (
"html/template"
"io/fs"
"net/http"

"github.com/julienschmidt/httprouter"
"github.com/rotationalio/vanity"
)

func Vanity(pkg *vanity.GoPackage) httprouter.Handle {
// Compile the template for serving the vanity url.
templates, _ := fs.Sub(content, "templates")
index := template.Must(template.ParseFS(templates, "*.html"))

return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Construct a request specific response
data := pkg.WithRequest(r)

// Issue an HTTP redirect if this is definitely a browser.
if r.FormValue("go-get") != "1" {
http.Redirect(w, r, data.Redirect(), http.StatusTemporaryRedirect)
return
}

// Write go-import and go-source meta tags to response.
w.Header().Set("Cache-Control", "public, max-age=300")
if err := index.ExecuteTemplate(w, "vanity.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
169 changes: 169 additions & 0 deletions vanity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package vanity

import (
"net/http"
"net/url"
"path/filepath"
"strings"

"github.com/rotationalio/vanity/config"
)

const (
protocolGit = "git"
protocolGitHub = "github"
protocolGOGS = "gogs"
)

var godoc *url.URL = &url.URL{
Scheme: "https",
Host: "godoc.org",
}

var validProtocols = map[string]struct{}{
protocolGit: {},
protocolGitHub: {},
protocolGOGS: {},
}

type GoPackage struct {
Domain string `json:"-"` // the vanity URL domain to use
Module string `json:"-"` // the module name where go.mod is located; parsed from the repository
Package string `json:"-"` // the full package path being requested for correct redirects
Protocol string `json:"protocol"` // can be "git", "github", or "gogs" -- defaults to "git"
Repository string `json:"repository"` // a path to the public repository starting with https://
Branch string `json:"branch"` // the name of the default branch -- defaults to "main"
repo *url.URL `json:"-"` // the parsed repository URL
user string `json:"-"` // the user or organization from the repository
}

func (p *GoPackage) Resolve(conf *config.Config) (err error) {
// Verify there is a repository
if p.Repository == "" {
return ErrNoRepository
}

// Parse the repository
if p.repo, err = url.Parse(p.Repository); err != nil {
return ErrInvalidRepository
}

parts := strings.Split(p.repo.Path, "/")
if len(parts) != 3 {
return ErrInvalidRepository
}

p.user = parts[1]
p.Module = parts[2]

// Check protocol
if p.Protocol == "" {
p.Protocol = protocolGit
}

if _, ok := validProtocols[p.Protocol]; !ok {
return ErrInvalidProtocol
}

// Manage the configuration
if conf != nil {
p.Domain = conf.Domain

if p.Branch == "" {
p.Branch = conf.DefaultBranch
}
}

// Check the ref
if p.Branch == "" {
p.Branch = "main"
}

return nil
}

func (p *GoPackage) WithRequest(r *http.Request) GoPackage {
pkg := p.Module
if r != nil {
pkg = r.URL.Path
}

clone := GoPackage{
Domain: p.Domain,
Module: p.Module,
Package: pkg,
Protocol: p.Protocol,
Repository: p.Repository,
Branch: p.Branch,
repo: p.repo,
user: p.user,
}

if clone.Domain == "" && r != nil {
clone.Domain = r.Host
}

return clone
}

func (p GoPackage) Redirect() string {
return godoc.ResolveReference(
&url.URL{
Path: filepath.Join("/", p.Domain, p.Package),
},
).String()
}

func (p GoPackage) GoImportMeta() string {
parts := []string{
p.Import(),
p.Protocol,
p.repo.String(),
}

return strings.Join(parts, " ")
}

func (p GoPackage) GoSourceMeta() string {
parts := []string{
p.Import(),
p.repo.String(),
"",
"",
}
parts[2], parts[3] = p.Source()
return strings.Join(parts, " ")
}

func (p GoPackage) Import() string {
return filepath.Join(p.Domain, p.Module)
}

func (p GoPackage) Source() (string, string) {
switch p.Protocol {
case protocolGit, protocolGitHub:
return p.githubSource()
case protocolGOGS:
return p.gogsSource()
default:
return "", ""
}
}

func (p GoPackage) githubSource() (string, string) {
base := filepath.Join(p.user, p.Module)
directoryPath := filepath.Join(base, "tree", p.Branch+"{/dir}")
filePath := filepath.Join(base, "blob", p.Branch+"{/dir}", "{file}#L{line}")

uri := p.repo.ResolveReference(&url.URL{Path: "/"}).String()
return uri + directoryPath, uri + filePath
}

func (p GoPackage) gogsSource() (string, string) {
base := filepath.Join(p.user, p.Module)
directoryPath := filepath.Join(base, "src", p.Branch+"{/dir}")
filePath := filepath.Join(base, "src", p.Branch+"{/dir}", "{file}#L{line}")

uri := p.repo.ResolveReference(&url.URL{Path: "/"}).String()
return uri + directoryPath, uri + filePath
}
52 changes: 52 additions & 0 deletions vanity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package vanity_test

import (
"net/http"
"net/url"
"testing"

. "github.com/rotationalio/vanity"
"github.com/rotationalio/vanity/config"
"github.com/stretchr/testify/require"
)

type expected struct {
redirect string
importMeta string
sourceMeta string
}

func TestGoPackage(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
testCases := []struct {
in *GoPackage
conf *config.Config
req *http.Request
expected expected
}{
{
in: &GoPackage{Repository: "https://github.com/rotationalio/confire"},
conf: &config.Config{Domain: "go.rotational.io", DefaultBranch: "main"},
req: &http.Request{URL: &url.URL{Path: "/confire/validate"}},
expected: expected{
redirect: "https://godoc.org/go.rotational.io/confire/validate",
importMeta: "go.rotational.io/confire git https://github.com/rotationalio/confire",
sourceMeta: "go.rotational.io/confire https://github.com/rotationalio/confire https://github.com/rotationalio/confire/tree/main{/dir} https://github.com/rotationalio/confire/blob/main{/dir}/{file}#L{line}",
},
},
}

for i, tc := range testCases {
// Resolve the go package data
require.NoError(t, tc.in.Resolve(tc.conf), "test case %d failed: could not resolve", i)

// Finalize package for the request
pkg := tc.in.WithRequest(tc.req)

// Perform assertions
require.Equal(t, tc.expected.redirect, pkg.Redirect(), "test case %d failed: bad redirect", i)
require.Equal(t, tc.expected.importMeta, pkg.GoImportMeta(), "test case %d failed: bad import meta", i)
require.Equal(t, tc.expected.sourceMeta, pkg.GoSourceMeta(), "test case %d failed: bad source meta", i)
}
})
}

0 comments on commit 53fb814

Please sign in to comment.