new: embedded ui

This commit is contained in:
Simone Margaritelli 2024-08-21 17:33:47 +02:00
commit d8aeecb99f
5 changed files with 70 additions and 141 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "ui"]
path = modules/ui/ui
url = git@github.com:bettercap/ui.git

View file

@ -5,7 +5,7 @@ GO ?= go
all: build all: build
build: resources build: resources ui
$(GOFLAGS) $(GO) build -o $(TARGET) . $(GOFLAGS) $(GO) build -o $(TARGET) .
build_with_race_detector: resources build_with_race_detector: resources
@ -13,6 +13,11 @@ build_with_race_detector: resources
resources: network/manuf.go resources: network/manuf.go
ui: ./modules/ui/ui/dist/ui/index.html
./modules/ui/ui/dist/ui/index.html:
@cd modules/ui/ui && make build
network/manuf.go: network/manuf.go:
@python3 ./network/make_manuf.py @python3 ./network/make_manuf.py

4
hooks/post_checkout Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
# Docker hub does a recursive clone, then checks the branch out,
# so when a PR adds a submodule (or updates it), it fails.
git submodule update --init

1
modules/ui/ui Submodule

@ -0,0 +1 @@
Subproject commit 505b5edec3bdad82f63dfac158f0f43a263d098b

View file

