Skip to content

Commit

Permalink
add pkg.machengine.org implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Stephen Gutekanst <[email protected]>
  • Loading branch information
emidoots committed Jul 9, 2023
1 parent 5b033a9 commit 7e2de15
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 65 deletions.
35 changes: 21 additions & 14 deletions internal/wrench/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (b *Bot) idLogf(id, format string, v ...any) {
timeNow := time.Now().Format(time.RFC3339)
for _, line := range strings.Split(msg, "\n") {
fmt.Fprintf(b.logFile, "%s %s: %s\n", timeNow, id, line)
fmt.Fprintf(os.Stderr, "%s %s: %s\n", timeNow, id, line)
}
// May be called before DB is initialized.
if b.store != nil {
Expand Down Expand Up @@ -119,23 +120,27 @@ func (b *Bot) run(s service.Service) error {
}

if b.Config.Runner == "" {
b.store, err = OpenStore(filepath.Join(b.Config.WrenchDir, "wrench.db") + "?_pragma=busy_timeout%3d10000")
if err != nil {
return errors.Wrap(err, "OpenStore")
}
if err := b.githubStart(); err != nil {
return errors.Wrap(err, "github")
}
if err := b.discordStart(); err != nil {
return errors.Wrap(err, "discord")
if !b.Config.PkgProxy {
b.store, err = OpenStore(filepath.Join(b.Config.WrenchDir, "wrench.db") + "?_pragma=busy_timeout%3d10000")
if err != nil {
return errors.Wrap(err, "OpenStore")
}
if err := b.githubStart(); err != nil {
return errors.Wrap(err, "github")
}
if err := b.discordStart(); err != nil {
return errors.Wrap(err, "discord")
}
}
if err := b.httpStart(); err != nil {
return errors.Wrap(err, "http")
}
if err := b.schedulerStart(); err != nil {
return errors.Wrap(err, "scheduler")
if !b.Config.PkgProxy {
if err := b.schedulerStart(); err != nil {
return errors.Wrap(err, "scheduler")
}
b.registerCommands()
}
b.registerCommands()
} else {
if err := b.runnerStart(); err != nil {
return errors.Wrap(err, "runner")
Expand Down Expand Up @@ -178,8 +183,10 @@ func (b *Bot) stop() error {
if err := b.httpStop(); err != nil {
return errors.Wrap(err, "http")
}
if err := b.store.Close(); err != nil {
return errors.Wrap(err, "Store.Close")
if b.store != nil {
if err := b.store.Close(); err != nil {
return errors.Wrap(err, "Store.Close")
}
}
if err := b.schedulerStop(); err != nil {
return errors.Wrap(err, "scheduler")
Expand Down
3 changes: 3 additions & 0 deletions internal/wrench/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ type Config struct {
// Disabled if an empty string.
Address string `toml:"Address,omitempty"`

// Act as a Zig package proxy like pkg.machengine.org, instead of as a regular wrench server.
PkgProxy bool `toml:"PkgProxy,omitempty"`

// (optional) Discord bot token. See README.md for details on how to create this.
//
// Disabled if an empty string.
Expand Down
103 changes: 57 additions & 46 deletions internal/wrench/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,62 @@ func (b *Bot) httpStart() error {
})
}

var mux http.Handler
if b.Config.PkgProxy {
b.logf("http: PkgProxy mode enabled")
mux = b.httpMuxPkgProxy(handler)
} else {
mux = b.httpMuxDefault(handler)
}

b.logf("http: listening on %v - %v", b.Config.Address, b.Config.ExternalURL)
if strings.HasSuffix(b.Config.Address, ":443") || strings.HasSuffix(b.Config.Address, ":https") {
// Serve HTTPS using LetsEncrypt
u, err := url.Parse(b.Config.ExternalURL)
if err != nil {
return fmt.Errorf("expected valid config.ExternalURL for LetsEncrypt, found: %v", b.Config.ExternalURL)
}
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(b.Config.LetsEncryptCacheDir),
Email: b.Config.LetsEncryptEmail,
HostPolicy: autocert.HostWhitelist(u.Hostname()),
}

server := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
},
Handler: mux,
}

go func() {
err := http.ListenAndServe(":http", certManager.HTTPHandler(nil))
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}()

// Key and cert are provided by LetsEncrypt
go func() {
err := server.ListenAndServeTLS("", "")
if err != nil {
log.Fatal("ListenAndServeTLS:", err)
}
}()
return nil
}
go func() {
err := http.ListenAndServe(b.Config.Address, mux)
if err != nil {
log.Fatal("ListenAndServe(addr):", err)
}
}()
return nil
}

func (b *Bot) httpMuxDefault(handler func(prefix string, handle handlerFunc) http.Handler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
Expand Down Expand Up @@ -95,52 +151,7 @@ func (b *Bot) httpStart() error {
mux.Handle("/api/secrets/list", handler("api-secrets-list", botHttpAPI(b, b.httpServeSecretsList)))
mux.Handle("/api/secrets/delete", handler("api-secrets-delete", botHttpAPI(b, b.httpServeSecretsDelete)))
mux.Handle("/api/secrets/upsert", handler("api-secrets-upsert", botHttpAPI(b, b.httpServeSecretsUpsert)))

b.logf("http: listening on %v - %v", b.Config.Address, b.Config.ExternalURL)
if strings.HasSuffix(b.Config.Address, ":443") || strings.HasSuffix(b.Config.Address, ":https") {
// Serve HTTPS using LetsEncrypt
u, err := url.Parse(b.Config.ExternalURL)
if err != nil {
return fmt.Errorf("expected valid config.ExternalURL for LetsEncrypt, found: %v", b.Config.ExternalURL)
}
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(b.Config.LetsEncryptCacheDir),
Email: b.Config.LetsEncryptEmail,
HostPolicy: autocert.HostWhitelist(u.Hostname()),
}

server := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
},
Handler: mux,
}

go func() {
err := http.ListenAndServe(":http", certManager.HTTPHandler(nil))
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}()

// Key and cert are provided by LetsEncrypt
go func() {
err := server.ListenAndServeTLS("", "")
if err != nil {
log.Fatal("ListenAndServeTLS:", err)
}
}()
return nil
}
go func() {
err := http.ListenAndServe(b.Config.Address, mux)
if err != nil {
log.Fatal("ListenAndServe(addr):", err)
}
}()
return nil
return mux
}

func (b *Bot) httpStop() error {
Expand Down
152 changes: 152 additions & 0 deletions internal/wrench/http_pkg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package wrench

import (
"fmt"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strings"

"github.com/hexops/wrench/internal/wrench/scripts"
)

func (b *Bot) httpMuxPkgProxy(handler func(prefix string, handle handlerFunc) http.Handler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
handler("general", b.httpPkgRoot).ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/zig") {
handler("zig", b.httpPkgZig).ServeHTTP(w, r)
return
}
handler("pkg", b.httpPkgPkg).ServeHTTP(w, r)
return
})
return mux
}

