5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 22:31:06 +08:00

Add garble support (#1793)

Co-authored-by: AlbinoDrought <sean@albinodrought.com>
Co-authored-by: stffabi <stffabi@users.noreply.github.com>
This commit is contained in:
Lea Anthony 2022-09-13 10:05:37 +10:00 committed by GitHub
parent eef99ee577
commit 052b9222c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 866 additions and 293 deletions

View File

@ -2,6 +2,7 @@ package build
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/commands/buildtags"
"io"
"os"
"os/exec"
@ -75,7 +76,7 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
// tags to pass to `go`
tags := ""
command.StringFlag("tags", "tags to pass to Go compiler (quoted and space separated)", &tags)
command.StringFlag("tags", "Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated", &tags)
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
@ -111,9 +112,18 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
windowsConsole := false
command.BoolFlag("windowsconsole", "Keep the console when building for Windows", &windowsConsole)
obfuscated := false
command.BoolFlag("obfuscated", "Code obfuscation of bound Wails methods", &obfuscated)
garbleargs := "-literals -tiny -seed=random"
command.StringFlag("garbleargs", "Arguments to pass to garble", &garbleargs)
dryRun := false
command.BoolFlag("dryrun", "Dry run, prints the config for the command that would be executed", &dryRun)
skipBindings := false
command.BoolFlag("skipbindings", "Skips generation of bindings", &skipBindings)
command.Action(func() error {
quiet := verbosity == 0
@ -137,13 +147,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
return fmt.Errorf("unable to find compiler: %s", compilerCommand)
}
// Tags
userTags := []string{}
for _, tag := range strings.Split(tags, " ") {
thisTag := strings.TrimSpace(tag)
if thisTag != "" {
userTags = append(userTags, thisTag)
}
// Process User Tags
userTags, err := buildtags.Parse(tags)
if err != nil {
return err
}
// Webview2 installer strategy (download by default)
@ -176,6 +183,15 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
targets.AddSlice(strings.Split(platform, ","))
targets.Deduplicate()
cwd, err := os.Getwd()
if err != nil {
return err
}
projectOptions, err := project.Load(cwd)
if err != nil {
return err
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
@ -197,6 +213,9 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
TrimPath: trimpath,
RaceDetector: raceDetector,
WindowsConsole: windowsConsole,
Obfuscated: obfuscated,
GarbleArgs: garbleargs,
SkipBindings: skipBindings,
}
// Start a new tabwriter
@ -208,7 +227,12 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
_, _ = fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
_, _ = fmt.Fprintf(w, "Platforms: \t%s\n", platform)
_, _ = fmt.Fprintf(w, "Compiler: \t%s\n", compilerPath)
_, _ = fmt.Fprintf(w, "Skip Bindings: \t%t\n", skipBindings)
_, _ = fmt.Fprintf(w, "Build Mode: \t%s\n", modeString)
_, _ = fmt.Fprintf(w, "Obfuscated: \t%t\n", buildOptions.Obfuscated)
if buildOptions.Obfuscated {
_, _ = fmt.Fprintf(w, "Garble Args: \t%s\n", buildOptions.GarbleArgs)
}
_, _ = fmt.Fprintf(w, "Skip Frontend: \t%t\n", skipFrontend)
_, _ = fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
_, _ = fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
@ -230,15 +254,6 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
projectOptions, err := project.Load(cwd)
if err != nil {
return err
}
// Check platform
validPlatformArch := slicer.String([]string{
"darwin",
@ -329,6 +344,11 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
buildOptions.OutputFile = outputFilename
}
if obfuscated && skipBindings {
logger.Println("Warning: obfuscated flag overrides skipbindings flag.")
buildOptions.SkipBindings = false
}
if !dryRun {
// Start Time
start := time.Now()

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"github.com/wailsapp/wails/v2/pkg/commands/bindings"
"github.com/wailsapp/wails/v2/pkg/commands/buildtags"
"io"
"net"
"net/http"
@ -77,7 +79,7 @@ type devFlags struct {
reloadDirs string
openBrowser bool
noReload bool
noGen bool
skipBindings bool
wailsjsdir string
tags string
verbosity int
@ -106,9 +108,9 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
command.StringFlag("reloaddirs", "Additional directories to trigger reloads (comma separated)", &flags.reloadDirs)
command.BoolFlag("browser", "Open application in browser", &flags.openBrowser)
command.BoolFlag("noreload", "Disable reload on asset change", &flags.noReload)
command.BoolFlag("nogen", "Disable generate module", &flags.noGen)
command.BoolFlag("skipbindings", "Skip bindings generation", &flags.skipBindings)
command.StringFlag("wailsjsdir", "Directory to generate the Wails JS modules", &flags.wailsjsdir)
command.StringFlag("tags", "tags to pass to Go compiler (quoted and space separated)", &flags.tags)
command.StringFlag("tags", "Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated", &flags.tags)
command.IntFlag("v", "Verbosity level (0 - silent, 1 - standard, 2 - verbose)", &flags.verbosity)
command.StringFlag("loglevel", "Loglevel to use - Trace, Debug, Info, Warning, Error", &flags.loglevel)
command.BoolFlag("f", "Force build application", &flags.forceBuild)
@ -125,14 +127,6 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
logger := clilogger.New(w)
app.PrintBanner()
userTags := []string{}
for _, tag := range strings.Split(flags.tags, " ") {
thisTag := strings.TrimSpace(tag)
if thisTag != "" {
userTags = append(userTags, thisTag)
}
}
cwd, err := os.Getwd()
if err != nil {
return err
@ -159,27 +153,36 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
return err
}
// Run go mod tidy to ensure we're up to date
err = runCommand(cwd, false, "go", "mod", "tidy", "-compat=1.17")
// Run go mod tidy to ensure we're up-to-date
err = runCommand(cwd, false, "go", "mod", "tidy")
if err != nil {
return err
}
if !flags.noGen {
self := os.Args[0]
if flags.tags != "" {
err = runCommand(cwd, true, self, "generate", "module", "-tags", flags.tags)
} else {
err = runCommand(cwd, true, self, "generate", "module")
}
if err != nil {
return err
}
}
buildOptions := generateBuildOptions(flags)
buildOptions.Logger = logger
buildOptions.UserTags = internal.ParseUserTags(flags.tags)
userTags, err := buildtags.Parse(flags.tags)
if err != nil {
return err
}
buildOptions.UserTags = userTags
if !flags.skipBindings {
if flags.verbosity == build.VERBOSE {
LogGreen("Generating Bindings...")
}
stdout, err := bindings.GenerateBindings(bindings.Options{
Tags: buildOptions.UserTags,
})
if err != nil {
return err
}
if flags.verbosity == build.VERBOSE {
LogGreen(stdout)
}
}
// Setup signal handler
quitChannel := make(chan os.Signal, 1)
@ -269,7 +272,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
return err
}
// Reset the process and the binary so the defer knows about it and is a nop.
// Reset the process and the binary so defer knows about it and is a nop.
debugBinaryProcess = nil
appBinary = ""
@ -654,7 +657,7 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc
}
if flags.frontendDevServerURL != "" {
// If we are using an external dev server all the reload of the frontend part can be skipped
// If we are using an external dev server, the reloading of the frontend part can be skipped
continue
}
if len(changedPaths) != 0 {

View File

@ -1,16 +1,10 @@
package generate
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/pkg/commands/bindings"
"github.com/wailsapp/wails/v2/pkg/commands/buildtags"
"io"
)
// AddModuleCommand adds the `module` subcommand for the `generate` command
@ -22,37 +16,18 @@ func AddModuleCommand(app *clir.Cli, parent *clir.Command, w io.Writer) error {
command.Action(func() error {
filename := "wailsbindings"
if runtime.GOOS == "windows" {
filename += ".exe"
}
// go build -tags bindings -o bindings.exe
tempDir := os.TempDir()
filename = filepath.Join(tempDir, filename)
cwd, err := os.Getwd()
buildTags, err := buildtags.Parse(tags)
if err != nil {
return err
}
tagList := internal.ParseUserTags(tags)
tagList = append(tagList, "bindings")
stdout, stderr, err := shell.RunCommand(cwd, "go", "build", "-tags", strings.Join(tagList, ","), "-o", filename)
_, err = bindings.GenerateBindings(bindings.Options{
Tags: buildTags,
})
if err != nil {
return fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
return err
}
stdout, stderr, err = shell.RunCommand(cwd, filename)
println(stdout)
println(stderr)
if err != nil {
return fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
}
// Best effort removal of temp file
_ = os.Remove(filename)
return nil
})
return nil

View File

@ -186,7 +186,7 @@ func Install(options *Options) (bool, *Template, error) {
}
} else {
// Get the absolute path of the given directory
targetDir, err := filepath.Abs(filepath.Join(cwd, options.TargetDir))
targetDir, err := filepath.Abs(options.TargetDir)
if err != nil {
return false, nil, err
}

View File

@ -1,15 +0,0 @@
package internal
import "strings"
// ParseUserTags takes the string form of tags and converts to a slice of strings
func ParseUserTags(tagString string) []string {
userTags := make([]string, 0)
for _, tag := range strings.Split(tagString, " ") {
thisTag := strings.TrimSpace(tag)
if thisTag != "" {
userTags = append(userTags, thisTag)
}
}
return userTags
}

View File

@ -31,7 +31,8 @@ func (a *App) Run() error {
a.appoptions.OnDomReady,
a.appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(a.logger, a.appoptions.Bind, bindingExemptions)
appBindings := binding.NewBindings(a.logger, a.appoptions.Bind, bindingExemptions, IsObfuscated())
err := generateBindings(appBindings)
if err != nil {

View File

@ -191,7 +191,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions)
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false)
err = generateBindings(appBindings)
if err != nil {

View File

@ -0,0 +1,8 @@
//go:build !obfuscated
package app
// IsObfuscated returns false if the obfuscated build tag is not set
func IsObfuscated() bool {
return false
}

View File

@ -0,0 +1,8 @@
//go:build obfuscated
package app
// IsObfuscated returns true if the obfuscated build tag is set
func IsObfuscated() bool {
return true
}

View File

@ -1,5 +1,4 @@
//go:build production
// +build production
package app
@ -65,6 +64,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
myLogger.SetLogLevel(appoptions.LogLevelProduction)
}
ctx = context.WithValue(ctx, "logger", myLogger)
ctx = context.WithValue(ctx, "obfuscated", IsObfuscated())
// Preflight Checks
err = PreflightChecks(appoptions, myLogger)
@ -90,7 +90,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions)
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated())
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
// Attach logger to context

View File

@ -22,14 +22,16 @@ type Bindings struct {
exemptions slicer.StringSlicer
structsToGenerateTS map[string]map[string]interface{}
obfuscate bool
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}) *Bindings {
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool) *Bindings {
result := &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate,
}
for _, exemption := range exemptions {

View File

@ -2,6 +2,7 @@ package binding
import (
"encoding/json"
"sort"
"sync"
"unsafe"
)
@ -15,6 +16,9 @@ type DB struct {
// It used for performance gains at runtime
methodMap map[string]*BoundMethod
// This uses ids to reference bound methods at runtime
obfuscatedMethodMap map[int]*BoundMethod
// Lock to ensure sync access to the data
lock sync.RWMutex
}
@ -23,6 +27,7 @@ func newDB() *DB {
return &DB{
store: make(map[string]map[string]map[string]*BoundMethod),
methodMap: make(map[string]*BoundMethod),
obfuscatedMethodMap: make(map[int]*BoundMethod),
}
}
@ -56,11 +61,18 @@ func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {
return d.methodMap[qualifiedMethodName]
}
// GetObfuscatedMethod returns the method for the given ID
func (d *DB) GetObfuscatedMethod(id int) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
return d.obfuscatedMethodMap[id]
}
// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {
// TODO: Validate inputs?
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
@ -97,8 +109,31 @@ func (d *DB) ToJSON() (string, error) {
d.lock.RLock()
defer d.lock.RUnlock()
d.UpdateObfuscatedCallMap()
bytes, err := json.Marshal(&d.store)
// Return zero copy string as this string will be read only
return *(*string)(unsafe.Pointer(&bytes)), err
result := *(*string)(unsafe.Pointer(&bytes))
return result, err
}
// UpdateObfuscatedCallMap sets up the secure call mappings
func (d *DB) UpdateObfuscatedCallMap() map[string]int {
var mappings = make(map[string]int)
// Iterate map keys and sort them
keys := make([]string, 0, len(d.methodMap))
for k := range d.methodMap {
keys = append(keys, k)
}
sort.Strings(keys)
// Iterate sorted keys and add to obfuscated method map
for id, k := range keys {
mappings[k] = id
d.obfuscatedMethodMap[id] = d.methodMap[k]
}
return mappings
}

View File

@ -16,6 +16,10 @@ import (
func (b *Bindings) GenerateGoBindings(baseDir string) error {
store := b.db.store
var obfuscatedBindings map[string]int
if b.obfuscate {
obfuscatedBindings = b.db.UpdateObfuscatedCallMap()
}
for packageName, structs := range store {
packageDir := filepath.Join(baseDir, packageName)
err := fs.Mkdir(packageDir)
@ -54,7 +58,12 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error {
argsString := args.Join(", ")
jsoutput.WriteString(fmt.Sprintf("\nexport function %s(%s) {", methodName, argsString))
jsoutput.WriteString("\n")
if b.obfuscate {
id := obfuscatedBindings[strings.Join([]string{packageName, structName, methodName}, ".")]
jsoutput.WriteString(fmt.Sprintf(" return ObfuscatedCall(%d, [%s]);", id, argsString))
} else {
jsoutput.WriteString(fmt.Sprintf(" return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString))
}
jsoutput.WriteString("\n")
jsoutput.WriteString(fmt.Sprintf("}"))
jsoutput.WriteString("\n")

View File

@ -25,7 +25,7 @@ type B struct {
func TestNestedStruct(t *testing.T) {
bind := &BindForTest{}
testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{})
testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false)
namesStrSlicer := testBindings.getAllStructNames()
names := []string{}

View File

@ -42,7 +42,9 @@ func NewAssetServer(ctx context.Context, options *options.App, bindingsJSON stri
func NewAssetServerWithHandler(ctx context.Context, handler http.Handler, bindingsJSON string) (*AssetServer, error) {
var buffer bytes.Buffer
if bindingsJSON != "" {
buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n")
}
buffer.Write(runtime.RuntimeDesktopJS)
result := &AssetServer{

View File

@ -72,12 +72,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
} else {
bindingsJSON, err := appBindings.ToJSON()
var bindings string
var err error
if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
bindings, err = appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindingsJSON)
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindings)
if err != nil {
log.Fatal(err)
}

View File

@ -75,12 +75,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
} else {
bindingsJSON, err := appBindings.ToJSON()
var bindings string
var err error
if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
bindings, err = appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindingsJSON)
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindings)
if err != nil {
log.Fatal(err)
}

View File

@ -91,12 +91,18 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
return result
}
bindingsJSON, err := appBindings.ToJSON()
var bindings string
var err error
if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
bindings, err = appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindingsJSON)
assets, err := assetserver.NewAssetServer(ctx, appoptions, bindings)
if err != nil {
log.Fatal(err)
}

View File

@ -38,6 +38,8 @@ func (d *Dispatcher) ProcessMessage(message string, sender frontend.Frontend) (s
return d.processEventMessage(message, sender)
case 'C':
return d.processCallMessage(message, sender)
case 'c':
return d.processSecureCallMessage(message, sender)
case 'W':
return d.processWindowMessage(message, sender)
case 'B':

View File

@ -0,0 +1,57 @@
package dispatcher
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/internal/frontend"
)
type secureCallMessage struct {
ID int `json:"id"`
Args []json.RawMessage `json:"args"`
CallbackID string `json:"callbackID"`
}
func (d *Dispatcher) processSecureCallMessage(message string, sender frontend.Frontend) (string, error) {
var payload secureCallMessage
err := json.Unmarshal([]byte(message[1:]), &payload)
if err != nil {
return "", err
}
var result interface{}
// Lookup method
registeredMethod := d.bindingsDB.GetObfuscatedMethod(payload.ID)
// Check we have it
if registeredMethod == nil {
return "", fmt.Errorf("method '%d' not registered", payload.ID)
}
args, err2 := registeredMethod.ParseArgs(payload.Args)
if err2 != nil {
errmsg := fmt.Errorf("error parsing arguments: %s", err2.Error())
result, _ := d.NewErrorCallback(errmsg.Error(), payload.CallbackID)
return result, errmsg
}
result, err = registeredMethod.Call(args)
callbackMessage := &CallbackMessage{
CallbackID: payload.CallbackID,
}
if err != nil {
callbackMessage.Err = err.Error()
} else {
callbackMessage.Result = result
}
messageData, err := json.Marshal(callbackMessage)
d.log.Trace("json call result data: %+v\n", string(messageData))
if err != nil {
// what now?
d.log.Fatal(err.Error())
}
return "c" + string(messageData), nil
}

View File

@ -101,6 +101,52 @@ export function Call(name, args, timeout) {
});
}
window.ObfuscatedCall = (id, args, timeout) => {
// Timeout infinite by default
if (timeout == null) {
timeout = 0;
}
// Create a promise
return new Promise(function (resolve, reject) {
// Create a unique callbackID
var callbackID;
do {
callbackID = id + '-' + randomFunc();
} while (callbacks[callbackID]);
var timeoutHandle;
// Set timeout
if (timeout > 0) {
timeoutHandle = setTimeout(function () {
reject(Error('Call to method ' + id + ' timed out. Request ID: ' + callbackID));
}, timeout);
}
// Store callback
callbacks[callbackID] = {
timeoutHandle: timeoutHandle,
reject: reject,
resolve: resolve
};
try {
const payload = {
id,
args,
callbackID,
};
// Make the call
window.WailsInvoke('c' + JSON.stringify(payload));
} catch (e) {
// eslint-disable-next-line
console.error(e);
}
});
};
/**

View File

@ -70,13 +70,15 @@ window.wails = {
};
// Set the bindings
window.wails.SetBindings(window.wailsbindings);
delete window.wails.SetBindings;
if (window.wailsbindings) {
window.wails.SetBindings(window.wailsbindings);
delete window.wails.SetBindings;
}
// This is evaluated at build time in package.json
// const dev = 0;
// const production = 1;
if (ENV === 0) {
if (ENV === 1) {
delete window.wailsbindings;
}

View File

@ -198,6 +198,15 @@
"esbuild": ">=0.9.6"
}
},
"node_modules/esbuild-svelte/node_modules/svelte": {
"version": "3.43.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.43.1.tgz",
"integrity": "sha512-nvPIaKx4HLzYlSdquISZpgG1Kqr2VAWQjZOt3Iwm3UhbqmA0LnSx4k1YpRMEhjQYW3ZCqQoK8Egto9tv4YewMA==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -702,9 +711,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/side-channel": {
@ -1028,6 +1037,14 @@
"dev": true,
"requires": {
"svelte": "^3.42.6"
},
"dependencies": {
"svelte": {
"version": "3.43.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.43.1.tgz",
"integrity": "sha512-nvPIaKx4HLzYlSdquISZpgG1Kqr2VAWQjZOt3Iwm3UhbqmA0LnSx4k1YpRMEhjQYW3ZCqQoK8Egto9tv4YewMA==",
"dev": true
}
}
},
"escape-string-regexp": {
@ -1388,9 +1405,9 @@
"dev": true
},
"shell-quote": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"side-channel": {

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
//go:build dev || bindings || (!dev && !production && !bindings)
// +build dev bindings !dev,!production,!bindings
package runtime

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
//go:build production && desktop
// +build production,desktop
//go:build production
package runtime

File diff suppressed because one or more lines are too long

View File

@ -84,6 +84,10 @@ type Project struct {
// NSISType to be build
NSISType string `json:"nsisType"`
// Garble
Obfuscated bool `json:"obfuscated"`
GarbleArgs string `json:"garbleargs"`
}
func (p *Project) GetDevBuildCommand() string {

View File

@ -0,0 +1,66 @@
package bindings
import (
"fmt"
"github.com/samber/lo"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/pkg/commands/buildtags"
"os"
"path/filepath"
"runtime"
)
// Options for generating bindings
type Options struct {
Filename string
Tags []string
ProjectDirectory string
GoModTidy bool
}
// GenerateBindings generates bindings for the Wails project in the given ProjectDirectory.
// If no project directory is given then the current working directory is used.
func GenerateBindings(options Options) (string, error) {
filename, _ := lo.Coalesce(options.Filename, "wailsbindings")
if runtime.GOOS == "windows" {
filename += ".exe"
}
// go build -tags bindings -o bindings.exe
tempDir := os.TempDir()
filename = filepath.Join(tempDir, filename)
workingDirectory, _ := lo.Coalesce(options.ProjectDirectory, lo.Must(os.Getwd()))
var stdout, stderr string
var err error
tags := append(options.Tags, "bindings")
genModuleTags := lo.Without(tags, "desktop", "production", "debug", "dev")
tagString := buildtags.Stringify(genModuleTags)
if options.GoModTidy {
stdout, stderr, err = shell.RunCommand(workingDirectory, "go", "mod", "tidy")
if err != nil {
return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
}
}
stdout, stderr, err = shell.RunCommand(workingDirectory, "go", "build", "-tags", tagString, "-o", filename)
if err != nil {
return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
}
defer func() {
// Best effort removal of temp file
_ = os.Remove(filename)
}()
stdout, stderr, err = shell.RunCommand(workingDirectory, filename)
if err != nil {
return stdout, fmt.Errorf("%s\n%s\n%s", stdout, stderr, err)
}
return stdout, nil
}

View File

@ -0,0 +1,119 @@
package bindings
import (
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/initialise/templates"
"os"
"path/filepath"
"strings"
"testing"
)
const standardBindings = `// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
`
const obfuscatedBindings = `// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return ObfuscatedCall(0, [arg1]);
}
`
func TestGenerateBindings(t *testing.T) {
i := is.New(t)
cwd, err := os.Getwd()
if err != nil {
println(err.Error())
t.Fail()
}
projectDir := filepath.Join(cwd, "test")
_ = os.RemoveAll(projectDir)
_, _, err = templates.Install(&templates.Options{
ProjectName: "test",
TemplateName: "plain",
WailsVersion: "latest",
})
if err != nil {
println(err.Error())
t.Fail()
}
defer func() {
_ = os.RemoveAll("test")
}()
// Make the go.mod point to local
goModPath := filepath.Join(projectDir, "go.mod")
goMod, err := os.ReadFile(goModPath)
i.NoErr(err)
goMod = []byte(strings.ReplaceAll(string(goMod), "// replace", "replace"))
// Write file back
err = os.WriteFile(goModPath, goMod, 0755)
i.NoErr(err)
tests := []struct {
name string
options Options
stdout string
expectedBindings string
wantErr bool
}{
{
name: "should generate standard bindings with no user tags",
options: Options{
ProjectDirectory: projectDir,
GoModTidy: true,
},
expectedBindings: standardBindings,
stdout: "",
wantErr: false,
},
{
name: "should generate bindings when given tags",
options: Options{
ProjectDirectory: projectDir,
Tags: []string{"test"},
GoModTidy: true,
},
expectedBindings: standardBindings,
stdout: "",
wantErr: false,
},
{
name: "should generate obfuscated bindings",
options: Options{
ProjectDirectory: projectDir,
Tags: []string{"obfuscated"},
GoModTidy: true,
},
expectedBindings: obfuscatedBindings,
stdout: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stdout, err := GenerateBindings(tt.options)
i.True((err != nil) == tt.wantErr)
i.Equal(stdout, tt.stdout)
// Read bindings
bindingsFile := filepath.Join(projectDir, "frontend", "wailsjs", "go", "main", "App.js")
bindings, err := os.ReadFile(bindingsFile)
i.NoErr(err)
i.Equal(string(bindings), tt.expectedBindings)
})
}
}

View File

@ -153,7 +153,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
verbose := options.Verbosity == VERBOSE
// Run go mod tidy first
if !options.SkipModTidy {
cmd := exec.Command(options.Compiler, "mod", "tidy", "-compat=1.17")
cmd := exec.Command(options.Compiler, "mod", "tidy")
cmd.Stderr = os.Stderr
if verbose {
println("")
@ -165,8 +165,23 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
}
}
commands := slicer.String()
compiler := options.Compiler
if options.Obfuscated {
if !shell.CommandExists("garble") {
return fmt.Errorf("the 'garble' command was not found. Please install it with `go install mvdan.cc/garble@latest`")
} else {
compiler = "garble"
if options.GarbleArgs != "" {
commands.AddSlice(strings.Split(options.GarbleArgs, " "))
}
options.UserTags = append(options.UserTags, "obfuscated")
}
}
// Default go build command
commands := slicer.String([]string{"build"})
commands.Add("build")
// Add better debugging flags
if options.Mode == Dev || options.Mode == Debug {
@ -203,6 +218,10 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
tags.Add("debug")
}
if options.Obfuscated {
tags.Add("obfuscated")
}
tags.Deduplicate()
// Add the output type build tag
@ -247,11 +266,11 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
b.projectData.OutputFilename = strings.TrimPrefix(compiledBinary, options.ProjectData.Path)
options.CompiledBinary = compiledBinary
// Create the command
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
// Build the application
cmd := exec.Command(compiler, commands.AsSlice()...)
cmd.Stderr = os.Stderr
if verbose {
println(" Build command:", commands.Join(" "))
println(" Build command:", compiler, commands.Join(" "))
cmd.Stdout = os.Stdout
}
// Set the directory

View File

@ -2,6 +2,7 @@ package build
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/commands/bindings"
"log"
"os"
"path/filepath"
@ -59,6 +60,9 @@ type Options struct {
TrimPath bool // Use Go's trimpath compiler flag
RaceDetector bool // Build with Go's race detector
WindowsConsole bool // Indicates that the windows console should be kept
Obfuscated bool // Indicates that bound methods should be obfuscated
GarbleArgs string // The arguments for Garble
SkipBindings bool // Skip binding generation
}
// Build the project!
@ -126,6 +130,14 @@ func Build(options *Options) (string, error) {
}
}
// Generate bindings
if !options.SkipBindings {
err = GenerateBindings(options)
if err != nil {
return "", err
}
}
if !options.IgnoreFrontend {
err = builder.BuildFrontend(outputLogger)
if err != nil {
@ -151,6 +163,34 @@ func Build(options *Options) (string, error) {
return compileBinary, nil
}
func GenerateBindings(buildOptions *Options) error {
obfuscated := buildOptions.Obfuscated
if obfuscated {
buildOptions.Logger.Print(" - Generating obfuscated bindings: ")
buildOptions.UserTags = append(buildOptions.UserTags, "obfuscated")
} else {
buildOptions.Logger.Print(" - Generating bindings: ")
}
// Generate Bindings
output, err := bindings.GenerateBindings(bindings.Options{
Tags: buildOptions.UserTags,
GoModTidy: !buildOptions.SkipModTidy,
})
if err != nil {
return err
}
if buildOptions.Verbosity == VERBOSE {
buildOptions.Logger.Println(output)
}
buildOptions.Logger.Println("Done.")
return nil
}
func execBuildApplication(builder Builder, options *Options) (string, error) {
// Extract logger
outputLogger := options.Logger

View File

@ -0,0 +1,45 @@
package buildtags
import (
"errors"
"github.com/samber/lo"
"strings"
)
// Parse parses the given tags string and returns
// a cleaned slice of strings. Both comma and space delimeted
// tags are supported but not mixed. If mixed, an error is returned.
func Parse(tags string) ([]string, error) {
if tags == "" {
return nil, nil
}
var userTags []string
separator := ""
if strings.Contains(tags, ",") {
separator = ","
}
if strings.Contains(tags, " ") {
if separator != "" {
return nil, errors.New("cannot use both space and comma separated values with `-tags` flag")
}
separator = " "
}
for _, tag := range strings.Split(tags, separator) {
thisTag := strings.TrimSpace(tag)
if thisTag != "" {
userTags = append(userTags, thisTag)
}
}
return userTags, nil
}
// Stringify converts the given tags slice to a string compatible
// with the go build -tags flag
func Stringify(tags []string) string {
tags = lo.Map(tags, func(tag string, _ int) string {
return strings.TrimSpace(tag)
})
return strings.Join(tags, ",")
}

View File

@ -0,0 +1,43 @@
# Obfuscated Builds
Wails includes support for obfuscating your application using [garble](https://github.com/burrowers/garble).
To produce an obfuscated build, you can use the `-obfuscate` flag with the `wails build` command:
```bash
wails build -obfuscate
```
To customise the obfuscation settings, you can use the `-garbleargs` flag:
```bash
wails build -obfuscate -garbleargs "-literals -tiny -seed=myrandomseed"
```
These settings may be persisted in your [project config](/guides/reference/project-config).
## How it works
In a standard build, all bound methods are available in the frontend under the `window.go`
variable. When these methods are called, the corresponding backend method is called using
the fully qualified function name. When using an obfuscated build, methods are bound using
an ID instead of a name. The bindings generated in the `wailsjs` directory use these IDs to
call the backend functions.
:::note
To ensure that your application will work in obfuscated mode, you must use the generated
bindings under the `wailsjs` directory in your application.
:::
## Example
Importing the "Greet" method from the bindings like this:
```js
import { Greet } from '../../wailsjs/go/main/App';
// snip
Greet('World');
```
will ensure that the method will work correctly in obfuscated mode, as the bindings will
be regenerated with IDs and the call mechanism updated.

View File

@ -53,7 +53,7 @@ If you are unsure about a template, inspect `package.json` and `wails.json` for
`wails build` is used for compiling your project to a production-ready binary.
| Flag | Description | Default |
| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| :------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------- |
| -platform | Build for the given (comma delimited) [platforms](../reference/cli.mdx#platforms) eg. `windows/arm64`. Note, if you do not give the architecture, `runtime.GOARCH` is used. | platform = `GOOS` environment variable if given else `runtime.GOOS`.<br/>arch = `GOARCH` envrionment variable if given else `runtime.GOARCH`. |
| -clean | Cleans the `build/bin` directory | |
| -compiler "compiler" | Use a different go compiler to build, eg go1.15beta1 | go |
@ -62,7 +62,7 @@ If you are unsure about a template, inspect `package.json` and `wails.json` for
| -o filename | Output filename | |
| -s | Skip building the frontend | false |
| -f | Force build application | false |
| -tags "extra tags" | Build tags to pass to compiler (quoted and space separated) | |
| -tags "extra tags" | Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated | |
| -upx | Compress final binary using "upx" | |
| -upxflags | Flags to pass to upx | |
| -v int | Verbosity level (0 - silent, 1 - default, 2 - verbose) | 1 |

View File

@ -42,7 +42,9 @@ The project config resides in the `wails.json` file in the project directory. Th
"copyright": "[The copyright of the product. Default: 'Copyright.........']",
"comments": "[A short comment of the app. Default: 'Built using Wails (https://wails.app)']"
},
"nsisType": "['multiple': One installer per architecture. 'single': Single universal installer for all architectures being built. Default: 'multiple']"
"nsisType": "['multiple': One installer per architecture. 'single': Single universal installer for all architectures being built. Default: 'multiple']",
"obfuscated": "[Whether the app should be obfuscated. Default: false]",
"garbleargs": "[The arguments to pass to the garble command when using the obfuscated flag]"
}
```