@ -2,69 +2,55 @@ package ui
import ( import (
"context" "context"
"embed"
"fmt" "fmt"
"io" "io/fs"
"io/ioutil"
"net/http" "net/http"
"os" "time"
"path/filepath"
"regexp"
"runtime"
"github.com/bettercap/bettercap/v2/session" "github.com/bettercap/bettercap/v2/session"
"github.com/google/go-github/github"
"github.com/evilsocket/islazy/fs"
"github.com/evilsocket/islazy/tui"
"github.com/evilsocket/islazy/zip"
) )
var versionParser = regexp.MustCompile(`name:"ui",version:"([^"]+)"`) var (
//go:embed ui/dist/ui
web embed.FS
)
type UIModule struct { type UIModule struct {
session.SessionModule session.SessionModule
client *github.Client
tmpFile string
basePath string
uiPath string
}
func getDefaultInstallBase() string { server *http.Server
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("ALLUSERSPROFILE"), "bettercap")
}
return "/usr/local/share/bettercap/"
} }
func NewUIModule(s *session.Session) *UIModule { func NewUIModule(s *session.Session) *UIModule {
mod := &UIModule{ mod := &UIModule{
SessionModule: session.NewSessionModule("ui", s), SessionModule: session.NewSessionModule("ui", s),
client: github.NewClient(nil), server: &http.Server{},
} }
mod.AddParam(session.NewStringParameter("ui.basepath", mod.SessionModule.Requires("api.rest")
getDefaultInstallBase(),
"",
"UI base installation path."))
mod.AddParam(session.NewStringParameter("ui.tmpfile", mod.AddHandler(session.NewModuleHandler("ui on", "",
filepath.Join(os.TempDir(), "ui.zip"), "Start the web user interface.",
"",
"Temporary file to use while downloading UI updates."))
mod.AddHandler(session.NewModuleHandler("ui.version", "",
"Print the currently installed UI version.",
func(args []string) error {
return mod.showVersion()
}))
mod.AddHandler(session.NewModuleHandler("ui.update", "",
"Download the latest available version of the UI and install it.",
func(args []string) error { func(args []string) error {
return mod.Start() return mod.Start()
})) }))
mod.AddHandler(session.NewModuleHandler("ui off", "",
"Stop the web user interface.",
func(args []string) error {
return mod.Stop()
}))
mod.AddParam(session.NewStringParameter("ui.address",
"127.0.0.1",
session.IPv4Validator,
"Address to bind the web ui to."))
mod.AddParam(session.NewIntParameter("ui.port",
"8080",
"Port to bind the web ui server to."))
return mod return mod
} }
@ -73,7 +59,7 @@ func (mod *UIModule) Name() string {
} }
func (mod *UIModule) Description() string { func (mod *UIModule) Description() string {
return "A module to manage bettercap's UI updates and installed version." return "Web User Interface."
} }
func (mod *UIModule) Author() string { func (mod *UIModule) Author() string {
@ -81,118 +67,48 @@ func (mod *UIModule) Author() string {
} }
func (mod *UIModule) Configure() (err error) { func (mod *UIModule) Configure() (err error) {
if err, mod.basePath = mod.StringParam("ui.basepath"); err != nil { var ip string
var port int
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if err, ip = mod.StringParam("ui.address"); err != nil {
return err
} else if err, port = mod.IntParam("ui.port"); err != nil {
return err return err
} else {
mod.uiPath = filepath.Join(mod.basePath, "ui")
} }
if err, mod.tmpFile = mod.StringParam("ui.tmpfile"); err != nil { mod.server.Addr = fmt.Sprintf("%s:%d", ip, port)
return err
} dist, _ := fs.Sub(web, "ui/dist/ui")
mod.server.Handler = http.FileServer(http.FS(dist))
return nil return nil
} }
func (mod *UIModule) Stop() error {
return nil
}
func (mod *UIModule) download(version, url string) error {
if !fs.Exists(mod.basePath) {
mod.Warning("creating ui install path %s ...", mod.basePath)
if err := os.MkdirAll(mod.basePath, os.ModePerm); err != nil {
return err
}
}
out, err := os.Create(mod.tmpFile)
if err != nil {
return err
}
defer out.Close()
defer os.Remove(mod.tmpFile)
mod.Info("downloading ui %s from %s ...", tui.Bold(version), url)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
if fs.Exists(mod.uiPath) {
mod.Warning("removing previously installed UI from %s ...", mod.uiPath)
if err := os.RemoveAll(mod.uiPath); err != nil {
return err
}
}
mod.Info("installing to %s ...", mod.uiPath)
if _, err = zip.Unzip(mod.tmpFile, mod.basePath); err != nil {
return err
}
mod.Info("installation complete, you can now run the %s (or https-ui) caplet to start the UI.", tui.Bold("http-ui"))
return nil
}
func (mod *UIModule) showVersion() error {
if err := mod.Configure(); err != nil {
return err
}
if !fs.Exists(mod.uiPath) {
return fmt.Errorf("path %s does not exist, ui not installed", mod.uiPath)
}
search := filepath.Join(mod.uiPath, "/main.*.js")
matches, err := filepath.Glob(search)
if err != nil {
return err
} else if len(matches) == 0 {
return fmt.Errorf("can't find any main.*.js files in %s", mod.uiPath)
}
for _, filename := range matches {
if raw, err := ioutil.ReadFile(filename); err != nil {
return err
} else if m := versionParser.FindStringSubmatch(string(raw)); m != nil {
version := m[1]
mod.Info("v%s", version)
return nil
}
}
return fmt.Errorf("can't parse version from %s", search)
}
func (mod *UIModule) Start() error { func (mod *UIModule) Start() error {
if err := mod.Configure(); err != nil { if err := mod.Configure(); err != nil {
return err return err
} else if err := mod.SetRunning(true, nil); err != nil {
return err
} }
defer mod.SetRunning(false, nil)
mod.Info("checking latest stable release ...") mod.SetRunning(true, func() {
var err error
if releases, _, err := mod.client.Repositories.ListReleases(context.Background(), "bettercap", "ui", nil); err == nil { mod.Info("web ui starting on http://%s", mod.server.Addr)
latest := releases[0] err = mod.server.ListenAndServe()
for _, a := range latest.Assets {
if *a.Name == "ui.zip" { if err != nil && err != http.ErrServerClosed {
return mod.download(*latest.TagName, *a.BrowserDownloadURL) panic(err)
}
}
} else {
mod.Error("error while fetching latest release info from GitHub: %s", err)
} }
})
return nil return nil
} }
func (mod *UIModule) Stop() error {
return mod.SetRunning(false, func() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
mod.server.Shutdown(ctx)
})
}