From d8aeecb99f981e697f8658a3e0e6aeb611dc6650 Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Wed, 21 Aug 2024 17:33:47 +0200 Subject: [PATCH] new: embedded ui --- .gitmodules | 3 + Makefile | 7 +- hooks/post_checkout | 4 + modules/ui/ui | 1 + modules/ui/ui.go | 196 +++++++++++++------------------------------- 5 files changed, 70 insertions(+), 141 deletions(-) create mode 100644 .gitmodules create mode 100644 hooks/post_checkout create mode 160000 modules/ui/ui diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..2ccd911f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ui"] + path = modules/ui/ui + url = git@github.com:bettercap/ui.git diff --git a/Makefile b/Makefile index 65a2e917..f0b66583 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ GO ?= go all: build -build: resources +build: resources ui $(GOFLAGS) $(GO) build -o $(TARGET) . build_with_race_detector: resources @@ -13,6 +13,11 @@ build_with_race_detector: resources 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: @python3 ./network/make_manuf.py diff --git a/hooks/post_checkout b/hooks/post_checkout new file mode 100644 index 00000000..398f28a0 --- /dev/null +++ b/hooks/post_checkout @@ -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 \ No newline at end of file diff --git a/modules/ui/ui b/modules/ui/ui new file mode 160000 index 00000000..505b5ede --- /dev/null +++ b/modules/ui/ui @@ -0,0 +1 @@ +Subproject commit 505b5edec3bdad82f63dfac158f0f43a263d098b diff --git a/modules/ui/ui.go b/modules/ui/ui.go index 0e29a525..38339a3b 100644 --- a/modules/ui/ui.go +++ b/modules/ui/ui.go @@ -2,69 +2,55 @@ package ui import ( "context" + "embed" "fmt" - "io" - "io/ioutil" + "io/fs" "net/http" - "os" - "path/filepath" - "regexp" - "runtime" + "time" "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 { session.SessionModule - client *github.Client - tmpFile string - basePath string - uiPath string -} -func getDefaultInstallBase() string { - if runtime.GOOS == "windows" { - return filepath.Join(os.Getenv("ALLUSERSPROFILE"), "bettercap") - } - return "/usr/local/share/bettercap/" + server *http.Server } func NewUIModule(s *session.Session) *UIModule { mod := &UIModule{ SessionModule: session.NewSessionModule("ui", s), - client: github.NewClient(nil), + server: &http.Server{}, } - mod.AddParam(session.NewStringParameter("ui.basepath", - getDefaultInstallBase(), - "", - "UI base installation path.")) + mod.SessionModule.Requires("api.rest") - mod.AddParam(session.NewStringParameter("ui.tmpfile", - filepath.Join(os.TempDir(), "ui.zip"), - "", - "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.", + mod.AddHandler(session.NewModuleHandler("ui on", "", + "Start the web user interface.", func(args []string) error { 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 } @@ -73,7 +59,7 @@ func (mod *UIModule) Name() 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 { @@ -81,118 +67,48 @@ func (mod *UIModule) Author() string { } 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 - } else { - mod.uiPath = filepath.Join(mod.basePath, "ui") } - if err, mod.tmpFile = mod.StringParam("ui.tmpfile"); err != nil { - return err - } + mod.server.Addr = fmt.Sprintf("%s:%d", ip, port) + + dist, _ := fs.Sub(web, "ui/dist/ui") + mod.server.Handler = http.FileServer(http.FS(dist)) 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 { if err := mod.Configure(); err != nil { 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 { - latest := releases[0] - for _, a := range latest.Assets { - if *a.Name == "ui.zip" { - return mod.download(*latest.TagName, *a.BrowserDownloadURL) - } + mod.Info("web ui starting on http://%s", mod.server.Addr) + err = mod.server.ListenAndServe() + + if err != nil && err != http.ErrServerClosed { + panic(err) } - } else { - mod.Error("error while fetching latest release info from GitHub: %s", err) - } + }) 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) + }) +}