func (b *Bot) httpPkgRoot(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<h1>pkg.machengine.org</h1>`)
return nil
}

// https://pkg.machengine.org/zig/<file>
// -> https://ziglang.org/builds/<file>
func (b *Bot) httpPkgZig(w http.ResponseWriter, r *http.Request) error {
split := strings.Split(r.URL.Path, "/")
if len(split) == 0 {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "invalid path\n")
return nil
}
fname := split[len(split)-1]

// Validate this is an allowed file
validate := fname
validate = strings.TrimSuffix(validate, ".tar.xz")
validate = strings.TrimSuffix(validate, ".tar.xz.minisig")
validate = strings.TrimSuffix(validate, ".zip")
validate = strings.TrimSuffix(validate, ".zip.minisig")
zigVersionRegexp := regexp.MustCompile(`(\d\.?)+-[[:alnum:]]+.\d+\+[[:alnum:]]+`)
if !zigVersionRegexp.MatchString(validate) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "invalid filename\n")
return nil
}

path := path.Join("cache/zig/", fname)
serveCacheHit := func() error {
w.Header().Set("cache-control", "public, max-age=31536000, immutable")
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}

b.idLogf("zig", "serve %s", path)
http.ServeContent(w, r, fname, fi.ModTime(), f)
return nil
}
if _, err := os.Stat(path); err == nil {
return serveCacheHit()
}

url := "https://ziglang.org/builds/" + fname
logWriter := b.idWriter("zig")
_ = os.MkdirAll("cache/zig/", os.ModePerm)
if err := scripts.DownloadFile(url, path)(logWriter); err != nil {
b.idLogf("zig", "error downloading file: %s url=%s", err, url)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "unable to fetch\n")
return nil
}
return serveCacheHit()
}

// https://pkg.machengine.org/<project>/<file>
// -> https://github.com/hexops/<project>/archive/<file>
func (b *Bot) httpPkgPkg(w http.ResponseWriter, r *http.Request) error {
split := strings.Split(r.URL.Path, "/")
if len(split) != 3 {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "invalid path\n")
return nil
}
project, fname := split[1], split[2]
if project != path.Clean(project) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "illegal project name\n")
return nil
}
if !strings.HasSuffix(fname, ".tar.gz") {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "illegal file extension\n")
return nil
}

cachePath := path.Join("cache/pkg/", project, fname)
serveCacheHit := func() error {
w.Header().Set("cache-control", "public, max-age=31536000, immutable")
f, err := os.Open(cachePath)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}

b.idLogf("pkg", "serve %s", cachePath)
http.ServeContent(w, r, fname, fi.ModTime(), f)
return nil
}
if _, err := os.Stat(cachePath); err == nil {
return serveCacheHit()
}

u := &url.URL{
Scheme: "https",
Host: "github.com",
Path: path.Join("hexops", project, "archive", fname),
}
url := u.String()
logWriter := b.idWriter("pkg")
_ = os.MkdirAll(path.Join("cache/pkg/", project), os.ModePerm)
if err := scripts.DownloadFile(url, cachePath)(logWriter); err != nil {
b.idLogf("pkg", "error downloading file: %s url=%s", err, url)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "unable to fetch\n")
return nil
}
return serveCacheHit()
}
10 changes: 5 additions & 5 deletions internal/wrench/scripts/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,6 @@ func CopyFile(src, dst string) Cmd {
func DownloadFile(url string, filepath string) Cmd {
return func(w io.Writer) error {
fmt.Fprintf(w, "DownloadFile: %s > %s\n", url, filepath)
out, err := os.Create(filepath)
if err != nil {
return errors.Wrap(err, "Create")
}
defer out.Close()

resp, err := http.Get(url)
if err != nil {
Expand All @@ -169,6 +164,11 @@ func DownloadFile(url string, filepath string) Cmd {
return fmt.Errorf("bad response status: %s", resp.Status)
}

out, err := os.Create(filepath)
if err != nil {
return errors.Wrap(err, "Create")
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return errors.Wrap(err, "Copy")
Expand Down
4 changes: 4 additions & 0 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
Expand Down Expand Up @@ -64,6 +65,9 @@ Use "wrench service <command> -h" for more information about a command.
}

func defaultConfigFilePath() string {
if _, err := os.Stat("config.toml"); err == nil {
return "config.toml"
}
u, err := user.Current()
if err == nil {
return filepath.Join(u.HomeDir, "wrench/config.toml")
Expand Down

0 comments on commit 7e2de15

Please sign in to comment.