mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2206e0ae | ||
|
|
a43648a24a | ||
|
|
f65f98bf65 | ||
|
|
26d14e15ad | ||
|
|
cee2cc76c4 | ||
|
|
daa33b6301 | ||
|
|
ef52973536 | ||
|
|
6a7bd0803a | ||
|
|
6c70c1c1e6 | ||
|
|
ddc222b8a2 | ||
|
|
5506a582ac | ||
|
|
63d872c98b | ||
|
|
ef9f5dde0d | ||
|
|
e35b7fdb97 | ||
|
|
efbbf382eb | ||
|
|
0b8d00900d | ||
|
|
9077596f25 | ||
|
|
0a8104a32e | ||
|
|
15a0ba4c1e | ||
|
|
d452c8a676 | ||
|
|
fb74b491fa | ||
|
|
2af6e11adc | ||
|
|
a3a98858fd |
No files matched your search
@@ -29,11 +29,14 @@ MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 24
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
|
||||
@@ -43,3 +43,4 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
|
||||
|
||||
+10
-5
@@ -34,12 +34,19 @@ const (
|
||||
SliverClientDirName = ".sliver-client"
|
||||
|
||||
versionFileName = "version"
|
||||
envVarName = "SLIVER_CLIENT_ROOT_DIR"
|
||||
)
|
||||
|
||||
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
|
||||
func GetRootAppDir() string {
|
||||
user, _ := user.Current()
|
||||
dir := filepath.Join(user.HomeDir, SliverClientDirName)
|
||||
value := os.Getenv(envVarName)
|
||||
var dir string
|
||||
if len(value) == 0 {
|
||||
user, _ := user.Current()
|
||||
dir = filepath.Join(user.HomeDir, SliverClientDirName)
|
||||
} else {
|
||||
dir = value
|
||||
}
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
@@ -116,7 +123,5 @@ under certain conditions; type 'licenses' for details.`)
|
||||
}
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
|
||||
SaveSettings(nil)
|
||||
}
|
||||
_, _ = LoadSettings()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func legacyBackupPath(legacyPath string) string {
|
||||
dir := filepath.Dir(legacyPath)
|
||||
base := filepath.Base(legacyPath)
|
||||
name := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
return filepath.Join(dir, "."+name+".json-old")
|
||||
}
|
||||
|
||||
func renameLegacyConfig(legacyPath string) error {
|
||||
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
backupPath := legacyBackupPath(legacyPath)
|
||||
if _, err := os.Stat(backupPath); err == nil {
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Rename(legacyPath, backupPath)
|
||||
}
|
||||
+39
-16
@@ -22,35 +22,58 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
settingsFileName = "tui-settings.json"
|
||||
settingsFileName = "tui-settings.yaml"
|
||||
settingsLegacyFileName = "tui-settings.json"
|
||||
)
|
||||
|
||||
// ClientSettings - Client JSON config
|
||||
type ClientSettings struct {
|
||||
TableStyle string `json:"tables"`
|
||||
AutoAdult bool `json:"autoadult"`
|
||||
BeaconAutoResults bool `json:"beacon_autoresults"`
|
||||
SmallTermWidth int `json:"small_term_width"`
|
||||
AlwaysOverflow bool `json:"always_overflow"`
|
||||
VimMode bool `json:"vim_mode"`
|
||||
UserConnect bool `json:"user_connect"`
|
||||
ConsoleLogs bool `json:"console_logs"`
|
||||
TableStyle string `json:"tables" yaml:"tables"`
|
||||
AutoAdult bool `json:"autoadult" yaml:"autoadult"`
|
||||
BeaconAutoResults bool `json:"beacon_autoresults" yaml:"beacon_autoresults"`
|
||||
SmallTermWidth int `json:"small_term_width" yaml:"small_term_width"`
|
||||
AlwaysOverflow bool `json:"always_overflow" yaml:"always_overflow"`
|
||||
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
|
||||
UserConnect bool `json:"user_connect" yaml:"user_connect"`
|
||||
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
|
||||
}
|
||||
|
||||
// LoadSettings - Load the client settings from disk
|
||||
func LoadSettings() (*ClientSettings, error) {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
data, err := os.ReadFile(filepath.Join(rootDir, settingsFileName))
|
||||
if err != nil {
|
||||
settingsPath := filepath.Join(rootDir, settingsFileName)
|
||||
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
|
||||
settings := defaultSettings()
|
||||
migratedLegacy := false
|
||||
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err == nil {
|
||||
if err = yaml.Unmarshal(data, settings); err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return defaultSettings(), err
|
||||
} else if data, err = os.ReadFile(legacyPath); err == nil {
|
||||
if err = json.Unmarshal(data, settings); err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
migratedLegacy = true
|
||||
} else if !os.IsNotExist(err) {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
settings := defaultSettings()
|
||||
err = json.Unmarshal(data, settings)
|
||||
if err != nil {
|
||||
return defaultSettings(), err
|
||||
|
||||
if err := SaveSettings(settings); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
if migratedLegacy {
|
||||
if err := renameLegacyConfig(legacyPath); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
@@ -73,7 +96,7 @@ func SaveSettings(settings *ClientSettings) error {
|
||||
if settings == nil {
|
||||
settings = defaultSettings()
|
||||
}
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
data, err := yaml.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadSettingsParsesYAML(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
settingsPath := filepath.Join(rootDir, settingsFileName)
|
||||
|
||||
data := []byte(`tables: "Compact"
|
||||
autoadult: true
|
||||
beacon_autoresults: false
|
||||
small_term_width: 120
|
||||
always_overflow: true
|
||||
vim_mode: true
|
||||
user_connect: true
|
||||
console_logs: false
|
||||
`)
|
||||
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write yaml settings: %v", err)
|
||||
}
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "Compact" {
|
||||
t.Fatalf("expected TableStyle %q, got %q", "Compact", settings.TableStyle)
|
||||
}
|
||||
if !settings.AutoAdult {
|
||||
t.Fatalf("expected AutoAdult true")
|
||||
}
|
||||
if settings.BeaconAutoResults {
|
||||
t.Fatalf("expected BeaconAutoResults false")
|
||||
}
|
||||
if settings.SmallTermWidth != 120 {
|
||||
t.Fatalf("expected SmallTermWidth %d, got %d", 120, settings.SmallTermWidth)
|
||||
}
|
||||
if !settings.AlwaysOverflow {
|
||||
t.Fatalf("expected AlwaysOverflow true")
|
||||
}
|
||||
if !settings.VimMode {
|
||||
t.Fatalf("expected VimMode true")
|
||||
}
|
||||
if !settings.UserConnect {
|
||||
t.Fatalf("expected UserConnect true")
|
||||
}
|
||||
if settings.ConsoleLogs {
|
||||
t.Fatalf("expected ConsoleLogs false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
|
||||
|
||||
legacy := &ClientSettings{
|
||||
TableStyle: "Legacy",
|
||||
AutoAdult: true,
|
||||
BeaconAutoResults: false,
|
||||
SmallTermWidth: 200,
|
||||
AlwaysOverflow: true,
|
||||
VimMode: true,
|
||||
UserConnect: true,
|
||||
ConsoleLogs: false,
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal legacy settings: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write legacy settings: %v", err)
|
||||
}
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "Legacy" {
|
||||
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected yaml settings to exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected legacy settings to be renamed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
|
||||
t.Fatalf("expected legacy backup to exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsWritesDefault(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "SliverDefault" {
|
||||
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
|
||||
}
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected default settings to be written: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
slivermcp "github.com/bishopfox/sliver/client/mcp"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -26,6 +27,12 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
ServerVersion: version,
|
||||
}.WithDefaults()
|
||||
|
||||
msg := `Do you know what prompt injection is and are you an adult?`
|
||||
if !settings.IsUserAnAdultWithPrompt(con, msg) {
|
||||
con.PrintErrorf("Failed to start MCP server, the user is not qualified to use feature\n")
|
||||
return
|
||||
}
|
||||
|
||||
if err := slivermcp.Start(cfg, con.Rpc); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
|
||||
@@ -41,11 +41,16 @@ func SettingsAutoAdultCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
|
||||
// IsUserAnAdult - This should be called for any dangerous (OPSEC-wise) functions.
|
||||
func IsUserAnAdult(con *console.SliverClient) bool {
|
||||
return IsUserAnAdultWithPrompt(con, "This action is bad OPSEC, are you an adult?")
|
||||
}
|
||||
|
||||
// IsUserAnAdultWithPrompt prompts with a custom message before continuing.
|
||||
func IsUserAnAdultWithPrompt(con *console.SliverClient, prompt string) bool {
|
||||
if GetAutoAdult(con) {
|
||||
return true
|
||||
}
|
||||
confirm := false
|
||||
_ = forms.Confirm("This action is bad OPSEC, are you an adult?", &confirm)
|
||||
_ = forms.Confirm(prompt, &confirm)
|
||||
return confirm
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("update", false, updateCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("prereleases", "P", false, "include pre-released (unstable) versions")
|
||||
f.StringP("proxy", "p", "", "specify a proxy url (e.g. http://localhost:8080)")
|
||||
f.StringP("save", "s", "", "save downloaded files to specific directory (default user home dir)")
|
||||
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
|
||||
f.BoolP("insecure", "I", false, "skip tls certificate validation")
|
||||
f.Bool("force", false, "skip version comparison and download the latest release")
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
|
||||
|
||||
+408
-36
@@ -21,6 +21,7 @@ package update
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -40,8 +41,19 @@ import (
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
SliverPublicKey string
|
||||
)
|
||||
|
||||
const (
|
||||
downloadAttempts = 3
|
||||
downloadBackoff = 750 * time.Millisecond
|
||||
)
|
||||
|
||||
// UpdateCmd - Check for updates.
|
||||
@@ -94,7 +106,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
|
||||
con.Printf("\nChecking for updates ... ")
|
||||
prereleases, _ := cmd.Flags().GetBool("prereleases")
|
||||
release, err := version.CheckForUpdates(client, prereleases)
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
var release *version.Release
|
||||
if force {
|
||||
release, err = version.LatestRelease(client, prereleases)
|
||||
} else {
|
||||
release, err = version.CheckForUpdates(client, prereleases)
|
||||
}
|
||||
con.Printf("done!\n\n")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Update check failed %s\n", err)
|
||||
@@ -107,9 +125,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
updateAvailable(con, client, release, saveTo)
|
||||
updateAvailable(con, client, release, saveTo, force)
|
||||
} else {
|
||||
con.PrintInfof("No new releases.\n")
|
||||
if force {
|
||||
con.PrintInfof("No releases found.\n")
|
||||
} else {
|
||||
con.PrintInfof("No new releases.\n")
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
lastCheck := fmt.Appendf(nil, "%d", now.Unix())
|
||||
@@ -191,84 +213,434 @@ func findAssetFor(prefix string, suffixes []string, assets []version.Asset) *ver
|
||||
}
|
||||
|
||||
func serverAssetForGOOS(assets []version.Asset) *version.Asset {
|
||||
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
|
||||
if runtime.GOOS == "darwin" {
|
||||
suffixes = []string{"_macos.zip", "_macos"}
|
||||
if runtime.GOARCH == "arm64" {
|
||||
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
|
||||
}
|
||||
}
|
||||
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
|
||||
prefix := "sliver-server"
|
||||
return findAssetFor(prefix, suffixes, assets)
|
||||
}
|
||||
|
||||
func clientAssetForGOOS(assets []version.Asset) *version.Asset {
|
||||
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
|
||||
if runtime.GOOS == "darwin" {
|
||||
suffixes = []string{"_macos.zip", "_macos"}
|
||||
if runtime.GOARCH == "arm64" {
|
||||
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
|
||||
}
|
||||
}
|
||||
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
|
||||
prefix := "sliver-client"
|
||||
return findAssetFor(prefix, suffixes, assets)
|
||||
}
|
||||
|
||||
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string) {
|
||||
func assetSuffixes(goos, goarch string) []string {
|
||||
suffixes := make([]string, 0, 8)
|
||||
seen := make(map[string]struct{}, 8)
|
||||
add := func(suffix string) {
|
||||
if suffix == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[suffix]; ok {
|
||||
return
|
||||
}
|
||||
seen[suffix] = struct{}{}
|
||||
suffixes = append(suffixes, suffix)
|
||||
}
|
||||
|
||||
add(fmt.Sprintf("_%s-%s.zip", goos, goarch))
|
||||
add(fmt.Sprintf("_%s-%s", goos, goarch))
|
||||
add(fmt.Sprintf("_%s.zip", goos))
|
||||
add(fmt.Sprintf("_%s", goos))
|
||||
|
||||
if goos == "darwin" {
|
||||
add(fmt.Sprintf("_macos-%s.zip", goarch))
|
||||
add(fmt.Sprintf("_macos-%s", goarch))
|
||||
add("_macos.zip")
|
||||
add("_macos")
|
||||
}
|
||||
|
||||
return suffixes
|
||||
}
|
||||
|
||||
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string, force bool) {
|
||||
serverAsset := serverAssetForGOOS(release.Assets)
|
||||
clientAsset := clientAssetForGOOS(release.Assets)
|
||||
|
||||
con.Printf("New version available %s\n", release.TagName)
|
||||
msg := "New version available"
|
||||
if force {
|
||||
msg = "Latest release available"
|
||||
}
|
||||
con.Printf("%s %s\n", msg, release.TagName)
|
||||
if serverAsset != nil {
|
||||
con.Printf(" - Server: %s\n", util.ByteCountBinary(int64(serverAsset.Size)))
|
||||
}
|
||||
if clientAsset != nil {
|
||||
con.Printf(" - Client: %s\n", util.ByteCountBinary(int64(clientAsset.Size)))
|
||||
}
|
||||
if serverAsset == nil {
|
||||
con.PrintWarnf("No server asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
if clientAsset == nil {
|
||||
con.PrintWarnf("No client asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
con.Println()
|
||||
|
||||
confirm := false
|
||||
forms.Confirm("Download update?", &confirm)
|
||||
if confirm {
|
||||
con.Printf("Please wait ...")
|
||||
err := downloadAsset(client, serverAsset, saveTo)
|
||||
if serverAsset == nil && clientAsset == nil {
|
||||
con.PrintWarnf("No matching assets found for %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
return
|
||||
}
|
||||
publicKey, err := loadSliverPublicKey()
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
err = downloadAsset(client, clientAsset, saveTo)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
downloads := []struct {
|
||||
label string
|
||||
asset *version.Asset
|
||||
}{
|
||||
{label: "server", asset: serverAsset},
|
||||
{label: "client", asset: clientAsset},
|
||||
}
|
||||
for _, item := range downloads {
|
||||
if item.asset == nil {
|
||||
continue
|
||||
}
|
||||
con.Printf("Downloading %s update from %s ...\n", item.label, item.asset.BrowserDownloadURL)
|
||||
if err := downloadAssetWithSignature(con, client, item.asset, release.Assets, saveTo, publicKey); err != nil {
|
||||
con.PrintErrorf("Failed to download %s update: %s\n", item.label, err)
|
||||
return
|
||||
}
|
||||
con.Println(console.Clearln)
|
||||
}
|
||||
con.Println(console.Clearln)
|
||||
con.PrintInfof("Saved updates to: %s\n", saveTo)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadAsset(client *http.Client, asset *version.Asset, saveTo string) error {
|
||||
func downloadAssetWithSignature(con *console.SliverClient, client *http.Client, asset *version.Asset, assets []version.Asset, saveTo string, publicKey minisign.PublicKey) error {
|
||||
if asset == nil {
|
||||
return errors.New("no asset available for this platform")
|
||||
}
|
||||
downloadURL, err := url.Parse(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assetFileName := filepath.Base(downloadURL.Path)
|
||||
finalPath := filepath.Join(saveTo, assetFileName)
|
||||
|
||||
limit := int64(asset.Size)
|
||||
writer, err := os.Create(filepath.Join(saveTo, assetFileName))
|
||||
sigURL := asset.BrowserDownloadURL + ".minisig"
|
||||
var sigLimit int64
|
||||
if sigAsset := signatureAssetFor(assetFileName, assets); sigAsset != nil && sigAsset.BrowserDownloadURL != "" {
|
||||
sigURL = sigAsset.BrowserDownloadURL
|
||||
sigLimit = int64(sigAsset.Size)
|
||||
}
|
||||
|
||||
assetTempPath, err := downloadWithRetries(client, asset.BrowserDownloadURL, saveTo, int64(asset.Size), "asset")
|
||||
if err != nil {
|
||||
return fmt.Errorf("download asset: %w", err)
|
||||
}
|
||||
|
||||
sigData, err := downloadBytesWithRetries(client, sigURL, sigLimit, "signature")
|
||||
if err != nil {
|
||||
cleanupAssetFiles(assetTempPath, finalPath)
|
||||
return fmt.Errorf("download signature: %w", err)
|
||||
}
|
||||
|
||||
if err := verifyMinisignSignature(assetTempPath, sigData, assetFileName, publicKey); err != nil {
|
||||
cleanupAssetFiles(assetTempPath, finalPath)
|
||||
return err
|
||||
}
|
||||
defer writer.Close()
|
||||
con.PrintSuccessf("Signature verified for %s\n", finalPath)
|
||||
|
||||
resp, err := client.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
if err := replaceFile(assetTempPath, finalPath); err != nil {
|
||||
os.Remove(assetTempPath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func signatureAssetFor(assetFileName string, assets []version.Asset) *version.Asset {
|
||||
want := assetFileName + ".minisig"
|
||||
for _, asset := range assets {
|
||||
if asset.Name == want {
|
||||
return &asset
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSliverPublicKey() (minisign.PublicKey, error) {
|
||||
if strings.TrimSpace(SliverPublicKey) == "" {
|
||||
return minisign.PublicKey{}, errors.New("minisign public key not set at build time")
|
||||
}
|
||||
var publicKey minisign.PublicKey
|
||||
if err := publicKey.UnmarshalText([]byte(SliverPublicKey)); err != nil {
|
||||
return minisign.PublicKey{}, fmt.Errorf("invalid minisign public key: %w", err)
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
type httpStatusError struct {
|
||||
url string
|
||||
status int
|
||||
}
|
||||
|
||||
func (e httpStatusError) Error() string {
|
||||
return fmt.Sprintf("unexpected HTTP status %d for %s", e.status, e.url)
|
||||
}
|
||||
|
||||
func downloadWithRetries(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
|
||||
var lastErr error
|
||||
backoff := downloadBackoff
|
||||
for attempt := 1; attempt <= downloadAttempts; attempt++ {
|
||||
path, err := downloadOnce(client, downloadURL, saveTo, limit, label)
|
||||
if err == nil {
|
||||
return path, nil
|
||||
}
|
||||
lastErr = err
|
||||
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
|
||||
break
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
func downloadBytesWithRetries(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
|
||||
var lastErr error
|
||||
backoff := downloadBackoff
|
||||
for attempt := 1; attempt <= downloadAttempts; attempt++ {
|
||||
data, err := downloadBytesOnce(client, downloadURL, limit, label)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
lastErr = err
|
||||
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
|
||||
break
|
||||
}
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func downloadOnce(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
|
||||
parsedURL, err := url.Parse(downloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseName := filepath.Base(parsedURL.Path)
|
||||
tmpFile, err := os.CreateTemp(saveTo, baseName+".tmp-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
if !success {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFile.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bar := pb.Full.Start64(limit)
|
||||
barReader := bar.NewProxyReader(resp.Body)
|
||||
io.Copy(writer, barReader)
|
||||
bar.Finish()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", httpStatusError{url: downloadURL, status: resp.StatusCode}
|
||||
}
|
||||
|
||||
if limit <= 0 && resp.ContentLength > 0 {
|
||||
limit = resp.ContentLength
|
||||
}
|
||||
|
||||
var reader io.Reader = resp.Body
|
||||
var bar *pb.ProgressBar
|
||||
if limit > 0 {
|
||||
bar = pb.Full.Start64(limit)
|
||||
bar.Set("prefix", fmt.Sprintf("%s: ", label))
|
||||
bar.Set(pb.Bytes, true)
|
||||
bar.Set(pb.CleanOnFinish, true)
|
||||
bar.Set(pb.ReturnSymbol, "\r")
|
||||
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
bar.Set(pb.Terminal, true)
|
||||
}
|
||||
reader = bar.NewProxyReader(reader)
|
||||
}
|
||||
|
||||
n, copyErr := io.Copy(tmpFile, reader)
|
||||
if bar != nil {
|
||||
bar.Finish()
|
||||
}
|
||||
if copyErr != nil {
|
||||
return "", copyErr
|
||||
}
|
||||
|
||||
if limit > 0 && n != limit {
|
||||
return "", fmt.Errorf("downloaded %d bytes, expected %d", n, limit)
|
||||
}
|
||||
|
||||
if err := tmpFile.Sync(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
success = true
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
func downloadBytesOnce(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, httpStatusError{url: downloadURL, status: resp.StatusCode}
|
||||
}
|
||||
|
||||
if limit <= 0 && resp.ContentLength > 0 {
|
||||
limit = resp.ContentLength
|
||||
}
|
||||
|
||||
var reader io.Reader = resp.Body
|
||||
var bar *pb.ProgressBar
|
||||
if limit > 0 {
|
||||
bar = pb.Full.Start64(limit)
|
||||
bar.Set("prefix", fmt.Sprintf("%s: ", label))
|
||||
bar.Set(pb.Bytes, true)
|
||||
bar.Set(pb.CleanOnFinish, true)
|
||||
bar.Set(pb.ReturnSymbol, "\r")
|
||||
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
bar.Set(pb.Terminal, true)
|
||||
}
|
||||
reader = bar.NewProxyReader(reader)
|
||||
}
|
||||
|
||||
data, readErr := readAllWithLimit(reader, limit)
|
||||
if bar != nil {
|
||||
bar.Finish()
|
||||
}
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func isRetryableDownloadError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return true
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) {
|
||||
return netErr.Timeout() || netErr.Temporary()
|
||||
}
|
||||
var statusErr httpStatusError
|
||||
if errors.As(err, &statusErr) {
|
||||
switch statusErr.status {
|
||||
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func verifyMinisignSignature(artifactPath string, sigData []byte, expectedFile string, publicKey minisign.PublicKey) error {
|
||||
artifact, err := os.Open(artifactPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open artifact: %w", err)
|
||||
}
|
||||
defer artifact.Close()
|
||||
|
||||
reader := minisign.NewReader(artifact)
|
||||
if _, err := io.Copy(io.Discard, reader); err != nil {
|
||||
return fmt.Errorf("stream artifact: %w", err)
|
||||
}
|
||||
|
||||
if !reader.Verify(publicKey, sigData) {
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
if err := verifyTrustedComment(sigData, expectedFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAllWithLimit(reader io.Reader, limit int64) ([]byte, error) {
|
||||
if limit <= 0 {
|
||||
return io.ReadAll(reader)
|
||||
}
|
||||
|
||||
lr := &io.LimitedReader{R: reader, N: limit + 1}
|
||||
data, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(data)) > limit {
|
||||
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
|
||||
}
|
||||
if int64(len(data)) != limit {
|
||||
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func verifyTrustedComment(signature []byte, expectedFile string) error {
|
||||
var sig minisign.Signature
|
||||
if err := sig.UnmarshalText(signature); err != nil {
|
||||
return fmt.Errorf("parse signature: %w", err)
|
||||
}
|
||||
|
||||
fileField := trustedCommentFile(sig.TrustedComment)
|
||||
if fileField == "" {
|
||||
return fmt.Errorf("trusted comment missing file field: %q", sig.TrustedComment)
|
||||
}
|
||||
if fileField != expectedFile {
|
||||
return fmt.Errorf("trusted comment file mismatch: expected %q, got %q", expectedFile, fileField)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trustedCommentFile(trustedComment string) string {
|
||||
for _, field := range strings.Fields(trustedComment) {
|
||||
if after, ok := strings.CutPrefix(field, "file:"); ok {
|
||||
return after
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func replaceFile(src, dst string) error {
|
||||
if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return os.Rename(src, dst)
|
||||
}
|
||||
|
||||
func cleanupAssetFiles(tempPath, finalPath string) {
|
||||
if tempPath != "" {
|
||||
_ = os.Remove(tempPath)
|
||||
}
|
||||
if finalPath != "" {
|
||||
_ = os.Remove(finalPath)
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
@@ -117,7 +119,7 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
|
||||
}
|
||||
|
||||
if contentType == "" {
|
||||
contentType = sniffContentType(file)
|
||||
contentType = sniffContentType(file, contentPath)
|
||||
}
|
||||
|
||||
web.Contents[webpath] = &clientpb.WebContent{
|
||||
@@ -135,13 +137,19 @@ func confirmAddDirectory() bool {
|
||||
return confirm
|
||||
}
|
||||
|
||||
func sniffContentType(out *os.File) string {
|
||||
func sniffContentType(out *os.File, contentPath string) string {
|
||||
ext := strings.ToLower(filepath.Ext(contentPath))
|
||||
if ext != "" {
|
||||
contentType := mime.TypeByExtension(ext)
|
||||
if contentType != "" {
|
||||
return contentType
|
||||
}
|
||||
}
|
||||
out.Seek(0, io.SeekStart)
|
||||
buffer := make([]byte, fileSampleSize)
|
||||
_, err := out.Read(buffer)
|
||||
if err != nil {
|
||||
return defaultMimeType
|
||||
}
|
||||
contentType := http.DetectContentType(buffer)
|
||||
return contentType
|
||||
return http.DetectContentType(buffer)
|
||||
}
|
||||
@@ -227,6 +227,15 @@ func (con *SliverClient) PrintSuccessf(format string, args ...any) {
|
||||
con.printf(Clearln+Success+format, args...)
|
||||
}
|
||||
|
||||
// PrintSuccess prints a success message with a default or provided message.
|
||||
func (con *SliverClient) PrintSuccess(args ...any) {
|
||||
if len(args) == 0 {
|
||||
con.PrintSuccessf("Success")
|
||||
return
|
||||
}
|
||||
con.PrintSuccessf("%s", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// PrintWarnf a warning message immediately below the last line of output.
|
||||
func (con *SliverClient) PrintWarnf(format string, args ...any) {
|
||||
logger := slog.New(con.jsonHandler)
|
||||
|
||||
+65
-20
@@ -63,6 +63,68 @@ func (r *Release) Published() (time.Time, error) {
|
||||
// occurs we don't really try to recover. If client is nil we just use the Go
|
||||
// default client with default settings.
|
||||
func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
|
||||
releases, err := fetchReleases(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(releases) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
current := canonicalSemver(Version)
|
||||
if current == "" {
|
||||
current = "v0.0.0"
|
||||
}
|
||||
for _, release := range releases {
|
||||
if release == nil {
|
||||
continue
|
||||
}
|
||||
if release.Prerelease && !prereleases {
|
||||
continue
|
||||
}
|
||||
releaseVersion := canonicalSemver(release.TagName)
|
||||
if releaseVersion == "" {
|
||||
continue
|
||||
}
|
||||
if semver.Compare(releaseVersion, current) > 0 {
|
||||
return release, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// LatestRelease - Returns the latest release without comparing to the current version.
|
||||
func LatestRelease(client *http.Client, prereleases bool) (*Release, error) {
|
||||
releases, err := fetchReleases(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(releases) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var latest *Release
|
||||
var latestVersion string
|
||||
for _, release := range releases {
|
||||
if release == nil {
|
||||
continue
|
||||
}
|
||||
if release.Prerelease && !prereleases {
|
||||
continue
|
||||
}
|
||||
releaseVersion := canonicalSemver(release.TagName)
|
||||
if releaseVersion == "" {
|
||||
continue
|
||||
}
|
||||
if latest == nil || semver.Compare(releaseVersion, latestVersion) > 0 {
|
||||
latest = release
|
||||
latestVersion = releaseVersion
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func fetchReleases(client *http.Client) ([]*Release, error) {
|
||||
skip := os.Getenv(skipCheckEnv)
|
||||
if skip != "" || GithubReleasesURL == "" {
|
||||
return nil, nil
|
||||
@@ -84,27 +146,10 @@ func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New("API returned non-200 status code")
|
||||
}
|
||||
releases := &[]*Release{}
|
||||
err = json.Unmarshal(body, releases)
|
||||
if err != nil {
|
||||
releases := []*Release{}
|
||||
if err := json.Unmarshal(body, &releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
current := canonicalSemver(Version)
|
||||
if current == "" {
|
||||
current = "v0.0.0"
|
||||
}
|
||||
for _, release := range *releases {
|
||||
if release.Prerelease && !prereleases {
|
||||
continue
|
||||
}
|
||||
releaseVersion := canonicalSemver(release.TagName)
|
||||
if releaseVersion == "" {
|
||||
continue
|
||||
}
|
||||
if semver.Compare(releaseVersion, current) > 0 {
|
||||
return release, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return releases, nil
|
||||
}
|
||||
@@ -4,7 +4,9 @@ go 1.25.5
|
||||
|
||||
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a
|
||||
|
||||
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
|
||||
|
||||
require (
|
||||
filippo.io/age v1.3.1
|
||||
@@ -36,7 +38,7 @@ require (
|
||||
github.com/miekg/dns v1.1.70
|
||||
github.com/moloch--/asciicast v0.1.1
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e
|
||||
github.com/moloch--/sgn v0.0.4
|
||||
github.com/moloch--/sgn v0.0.5
|
||||
github.com/ncruces/go-sqlite3 v0.30.4
|
||||
github.com/reeflective/console v0.1.25
|
||||
github.com/reeflective/readline v1.1.4
|
||||
@@ -99,7 +101,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0 // indirect
|
||||
github.com/carapace-sh/carapace v1.11.0
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
@@ -194,7 +196,7 @@ require (
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0 // indirect
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
@@ -91,12 +91,8 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/carapace-sh/carapace v1.9.0 h1:IuEP6YeeK4fhrH7MoOyL0I3GMhvROerkw6YGxkx8Fbw=
|
||||
github.com/carapace-sh/carapace v1.9.0/go.mod h1:Zs3DpsawpFm+8fEfRVYE9EzMBYH8u7S3Aw9yAAKX0JM=
|
||||
github.com/carapace-sh/carapace v1.11.0 h1:dTUFXvIrUTLd9BGLsfDt4wCgEyPl8bJeZOFDmN7fIfo=
|
||||
github.com/carapace-sh/carapace v1.11.0/go.mod h1:NVkuv1s8wyFvu7d98rdgYwqGKMB6GO1MEUZp5RcWri8=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0 h1:58QSJTUxghpa/rEn6+Z9UM/Hy0ipVHDUn9S1mp4i+ZE=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.0/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q=
|
||||
github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
@@ -354,14 +350,16 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/moloch--/asciicast v0.1.1 h1:btwwniCK9br3ys33XZ71IG/M3ZujSSCcBzsH/sieiXg=
|
||||
github.com/moloch--/asciicast v0.1.1/go.mod h1:OckO16UDLgxVLclrCnbocL1ix15Br/8Xv/caBoYq98o=
|
||||
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701 h1:6caCgDZpovjvv+eclRytFDf0GpVEnjDmoJRHpfkb42E=
|
||||
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
|
||||
github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+NTTRL8=
|
||||
github.com/moloch--/go-keystone v0.0.2/go.mod h1:kpGRJpw9b7Eq3MluHrba+GPKZhSyDrvhLULSwH5nGGM=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e h1:IkFCPlAa0iTiLxck+NqAwBx8JDlnHYm4orOQBbs4BDQ=
|
||||
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e/go.mod h1:eYeI6cQ5YHhHt9i0BBW0zc1DaQnb4ZMXsSPuEV/V5Og=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d h1:9RbKockJl6ZdgoEh2QqU3An6Ij/NWZtRWS3J6yG0+qo=
|
||||
github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/sgn v0.0.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
|
||||
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
|
||||
github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a h1:Q69DxLWF+Z8oIjuNbeKC6Xygk8Z4heWzzO79Io1cEYA=
|
||||
github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
|
||||
github.com/moloch--/sgn v0.0.5 h1:92GFaRpe+353uhqzFiNKXKgmTiD4dBxnvb8gd4MaV/g=
|
||||
github.com/moloch--/sgn v0.0.5/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
@@ -401,8 +399,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/reeflective/console v0.1.25 h1:do/r6rcKH6pCQDEzh5PhoVFcqHkqcjd0N6tNjpmYWWQ=
|
||||
github.com/reeflective/console v0.1.25/go.mod h1:It/IqEjo0PowaBuxBS9pEi5EYUgmtvQd0ytMtlS77kk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -423,11 +419,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
||||
@@ -136,6 +136,7 @@ var rootCmd = &cobra.Command{
|
||||
cryptography.AgeServerKeyPair()
|
||||
cryptography.MinisignServerPrivateKey()
|
||||
c2.SetupDefaultC2Profiles()
|
||||
_, _ = configs.LoadCrackConfig()
|
||||
|
||||
serverConfig := configs.GetServerConfig()
|
||||
listenerJobs, err := db.ListenerJobs()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/c2"
|
||||
"github.com/bishopfox/sliver/server/certs"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
"github.com/bishopfox/sliver/server/console"
|
||||
"github.com/bishopfox/sliver/server/cryptography"
|
||||
"github.com/bishopfox/sliver/server/daemon"
|
||||
@@ -63,6 +64,7 @@ var daemonCmd = &cobra.Command{
|
||||
certs.SetupWGKeys()
|
||||
cryptography.AgeServerKeyPair()
|
||||
cryptography.MinisignServerPrivateKey()
|
||||
_, _ = configs.LoadCrackConfig()
|
||||
|
||||
listenerJobs, err := db.ListenerJobs()
|
||||
if err != nil {
|
||||
|
||||
+83
-25
@@ -26,17 +26,19 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
crackConfigFileName = "crack.json"
|
||||
crackConfigFileName = "crack.yaml"
|
||||
crackLegacyConfigFileName = "crack.json"
|
||||
)
|
||||
|
||||
var (
|
||||
crackConfigLog = log.NamedLogger("config", "crack")
|
||||
)
|
||||
|
||||
// getCrackConfigPath - File path to config.json
|
||||
// getCrackConfigPath - File path to config.yaml
|
||||
func getCrackConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
confPath := filepath.Join(appDir, "configs", crackConfigFileName)
|
||||
@@ -44,6 +46,41 @@ func getCrackConfigPath() string {
|
||||
return confPath
|
||||
}
|
||||
|
||||
func getCrackLegacyConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
return filepath.Join(appDir, "configs", crackLegacyConfigFileName)
|
||||
}
|
||||
|
||||
type crackConfigYAML struct {
|
||||
AutoFire bool `yaml:"AutoFire"`
|
||||
MaxFileSize int64 `yaml:"MaxFileSize"`
|
||||
ChunkSize int64 `yaml:"ChunkSize"`
|
||||
MaxDiskUsage int64 `yaml:"MaxDiskUsage"`
|
||||
}
|
||||
|
||||
func crackConfigFromYAML(data []byte) (*clientpb.CrackConfig, error) {
|
||||
var raw crackConfigYAML
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientpb.CrackConfig{
|
||||
AutoFire: raw.AutoFire,
|
||||
MaxFileSize: raw.MaxFileSize,
|
||||
ChunkSize: raw.ChunkSize,
|
||||
MaxDiskUsage: raw.MaxDiskUsage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func crackConfigToYAML(config *clientpb.CrackConfig) ([]byte, error) {
|
||||
raw := crackConfigYAML{
|
||||
AutoFire: config.AutoFire,
|
||||
MaxFileSize: config.MaxFileSize,
|
||||
ChunkSize: config.ChunkSize,
|
||||
MaxDiskUsage: config.MaxDiskUsage,
|
||||
}
|
||||
return yaml.Marshal(raw)
|
||||
}
|
||||
|
||||
// Save - Save config file to disk
|
||||
func SaveCrackConfig(config *clientpb.CrackConfig) error {
|
||||
configPath := getCrackConfigPath()
|
||||
@@ -55,7 +92,7 @@ func SaveCrackConfig(config *clientpb.CrackConfig) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
data, err := crackConfigToYAML(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,6 +107,7 @@ func SaveCrackConfig(config *clientpb.CrackConfig) error {
|
||||
// LoadCrackConfig - Get config value
|
||||
func LoadCrackConfig() (*clientpb.CrackConfig, error) {
|
||||
configPath := getCrackConfigPath()
|
||||
legacyPath := getCrackLegacyConfigPath()
|
||||
const defaultMaxFileSize = 10 * 1024 * 1024 * 1024 // 10GB - max size of any one file
|
||||
const defaultMaxDiskUsage = 5 * defaultMaxFileSize // 50GB - total disk usage for all files
|
||||
const maxChunkSize = 1024 * 1024 * 1024 // 1GB - max size of any one chunk
|
||||
@@ -80,15 +118,28 @@ func LoadCrackConfig() (*clientpb.CrackConfig, error) {
|
||||
ChunkSize: defaultChunkSize,
|
||||
MaxDiskUsage: defaultMaxDiskUsage,
|
||||
}
|
||||
migratedLegacy := false
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
crackConfigLog.Errorf("Failed to read crack config file %s", err)
|
||||
return config, err
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
parsed, err := crackConfigFromYAML(data)
|
||||
if err != nil {
|
||||
crackConfigLog.Errorf("Failed to parse crack config file %s", err)
|
||||
return config, err
|
||||
}
|
||||
config = parsed
|
||||
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(legacyPath)
|
||||
if err != nil {
|
||||
crackConfigLog.Errorf("Failed to read legacy crack config file %s", err)
|
||||
return config, err
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
crackConfigLog.Errorf("Failed to parse legacy crack config file %s", err)
|
||||
return &clientpb.CrackConfig{
|
||||
AutoFire: true,
|
||||
MaxFileSize: defaultMaxFileSize,
|
||||
@@ -96,33 +147,40 @@ func LoadCrackConfig() (*clientpb.CrackConfig, error) {
|
||||
MaxDiskUsage: defaultMaxDiskUsage,
|
||||
}, err
|
||||
}
|
||||
if config.ChunkSize < 1024 {
|
||||
crackConfigLog.Warnf("Invalid chunk size %d, using default %d", config.ChunkSize, defaultChunkSize)
|
||||
config.ChunkSize = defaultChunkSize
|
||||
}
|
||||
if config.MaxFileSize < 1024 {
|
||||
crackConfigLog.Warnf("Invalid max file size %d, using default %d", config.MaxFileSize, defaultMaxFileSize)
|
||||
config.MaxFileSize = defaultMaxFileSize
|
||||
}
|
||||
if maxChunkSize < config.ChunkSize {
|
||||
crackConfigLog.Warnf("Chunk size is too large, setting to %d", maxChunkSize)
|
||||
config.ChunkSize = maxChunkSize
|
||||
}
|
||||
if config.MaxFileSize < config.ChunkSize {
|
||||
crackConfigLog.Warnf("Chunk size is larger than max file size, setting to %d", config.MaxFileSize)
|
||||
config.ChunkSize = config.MaxFileSize
|
||||
}
|
||||
if config.MaxDiskUsage < config.MaxFileSize {
|
||||
crackConfigLog.Warnf("Max disk usage is smaller than max file size, setting to %d", config.MaxFileSize)
|
||||
config.MaxDiskUsage = config.MaxFileSize
|
||||
}
|
||||
return config, nil
|
||||
migratedLegacy = true
|
||||
crackConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
|
||||
} else {
|
||||
crackConfigLog.Debugf("Crack config file does not exist, using defaults")
|
||||
}
|
||||
if config.ChunkSize < 1024 {
|
||||
crackConfigLog.Warnf("Invalid chunk size %d, using default %d", config.ChunkSize, defaultChunkSize)
|
||||
config.ChunkSize = defaultChunkSize
|
||||
}
|
||||
if config.MaxFileSize < 1024 {
|
||||
crackConfigLog.Warnf("Invalid max file size %d, using default %d", config.MaxFileSize, defaultMaxFileSize)
|
||||
config.MaxFileSize = defaultMaxFileSize
|
||||
}
|
||||
if maxChunkSize < config.ChunkSize {
|
||||
crackConfigLog.Warnf("Chunk size is too large, setting to %d", maxChunkSize)
|
||||
config.ChunkSize = maxChunkSize
|
||||
}
|
||||
if config.MaxFileSize < config.ChunkSize {
|
||||
crackConfigLog.Warnf("Chunk size is larger than max file size, setting to %d", config.MaxFileSize)
|
||||
config.ChunkSize = config.MaxFileSize
|
||||
}
|
||||
if config.MaxDiskUsage < config.MaxFileSize {
|
||||
crackConfigLog.Warnf("Max disk usage is smaller than max file size, setting to %d", config.MaxFileSize)
|
||||
config.MaxDiskUsage = config.MaxFileSize
|
||||
}
|
||||
err := SaveCrackConfig(config) // This updates the config with any missing fields
|
||||
if err != nil {
|
||||
crackConfigLog.Errorf("Failed to save default config %s", err)
|
||||
return config, err
|
||||
}
|
||||
if migratedLegacy {
|
||||
if err := renameLegacyConfig(legacyPath); err != nil {
|
||||
crackConfigLog.Errorf("Failed to rename legacy config %s", err)
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestCrackConfigParsesYAML(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configPath := getCrackConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
data := []byte(`AutoFire: false
|
||||
MaxFileSize: 2048
|
||||
ChunkSize: 1024
|
||||
MaxDiskUsage: 4096
|
||||
`)
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write yaml config: %v", err)
|
||||
}
|
||||
|
||||
config, err := LoadCrackConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if config.AutoFire {
|
||||
t.Fatalf("expected AutoFire false")
|
||||
}
|
||||
if config.MaxFileSize != 2048 {
|
||||
t.Fatalf("expected MaxFileSize %d, got %d", 2048, config.MaxFileSize)
|
||||
}
|
||||
if config.ChunkSize != 1024 {
|
||||
t.Fatalf("expected ChunkSize %d, got %d", 1024, config.ChunkSize)
|
||||
}
|
||||
if config.MaxDiskUsage != 4096 {
|
||||
t.Fatalf("expected MaxDiskUsage %d, got %d", 4096, config.MaxDiskUsage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackConfigWritesDefault(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
config, err := LoadCrackConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatalf("expected default crack config")
|
||||
}
|
||||
if _, err := os.Stat(getCrackConfigPath()); err != nil {
|
||||
t.Fatalf("expected default config file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackConfigMigratesLegacyJSON(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configDir := filepath.Dir(getCrackConfigPath())
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
legacyPath := filepath.Join(configDir, crackLegacyConfigFileName)
|
||||
|
||||
legacy := &clientpb.CrackConfig{
|
||||
AutoFire: false,
|
||||
MaxFileSize: 4096,
|
||||
ChunkSize: 2048,
|
||||
MaxDiskUsage: 8192,
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal legacy config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write legacy config: %v", err)
|
||||
}
|
||||
|
||||
config, err := LoadCrackConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if config.MaxFileSize != 4096 {
|
||||
t.Fatalf("expected MaxFileSize %d, got %d", 4096, config.MaxFileSize)
|
||||
}
|
||||
if _, err := os.Stat(getCrackConfigPath()); err != nil {
|
||||
t.Fatalf("expected migrated yaml config file to exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected legacy config to be renamed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
|
||||
t.Fatalf("expected legacy backup file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
+43
-15
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,7 +41,8 @@ const (
|
||||
// MySQL - MySQL protocol
|
||||
MySQL = "mysql"
|
||||
|
||||
databaseConfigFileName = "database.json"
|
||||
databaseConfigFileName = "database.yaml"
|
||||
databaseLegacyConfigFileName = "database.json"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -57,7 +59,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// GetDatabaseConfigPath - File path to config.json
|
||||
// GetDatabaseConfigPath - File path to config.yaml
|
||||
func GetDatabaseConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
databaseConfigPath := filepath.Join(appDir, "configs", databaseConfigFileName)
|
||||
@@ -65,22 +67,27 @@ func GetDatabaseConfigPath() string {
|
||||
return databaseConfigPath
|
||||
}
|
||||
|
||||
func getDatabaseLegacyConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
return filepath.Join(appDir, "configs", databaseLegacyConfigFileName)
|
||||
}
|
||||
|
||||
// DatabaseConfig - Server config
|
||||
type DatabaseConfig struct {
|
||||
Dialect string `json:"dialect"`
|
||||
Database string `json:"database"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
Dialect string `json:"dialect" yaml:"dialect"`
|
||||
Database string `json:"database" yaml:"database"`
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
|
||||
Params map[string]string `json:"params"`
|
||||
Pragmas map[string]string `json:"pragmas"`
|
||||
Params map[string]string `json:"params" yaml:"params"`
|
||||
Pragmas map[string]string `json:"pragmas" yaml:"pragmas"`
|
||||
|
||||
MaxIdleConns int `json:"max_idle_conns"`
|
||||
MaxOpenConns int `json:"max_open_conns"`
|
||||
MaxIdleConns int `json:"max_idle_conns" yaml:"max_idle_conns"`
|
||||
MaxOpenConns int `json:"max_open_conns" yaml:"max_open_conns"`
|
||||
|
||||
LogLevel string `json:"log_level"`
|
||||
LogLevel string `json:"log_level" yaml:"log_level"`
|
||||
}
|
||||
|
||||
// DSN - Get the db connections string
|
||||
@@ -170,7 +177,7 @@ func (c *DatabaseConfig) Save() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,18 +192,33 @@ func (c *DatabaseConfig) Save() error {
|
||||
// GetDatabaseConfig - Get config value
|
||||
func GetDatabaseConfig() *DatabaseConfig {
|
||||
configPath := GetDatabaseConfigPath()
|
||||
legacyPath := getDatabaseLegacyConfigPath()
|
||||
config := getDefaultDatabaseConfig()
|
||||
migratedLegacy := false
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to read config file %s", err)
|
||||
return config
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
err = yaml.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to parse config file %s", err)
|
||||
return config
|
||||
}
|
||||
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(legacyPath)
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to read legacy config file %s", err)
|
||||
return config
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to parse legacy config file %s", err)
|
||||
return config
|
||||
}
|
||||
migratedLegacy = true
|
||||
databaseConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
|
||||
} else {
|
||||
databaseConfigLog.Warnf("Config file does not exist, using defaults")
|
||||
}
|
||||
@@ -213,6 +235,12 @@ func GetDatabaseConfig() *DatabaseConfig {
|
||||
err := config.Save() // This updates the config with any missing fields
|
||||
if err != nil {
|
||||
databaseConfigLog.Errorf("Failed to save default config %s", err)
|
||||
return config
|
||||
}
|
||||
if migratedLegacy {
|
||||
if err := renameLegacyConfig(legacyPath); err != nil {
|
||||
databaseConfigLog.Errorf("Failed to rename legacy config %s", err)
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDatabaseConfigParsesYAML(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configPath := GetDatabaseConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
data := []byte(`dialect: mysql
|
||||
database: "sliver"
|
||||
username: "tester"
|
||||
password: "secret"
|
||||
host: "127.0.0.1"
|
||||
port: 3306
|
||||
params:
|
||||
charset: "utf8mb4"
|
||||
pragmas: {}
|
||||
max_idle_conns: 7
|
||||
max_open_conns: 15
|
||||
log_level: "info"
|
||||
`)
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write yaml config: %v", err)
|
||||
}
|
||||
|
||||
config := GetDatabaseConfig()
|
||||
if config.Dialect != MySQL {
|
||||
t.Fatalf("expected dialect %q, got %q", MySQL, config.Dialect)
|
||||
}
|
||||
if config.Database != "sliver" {
|
||||
t.Fatalf("expected database %q, got %q", "sliver", config.Database)
|
||||
}
|
||||
if config.Username != "tester" {
|
||||
t.Fatalf("expected username %q, got %q", "tester", config.Username)
|
||||
}
|
||||
if config.Password != "secret" {
|
||||
t.Fatalf("expected password %q, got %q", "secret", config.Password)
|
||||
}
|
||||
if config.Host != "127.0.0.1" {
|
||||
t.Fatalf("expected host %q, got %q", "127.0.0.1", config.Host)
|
||||
}
|
||||
if config.Port != 3306 {
|
||||
t.Fatalf("expected port %d, got %d", 3306, config.Port)
|
||||
}
|
||||
if config.Params["charset"] != "utf8mb4" {
|
||||
t.Fatalf("expected charset param %q, got %q", "utf8mb4", config.Params["charset"])
|
||||
}
|
||||
if config.MaxIdleConns != 7 {
|
||||
t.Fatalf("expected max_idle_conns %d, got %d", 7, config.MaxIdleConns)
|
||||
}
|
||||
if config.MaxOpenConns != 15 {
|
||||
t.Fatalf("expected max_open_conns %d, got %d", 15, config.MaxOpenConns)
|
||||
}
|
||||
if config.LogLevel != "info" {
|
||||
t.Fatalf("expected log_level %q, got %q", "info", config.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseConfigWritesDefault(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
config := GetDatabaseConfig()
|
||||
if config.Dialect != Sqlite {
|
||||
t.Fatalf("expected default dialect %q, got %q", Sqlite, config.Dialect)
|
||||
}
|
||||
if _, err := os.Stat(GetDatabaseConfigPath()); err != nil {
|
||||
t.Fatalf("expected default config file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseConfigMigratesLegacyJSON(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configDir := filepath.Dir(GetDatabaseConfigPath())
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
legacyPath := filepath.Join(configDir, databaseLegacyConfigFileName)
|
||||
|
||||
legacy := &DatabaseConfig{
|
||||
Dialect: Postgres,
|
||||
Database: "sliver",
|
||||
Username: "legacy",
|
||||
Password: "pass",
|
||||
Host: "db.internal",
|
||||
Port: 5432,
|
||||
Params: map[string]string{"sslmode": "disable"},
|
||||
Pragmas: map[string]string{},
|
||||
MaxIdleConns: 4,
|
||||
MaxOpenConns: 9,
|
||||
LogLevel: "debug",
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal legacy config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write legacy config: %v", err)
|
||||
}
|
||||
|
||||
config := GetDatabaseConfig()
|
||||
if config.Dialect != Postgres {
|
||||
t.Fatalf("expected dialect %q, got %q", Postgres, config.Dialect)
|
||||
}
|
||||
if config.Database != "sliver" {
|
||||
t.Fatalf("expected database %q, got %q", "sliver", config.Database)
|
||||
}
|
||||
if config.Params["sslmode"] != "disable" {
|
||||
t.Fatalf("expected sslmode param %q, got %q", "disable", config.Params["sslmode"])
|
||||
}
|
||||
if _, err := os.Stat(GetDatabaseConfigPath()); err != nil {
|
||||
t.Fatalf("expected migrated yaml config file to exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected legacy config to be renamed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
|
||||
t.Fatalf("expected legacy backup file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func legacyBackupPath(legacyPath string) string {
|
||||
dir := filepath.Dir(legacyPath)
|
||||
base := filepath.Base(legacyPath)
|
||||
name := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
return filepath.Join(dir, "."+name+".json-old")
|
||||
}
|
||||
|
||||
func renameLegacyConfig(legacyPath string) error {
|
||||
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
backupPath := legacyBackupPath(legacyPath)
|
||||
if _, err := os.Stat(backupPath); err == nil {
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Rename(legacyPath, backupPath)
|
||||
}
|
||||
+130
-62
@@ -24,20 +24,21 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
serverConfigFileName = "server.json"
|
||||
serverConfigFileName = "server.yaml"
|
||||
serverLegacyConfigFileName = "server.json"
|
||||
)
|
||||
|
||||
var (
|
||||
serverConfigLog = log.NamedLogger("config", "server")
|
||||
)
|
||||
|
||||
// GetServerConfigPath - File path to config.json
|
||||
// GetServerConfigPath - File path to config.yaml
|
||||
func GetServerConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
serverConfigPath := filepath.Join(appDir, "configs", serverConfigFileName)
|
||||
@@ -45,104 +46,150 @@ func GetServerConfigPath() string {
|
||||
return serverConfigPath
|
||||
}
|
||||
|
||||
func getServerLegacyConfigPath() string {
|
||||
appDir := assets.GetRootAppDir()
|
||||
return filepath.Join(appDir, "configs", serverLegacyConfigFileName)
|
||||
}
|
||||
|
||||
// LogConfig - Server logging config
|
||||
type LogConfig struct {
|
||||
Level int `json:"level"`
|
||||
GRPCUnaryPayloads bool `json:"grpc_unary_payloads"`
|
||||
GRPCStreamPayloads bool `json:"grpc_stream_payloads"`
|
||||
TLSKeyLogger bool `json:"tls_key_logger"`
|
||||
Level int `json:"level" yaml:"level"`
|
||||
GRPCUnaryPayloads bool `json:"grpc_unary_payloads" yaml:"grpc_unary_payloads"`
|
||||
GRPCStreamPayloads bool `json:"grpc_stream_payloads" yaml:"grpc_stream_payloads"`
|
||||
TLSKeyLogger bool `json:"tls_key_logger" yaml:"tls_key_logger"`
|
||||
}
|
||||
|
||||
// DaemonConfig - Configure daemon mode
|
||||
type DaemonConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Tailscale bool `json:"tailscale"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port int `json:"port" yaml:"port"`
|
||||
Tailscale bool `json:"tailscale" yaml:"tailscale"`
|
||||
}
|
||||
|
||||
// JobConfig - Restart Jobs on Load
|
||||
type JobConfig struct {
|
||||
Multiplayer []*MultiplayerJobConfig `json:"multiplayer"`
|
||||
MTLS []*MTLSJobConfig `json:"mtls,omitempty"`
|
||||
WG []*WGJobConfig `json:"wg,omitempty"`
|
||||
DNS []*DNSJobConfig `json:"dns,omitempty"`
|
||||
HTTP []*HTTPJobConfig `json:"http,omitempty"`
|
||||
Multiplayer []*MultiplayerJobConfig `json:"multiplayer" yaml:"multiplayer"`
|
||||
MTLS []*MTLSJobConfig `json:"mtls,omitempty" yaml:"mtls,omitempty"`
|
||||
WG []*WGJobConfig `json:"wg,omitempty" yaml:"wg,omitempty"`
|
||||
DNS []*DNSJobConfig `json:"dns,omitempty" yaml:"dns,omitempty"`
|
||||
HTTP []*HTTPJobConfig `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
}
|
||||
|
||||
type MultiplayerJobConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
JobID string `json:"job_id"`
|
||||
Tailscale bool `json:"tailscale"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
Tailscale bool `json:"tailscale" yaml:"tailscale"`
|
||||
}
|
||||
|
||||
// MTLSJobConfig - Per-type job configs
|
||||
type MTLSJobConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
JobID string `json:"job_id"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
}
|
||||
|
||||
// WGJobConfig - Per-type job configs
|
||||
type WGJobConfig struct {
|
||||
Port uint16 `json:"port"`
|
||||
NPort uint16 `json:"nport"`
|
||||
KeyPort uint16 `json:"key_port"`
|
||||
JobID string `json:"job_id"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
NPort uint16 `json:"nport" yaml:"nport"`
|
||||
KeyPort uint16 `json:"key_port" yaml:"key_port"`
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
}
|
||||
|
||||
// DNSJobConfig - Persistent DNS job config
|
||||
type DNSJobConfig struct {
|
||||
Domains []string `json:"domains"`
|
||||
Canaries bool `json:"canaries"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
JobID string `json:"job_id"`
|
||||
EnforceOTP bool `json:"enforce_otp"`
|
||||
Domains []string `json:"domains" yaml:"domains"`
|
||||
Canaries bool `json:"canaries" yaml:"canaries"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
EnforceOTP bool `json:"enforce_otp" yaml:"enforce_otp"`
|
||||
}
|
||||
|
||||
// HTTPJobConfig - Persistent HTTP job config
|
||||
type HTTPJobConfig struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
Secure bool `json:"secure"`
|
||||
Website string `json:"website"`
|
||||
Cert []byte `json:"cert"`
|
||||
Key []byte `json:"key"`
|
||||
ACME bool `json:"acme"`
|
||||
JobID string `json:"job_id"`
|
||||
EnforceOTP bool `json:"enforce_otp"`
|
||||
LongPollTimeout int64 `json:"long_poll_timeout"`
|
||||
LongPollJitter int64 `json:"long_poll_jitter"`
|
||||
RandomizeJARM bool `json:"randomize_jarm"`
|
||||
Domain string `json:"domain" yaml:"domain"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port uint16 `json:"port" yaml:"port"`
|
||||
Secure bool `json:"secure" yaml:"secure"`
|
||||
Website string `json:"website" yaml:"website"`
|
||||
Cert []byte `json:"cert" yaml:"cert"`
|
||||
Key []byte `json:"key" yaml:"key"`
|
||||
ACME bool `json:"acme" yaml:"acme"`
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
EnforceOTP bool `json:"enforce_otp" yaml:"enforce_otp"`
|
||||
LongPollTimeout int64 `json:"long_poll_timeout" yaml:"long_poll_timeout"`
|
||||
LongPollJitter int64 `json:"long_poll_jitter" yaml:"long_poll_jitter"`
|
||||
RandomizeJARM bool `json:"randomize_jarm" yaml:"randomize_jarm"`
|
||||
}
|
||||
|
||||
// WatchTowerConfig - Watch Tower job config
|
||||
type WatchTowerConfig struct {
|
||||
VTApiKey string `json:"vt_api_key"`
|
||||
XForceApiKey string `json:"xforce_api_key"`
|
||||
XForceApiPassword string `json:"xforce_api_password"`
|
||||
VTApiKey string `json:"vt_api_key" yaml:"vt_api_key"`
|
||||
XForceApiKey string `json:"xforce_api_key" yaml:"xforce_api_key"`
|
||||
XForceApiPassword string `json:"xforce_api_password" yaml:"xforce_api_password"`
|
||||
}
|
||||
|
||||
// http server defaults for anonymous requests
|
||||
type HttpDefaultHeader struct {
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Value string `json:"value" yaml:"value"`
|
||||
Probability int32 `json:"probability" yaml:"probability"`
|
||||
}
|
||||
|
||||
type httpDefaultHeaderLegacy struct {
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Value string `json:"value" yaml:"value"`
|
||||
Probability int32 `json:"probability" yaml:"probability"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
HttpC2ServerConfigID *string `json:"httpc2serverconfigid" yaml:"httpc2serverconfigid"`
|
||||
HttpC2ImplantConfigID *string `json:"httpc2implantconfigid" yaml:"httpc2implantconfigid"`
|
||||
}
|
||||
|
||||
func (h *HttpDefaultHeader) UnmarshalJSON(data []byte) error {
|
||||
var raw httpDefaultHeaderLegacy
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
h.Method = raw.Method
|
||||
h.Name = raw.Name
|
||||
h.Value = raw.Value
|
||||
h.Probability = raw.Probability
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HttpDefaultHeader) UnmarshalYAML(node *yaml.Node) error {
|
||||
var raw httpDefaultHeaderLegacy
|
||||
if err := node.Decode(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
h.Method = raw.Method
|
||||
h.Name = raw.Name
|
||||
h.Value = raw.Value
|
||||
h.Probability = raw.Probability
|
||||
return nil
|
||||
}
|
||||
|
||||
type HttpDefaultConfig struct {
|
||||
Headers []models.HttpC2Header `json:"headers"`
|
||||
Headers []HttpDefaultHeader `json:"headers" yaml:"headers"`
|
||||
}
|
||||
|
||||
// ServerConfig - Server config
|
||||
type ServerConfig struct {
|
||||
DaemonMode bool `json:"daemon_mode"`
|
||||
DaemonConfig *DaemonConfig `json:"daemon"`
|
||||
Logs *LogConfig `json:"logs"`
|
||||
Watchtower *WatchTowerConfig `json:"watch_tower"`
|
||||
GoProxy string `json:"go_proxy"`
|
||||
HTTPDefaults *HttpDefaultConfig `json:"http_default"`
|
||||
DonutBypass int `json:"donut_bypass"` // 1=skip, 2=abort on fail, 3=continue on fail.
|
||||
DaemonMode bool `json:"daemon_mode" yaml:"daemon_mode"`
|
||||
DaemonConfig *DaemonConfig `json:"daemon" yaml:"daemon"`
|
||||
Logs *LogConfig `json:"logs" yaml:"logs"`
|
||||
Watchtower *WatchTowerConfig `json:"watch_tower" yaml:"watch_tower"`
|
||||
GoProxy string `json:"go_proxy" yaml:"go_proxy"`
|
||||
HTTPDefaults *HttpDefaultConfig `json:"http_default" yaml:"http_default"`
|
||||
DonutBypass int `json:"donut_bypass" yaml:"donut_bypass"` // 1=skip, 2=abort on fail, 3=continue on fail.
|
||||
|
||||
// 'GOOS/GOARCH' -> CC path
|
||||
CC map[string]string `json:"cc"`
|
||||
CXX map[string]string `json:"cxx"`
|
||||
CC map[string]string `json:"cc" yaml:"cc"`
|
||||
CXX map[string]string `json:"cxx" yaml:"cxx"`
|
||||
}
|
||||
|
||||
// Save - Save config file to disk
|
||||
@@ -156,7 +203,7 @@ func (c *ServerConfig) Save() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,18 +218,33 @@ func (c *ServerConfig) Save() error {
|
||||
// GetServerConfig - Get config value
|
||||
func GetServerConfig() *ServerConfig {
|
||||
configPath := GetServerConfigPath()
|
||||
legacyPath := getServerLegacyConfigPath()
|
||||
config := getDefaultServerConfig()
|
||||
migratedLegacy := false
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
serverConfigLog.Errorf("Failed to read config file %s", err)
|
||||
return config
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
err = yaml.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
serverConfigLog.Errorf("Failed to parse config file %s", err)
|
||||
return config
|
||||
}
|
||||
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
data, err := os.ReadFile(legacyPath)
|
||||
if err != nil {
|
||||
serverConfigLog.Errorf("Failed to read legacy config file %s", err)
|
||||
return config
|
||||
}
|
||||
err = json.Unmarshal(data, config)
|
||||
if err != nil {
|
||||
serverConfigLog.Errorf("Failed to parse legacy config file %s", err)
|
||||
return config
|
||||
}
|
||||
migratedLegacy = true
|
||||
serverConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
|
||||
} else {
|
||||
serverConfigLog.Warnf("Config file does not exist, using defaults")
|
||||
}
|
||||
@@ -202,6 +264,12 @@ func GetServerConfig() *ServerConfig {
|
||||
err := config.Save() // This updates the config with any missing fields
|
||||
if err != nil {
|
||||
serverConfigLog.Errorf("Failed to save default config %s", err)
|
||||
return config
|
||||
}
|
||||
if migratedLegacy {
|
||||
if err := renameLegacyConfig(legacyPath); err != nil {
|
||||
serverConfigLog.Errorf("Failed to rename legacy config %s", err)
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -219,8 +287,8 @@ func getDefaultServerConfig() *ServerConfig {
|
||||
GRPCStreamPayloads: false,
|
||||
},
|
||||
HTTPDefaults: &HttpDefaultConfig{
|
||||
Headers: []models.HttpC2Header{
|
||||
models.HttpC2Header{
|
||||
Headers: []HttpDefaultHeader{
|
||||
{
|
||||
Method: "GET",
|
||||
Name: "Cache-Control",
|
||||
Value: "no-store, no-cache, must-revalidate",
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServerConfigParsesYAML(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configPath := GetServerConfigPath()
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
data := []byte(`daemon_mode: true
|
||||
daemon:
|
||||
host: "127.0.0.1"
|
||||
port: 4444
|
||||
tailscale: true
|
||||
logs:
|
||||
level: 5
|
||||
grpc_unary_payloads: true
|
||||
grpc_stream_payloads: true
|
||||
tls_key_logger: true
|
||||
watch_tower:
|
||||
vt_api_key: "vt"
|
||||
xforce_api_key: "xforce"
|
||||
xforce_api_password: "secret"
|
||||
go_proxy: "https://proxy.example"
|
||||
http_default:
|
||||
headers:
|
||||
- method: "POST"
|
||||
name: "X-Test"
|
||||
value: "abc"
|
||||
probability: 50
|
||||
donut_bypass: 2
|
||||
cc:
|
||||
linux/amd64: "/usr/bin/cc"
|
||||
cxx:
|
||||
linux/amd64: "/usr/bin/c++"
|
||||
`)
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write yaml config: %v", err)
|
||||
}
|
||||
|
||||
config := GetServerConfig()
|
||||
if !config.DaemonMode {
|
||||
t.Fatalf("expected daemon_mode true")
|
||||
}
|
||||
if config.DaemonConfig == nil || config.DaemonConfig.Port != 4444 {
|
||||
t.Fatalf("expected daemon port %d, got %v", 4444, config.DaemonConfig)
|
||||
}
|
||||
if config.Logs == nil || config.Logs.Level != 5 {
|
||||
t.Fatalf("expected logs level %d, got %v", 5, config.Logs)
|
||||
}
|
||||
if config.Logs == nil || !config.Logs.TLSKeyLogger {
|
||||
t.Fatalf("expected tls_key_logger true")
|
||||
}
|
||||
if config.Watchtower == nil || config.Watchtower.VTApiKey != "vt" {
|
||||
t.Fatalf("expected watch_tower vt_api_key %q, got %v", "vt", config.Watchtower)
|
||||
}
|
||||
if config.GoProxy != "https://proxy.example" {
|
||||
t.Fatalf("expected go_proxy %q, got %q", "https://proxy.example", config.GoProxy)
|
||||
}
|
||||
if config.HTTPDefaults == nil || len(config.HTTPDefaults.Headers) != 1 {
|
||||
t.Fatalf("expected 1 http default header, got %v", config.HTTPDefaults)
|
||||
}
|
||||
header := config.HTTPDefaults.Headers[0]
|
||||
if header.Method != "POST" || header.Name != "X-Test" || header.Value != "abc" || header.Probability != 50 {
|
||||
t.Fatalf("unexpected header values: %#v", header)
|
||||
}
|
||||
if config.DonutBypass != 2 {
|
||||
t.Fatalf("expected donut_bypass %d, got %d", 2, config.DonutBypass)
|
||||
}
|
||||
if config.CC["linux/amd64"] != "/usr/bin/cc" {
|
||||
t.Fatalf("expected cc override %q, got %q", "/usr/bin/cc", config.CC["linux/amd64"])
|
||||
}
|
||||
if config.CXX["linux/amd64"] != "/usr/bin/c++" {
|
||||
t.Fatalf("expected cxx override %q, got %q", "/usr/bin/c++", config.CXX["linux/amd64"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConfigWritesDefault(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
config := GetServerConfig()
|
||||
if config.DaemonConfig == nil {
|
||||
t.Fatalf("expected default daemon config")
|
||||
}
|
||||
if _, err := os.Stat(GetServerConfigPath()); err != nil {
|
||||
t.Fatalf("expected default config file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConfigMigratesLegacyJSON(t *testing.T) {
|
||||
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
|
||||
|
||||
configDir := filepath.Dir(GetServerConfigPath())
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
t.Fatalf("failed to create config dir: %v", err)
|
||||
}
|
||||
legacyPath := filepath.Join(configDir, serverLegacyConfigFileName)
|
||||
|
||||
type legacyHeader struct {
|
||||
ID string `json:"id"`
|
||||
HttpC2ServerConfigID *string `json:"httpc2serverconfigid"`
|
||||
HttpC2ImplantConfigID *string `json:"httpc2implantconfigid"`
|
||||
Method string `json:"method"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Probability int32 `json:"probability"`
|
||||
}
|
||||
type legacyHTTPDefaults struct {
|
||||
Headers []legacyHeader `json:"headers"`
|
||||
}
|
||||
type legacyServerConfig struct {
|
||||
DaemonMode bool `json:"daemon_mode"`
|
||||
DaemonConfig *DaemonConfig `json:"daemon"`
|
||||
Logs *LogConfig `json:"logs"`
|
||||
Watchtower *WatchTowerConfig `json:"watch_tower"`
|
||||
GoProxy string `json:"go_proxy"`
|
||||
HTTPDefaults *legacyHTTPDefaults `json:"http_default"`
|
||||
DonutBypass int `json:"donut_bypass"`
|
||||
CC map[string]string `json:"cc"`
|
||||
CXX map[string]string `json:"cxx"`
|
||||
}
|
||||
legacy := &legacyServerConfig{
|
||||
DaemonMode: true,
|
||||
DaemonConfig: &DaemonConfig{
|
||||
Host: "127.0.0.1",
|
||||
Port: 31338,
|
||||
Tailscale: false,
|
||||
},
|
||||
Logs: &LogConfig{
|
||||
Level: 4,
|
||||
GRPCUnaryPayloads: true,
|
||||
GRPCStreamPayloads: true,
|
||||
TLSKeyLogger: true,
|
||||
},
|
||||
Watchtower: &WatchTowerConfig{
|
||||
VTApiKey: "vt-legacy",
|
||||
XForceApiKey: "xforce-legacy",
|
||||
XForceApiPassword: "secret",
|
||||
},
|
||||
GoProxy: "https://proxy.legacy",
|
||||
HTTPDefaults: &legacyHTTPDefaults{
|
||||
Headers: []legacyHeader{
|
||||
{
|
||||
ID: "00000000-0000-0000-0000-000000000000",
|
||||
Method: "GET",
|
||||
Name: "Cache-Control",
|
||||
Value: "no-cache",
|
||||
Probability: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
DonutBypass: 2,
|
||||
CC: map[string]string{"linux/amd64": "/usr/bin/cc"},
|
||||
CXX: map[string]string{"linux/amd64": "/usr/bin/c++"},
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal legacy config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
|
||||
t.Fatalf("failed to write legacy config: %v", err)
|
||||
}
|
||||
|
||||
config := GetServerConfig()
|
||||
if !config.DaemonMode || config.DaemonConfig == nil || config.DaemonConfig.Port != 31338 {
|
||||
t.Fatalf("expected legacy daemon config to load")
|
||||
}
|
||||
if config.Watchtower == nil || config.Watchtower.VTApiKey != "vt-legacy" {
|
||||
t.Fatalf("expected legacy watch_tower to load")
|
||||
}
|
||||
if config.GoProxy != "https://proxy.legacy" {
|
||||
t.Fatalf("expected legacy go_proxy %q, got %q", "https://proxy.legacy", config.GoProxy)
|
||||
}
|
||||
if config.HTTPDefaults == nil || len(config.HTTPDefaults.Headers) != 1 {
|
||||
t.Fatalf("expected legacy http defaults to load")
|
||||
}
|
||||
if _, err := os.Stat(GetServerConfigPath()); err != nil {
|
||||
t.Fatalf("expected migrated yaml config file to exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected legacy config to be renamed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
|
||||
t.Fatalf("expected legacy backup file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,10 @@ func TestSliverSharedLibWindows(t *testing.T) {
|
||||
// 386
|
||||
multiLibrary(t, "windows", "386", true)
|
||||
multiLibrary(t, "windows", "386", false)
|
||||
|
||||
// Shared Library Shellcode
|
||||
multiWindowsLibraryShellcode(t, true)
|
||||
multiWindowsLibraryShellcode(t, false)
|
||||
}
|
||||
|
||||
func TestSliverExecutableLinux(t *testing.T) {
|
||||
@@ -554,7 +558,8 @@ func multiWindowsLibraryShellcode(t *testing.T, debug bool) {
|
||||
Debug: debug,
|
||||
ObfuscateSymbols: true,
|
||||
Format: clientpb.OutputFormat_SHELLCODE,
|
||||
IsSharedLib: true,
|
||||
IsShellcode: true,
|
||||
IsSharedLib: false,
|
||||
Exports: []string{"FoobarW"},
|
||||
IncludeMTLS: true,
|
||||
IncludeHTTP: true,
|
||||
|
||||
@@ -20,10 +20,13 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/server/sgn"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ShellcodeEncode - Encode a piece shellcode
|
||||
@@ -35,9 +38,11 @@ func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.Shellcode
|
||||
switch req.Encoder {
|
||||
case clientpb.ShellcodeEncoder_SHIKATA_GA_NAI:
|
||||
rpcLog.Infof("[rpc] Shellcode encoder request for: SHIKATA_GA_NAI")
|
||||
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), req.Architecture, req.Iterations, req.BadChars)
|
||||
resp.Data, err = sgn.EncodeShellcode(req.Data, req.Architecture, int(req.Iterations), req.BadChars)
|
||||
if err != nil {
|
||||
resp.Response.Err = err.Error()
|
||||
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
|
||||
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
|
||||
}
|
||||
default:
|
||||
resp.Response.Err = "Unknown encoder"
|
||||
|
||||
+8
-5
@@ -56,9 +56,12 @@ type SGNConfig struct {
|
||||
// EncodeShellcode - Encode a shellcode
|
||||
func EncodeShellcode(shellcode []byte, arch string, iterations int, badChars []byte) ([]byte, error) {
|
||||
cfg := SGNConfig{
|
||||
Architecture: arch,
|
||||
Iterations: iterations,
|
||||
BadChars: badChars,
|
||||
Architecture: arch,
|
||||
Iterations: iterations,
|
||||
BadChars: badChars,
|
||||
PlainDecoder: false,
|
||||
Safe: true,
|
||||
MaxObfuscation: 100,
|
||||
}
|
||||
return EncodeShellcodeWithConfig(shellcode, cfg)
|
||||
}
|
||||
@@ -83,10 +86,10 @@ func EncodeShellcodeWithConfig(shellcode []byte, cfg SGNConfig) ([]byte, error)
|
||||
|
||||
needsConstraints := cfg.Asci || len(cfg.BadChars) > 0
|
||||
seed := sgnpkg.GetRandomByte()
|
||||
const maxAttempts = 512
|
||||
const maxAttempts = 64
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
for attempt := range maxAttempts {
|
||||
encoder, err := newEncoderWithConfig(arch, cfg)
|
||||
if err != nil {
|
||||
sgnLog.Errorf("[sgn] EncodeShellcode setup failed: %s", err)
|
||||
|
||||
+56
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
keystone "github.com/moloch--/go-keystone"
|
||||
)
|
||||
@@ -197,6 +198,12 @@ func (encoder Encoder) Assemble(asm string) ([]byte, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
asm = normalizeAssembly(asm)
|
||||
if asm == "" {
|
||||
return []byte{}, true
|
||||
}
|
||||
asm = addCodeDirective(asm, encoder.architecture)
|
||||
|
||||
ks, err := keystone.NewEngine(keystone.ARCH_X86, mode)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
@@ -227,6 +234,12 @@ func (encoder Encoder) GetAssemblySize(asm string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
asm = normalizeAssembly(asm)
|
||||
if asm == "" {
|
||||
return 0
|
||||
}
|
||||
asm = addCodeDirective(asm, encoder.architecture)
|
||||
|
||||
ks, err := keystone.NewEngine(keystone.ARCH_X86, mode)
|
||||
if err != nil {
|
||||
return -1
|
||||
@@ -247,6 +260,49 @@ func (encoder Encoder) GetAssemblySize(asm string) int {
|
||||
return len(bin)
|
||||
}
|
||||
|
||||
func normalizeAssembly(asm string) string {
|
||||
if asm == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
cleaned := strings.ReplaceAll(asm, ";", "\n")
|
||||
lines := strings.Split(cleaned, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
func addCodeDirective(asm string, arch int) string {
|
||||
if asm == "" || hasCodeDirective(asm) {
|
||||
return asm
|
||||
}
|
||||
|
||||
switch arch {
|
||||
case 32:
|
||||
return ".code32\n" + asm
|
||||
case 64:
|
||||
return ".code64\n" + asm
|
||||
default:
|
||||
return asm
|
||||
}
|
||||
}
|
||||
|
||||
func hasCodeDirective(asm string) bool {
|
||||
for _, line := range strings.Split(asm, "\n") {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(line))
|
||||
if strings.HasPrefix(trimmed, ".code") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateIPToStack function generates instructions series that pushes the instruction pointer to stack
|
||||
func (encoder Encoder) GenerateIPToStack() []byte {
|
||||
|
||||
|
||||
+24
-9
@@ -23,6 +23,7 @@ type Console struct {
|
||||
menus map[string]*Menu // Different command trees, prompt engines, etc.
|
||||
filters []string // Hide commands based on their attributes and current context.
|
||||
isExecuting bool // Used by log functions, which need to adapt behavior (print the prompt, etc.)
|
||||
isReading bool // True while readline is waiting for input.
|
||||
printed bool // Used to adjust asynchronous messages too.
|
||||
mutex *sync.RWMutex // Concurrency management.
|
||||
|
||||
@@ -145,6 +146,27 @@ func (c *Console) SetDefaultFlagHighlight(seq string) {
|
||||
c.flagHighlight = seq
|
||||
}
|
||||
|
||||
func (c *Console) isExecutingSafe() bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
return c.isExecuting
|
||||
}
|
||||
|
||||
func (c *Console) isReadingSafe() bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
return c.isReading
|
||||
}
|
||||
|
||||
func (c *Console) setReading(reading bool) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.isReading = reading
|
||||
}
|
||||
|
||||
//
|
||||
// Menu Management --------------------------------------------------------------------------------- //
|
||||
//
|
||||
@@ -224,17 +246,10 @@ func (c *Console) SwitchMenu(menu string) {
|
||||
// If this function is called while a command is running, the console will simply print the log
|
||||
// below the line, and will not print the prompt. In any other case this function works normally.
|
||||
func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) {
|
||||
if c.isExecuting {
|
||||
if c.isExecutingSafe() && !c.isReadingSafe() {
|
||||
return fmt.Printf(msg, args...)
|
||||
}
|
||||
|
||||
// If the last message we printed asynchronously
|
||||
// immediately precedes this new message, move up
|
||||
// another row, so we don't waste too much space.
|
||||
if c.printed && c.NewlineAfter {
|
||||
fmt.Print("\x1b[1A")
|
||||
}
|
||||
|
||||
if c.NewlineAfter {
|
||||
msg += "\n"
|
||||
}
|
||||
@@ -250,7 +265,7 @@ func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) {
|
||||
// If this function is called while a command is running, the console will simply print the log
|
||||
// below the line, and will not print the prompt. In any other case this function works normally.
|
||||
func (c *Console) Printf(msg string, args ...any) (n int, err error) {
|
||||
if c.isExecuting {
|
||||
if c.isExecutingSafe() && !c.isReadingSafe() {
|
||||
return fmt.Printf(msg, args...)
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -29,14 +29,14 @@ func (m *Menu) DelInterrupt(errs ...error) {
|
||||
}
|
||||
|
||||
func (m *Menu) handleInterrupt(err error) {
|
||||
m.console.mutex.RLock()
|
||||
m.console.mutex.Lock()
|
||||
m.console.isExecuting = true
|
||||
m.console.mutex.RUnlock()
|
||||
m.console.mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
m.console.mutex.RLock()
|
||||
m.console.mutex.Lock()
|
||||
m.console.isExecuting = false
|
||||
m.console.mutex.RUnlock()
|
||||
m.console.mutex.Unlock()
|
||||
}()
|
||||
|
||||
// TODO: this is not a very, very safe way of comparing
|
||||
|
||||
+8
-6
@@ -47,7 +47,9 @@ func (c *Console) StartContext(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Block and read user input.
|
||||
input , err := c.shell.Readline()
|
||||
c.setReading(true)
|
||||
input, err := c.shell.Readline()
|
||||
c.setReading(false)
|
||||
|
||||
c.displayPostRun(input)
|
||||
|
||||
@@ -109,7 +111,7 @@ func (m *Menu) RunCommandArgs(ctx context.Context, args []string) (err error) {
|
||||
m.resetPreRun()
|
||||
|
||||
// Run the command and associated helpers.
|
||||
return m.console.execute(ctx, m, args, !m.console.isExecuting)
|
||||
return m.console.execute(ctx, m, args, !m.console.isExecutingSafe())
|
||||
}
|
||||
|
||||
// RunCommandLine is the equivalent of menu.RunCommandArgs(), but accepts
|
||||
@@ -137,15 +139,15 @@ func (m *Menu) RunCommandLine(ctx context.Context, line string) (err error) {
|
||||
// command is running, the menu's root command will be overwritten.
|
||||
func (c *Console) execute(ctx context.Context, menu *Menu, args []string, async bool) error {
|
||||
if !async {
|
||||
c.mutex.RLock()
|
||||
c.mutex.Lock()
|
||||
c.isExecuting = true
|
||||
c.mutex.RUnlock()
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.mutex.RLock()
|
||||
c.mutex.Lock()
|
||||
c.isExecuting = false
|
||||
c.mutex.RUnlock()
|
||||
c.mutex.Unlock()
|
||||
}()
|
||||
|
||||
// Our root command of interest, used throughout this function.
|
||||
|
||||
+78
-2
@@ -1,11 +1,13 @@
|
||||
package readline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
|
||||
@@ -448,8 +450,82 @@ func (rl *Shell) selfInsert() {
|
||||
}
|
||||
|
||||
func (rl *Shell) bracketedPasteBegin() {
|
||||
// keys, _ := rl.Keys.PeekAllBytes()
|
||||
// fmt.Println(string(keys))
|
||||
rl.History.SkipSave()
|
||||
rl.completer.TrimSuffix()
|
||||
|
||||
const endSeq = "\x1b[201~"
|
||||
|
||||
data := rl.Keys.DrainBuffer()
|
||||
|
||||
for {
|
||||
if idx := bytes.Index(data, []byte(endSeq)); idx >= 0 {
|
||||
payload := data[:idx]
|
||||
rest := data[idx+len(endSeq):]
|
||||
if len(rest) > 0 {
|
||||
rl.Keys.PrependBuffer(rest)
|
||||
}
|
||||
|
||||
rl.insertPastedBytes(payload)
|
||||
return
|
||||
}
|
||||
|
||||
more, err := rl.Keys.ReadInput()
|
||||
if err != nil {
|
||||
rl.insertPastedBytes(data)
|
||||
return
|
||||
}
|
||||
|
||||
if len(more) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
data = append(data, more...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *Shell) insertPastedBytes(data []byte) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
runes := normalizePasteBytes(data)
|
||||
if len(runes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rl.cursor.InsertAt(runes...)
|
||||
rl.Keys.SetMatched(runes...)
|
||||
}
|
||||
|
||||
func normalizePasteBytes(data []byte) []rune {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]rune, 0, len(data))
|
||||
|
||||
for len(data) > 0 {
|
||||
if data[0] == '\r' {
|
||||
if len(data) > 1 && data[1] == '\n' {
|
||||
data = data[1:]
|
||||
}
|
||||
out = append(out, '\n')
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRune(data)
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
out = append(out, rune(data[0]))
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
data = data[size:]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Drag the character before point forward over the character
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ func Display(eng *Engine, maxRows int) {
|
||||
// little more time. The engine itself is responsible for
|
||||
// deleting those lists when it deems them useless.
|
||||
if eng.Matches() == 0 || eng.skipDisplay {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func Display(eng *Engine, maxRows int) {
|
||||
completions, eng.usedY = eng.cropCompletions(completions, maxRows)
|
||||
|
||||
if completions != "" {
|
||||
fmt.Print(completions)
|
||||
term.WriteString(completions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
-1
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
keyScanBufSize = 1024
|
||||
keyScanBufSize = 8192
|
||||
)
|
||||
|
||||
// Stdin is used by the Keys struct to read and write keys.
|
||||
@@ -238,6 +238,89 @@ func FlushUsed(keys *Keys) {
|
||||
defer keys.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Pending returns the number of buffered keys waiting to be processed.
|
||||
func (k *Keys) Pending() int {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
return len(k.buf) + len(k.macroKeys)
|
||||
}
|
||||
|
||||
// BufferSize returns the number of raw input bytes buffered.
|
||||
func (k *Keys) BufferSize() int {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
return len(k.buf)
|
||||
}
|
||||
|
||||
// PeekBuffer returns up to max bytes from the raw input buffer.
|
||||
func (k *Keys) PeekBuffer(max int) []byte {
|
||||
k.mutex.RLock()
|
||||
defer k.mutex.RUnlock()
|
||||
|
||||
if max <= 0 || len(k.buf) <= max {
|
||||
return append([]byte(nil), k.buf...)
|
||||
}
|
||||
|
||||
return append([]byte(nil), k.buf[:max]...)
|
||||
}
|
||||
|
||||
// DrainBuffer returns all buffered raw input bytes and clears the buffer.
|
||||
func (k *Keys) DrainBuffer() []byte {
|
||||
k.mutex.Lock()
|
||||
defer k.mutex.Unlock()
|
||||
|
||||
if len(k.buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := append([]byte(nil), k.buf...)
|
||||
k.buf = nil
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// PrependBuffer pushes raw input bytes back to the front of the buffer.
|
||||
func (k *Keys) PrependBuffer(keys []byte) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k.mutex.Lock()
|
||||
k.buf = append(keys, k.buf...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// AppendMatched appends keys to the matched buffer, so they can be recorded by macros.
|
||||
func (k *Keys) AppendMatched(keys ...rune) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k.mutex.Lock()
|
||||
k.matched = append(k.matched, keys...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetMatched replaces the matched buffer with the provided keys.
|
||||
func (k *Keys) SetMatched(keys ...rune) {
|
||||
k.mutex.Lock()
|
||||
if len(keys) == 0 {
|
||||
k.matched = nil
|
||||
k.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
k.matched = append(k.matched[:0], keys...)
|
||||
k.mutex.Unlock()
|
||||
}
|
||||
|
||||
// ReadInput reads raw input bytes from stdin, passing through the input filter.
|
||||
func (k *Keys) ReadInput() ([]byte, error) {
|
||||
return k.readInputFiltered()
|
||||
}
|
||||
|
||||
// ReadKey reads keys from stdin like Read(), but immediately
|
||||
// returns them instead of storing them in the stack, along with
|
||||
// an indication on whether this key is an escape/abort one.
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -344,7 +343,7 @@ func DisplayLine(l *Line, indent int) {
|
||||
line += term.NewlineReturn
|
||||
}
|
||||
|
||||
fmt.Print(line)
|
||||
term.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-21
@@ -1,7 +1,7 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/reeflective/readline/inputrc"
|
||||
"github.com/reeflective/readline/internal/color"
|
||||
@@ -32,6 +32,10 @@ type Engine struct {
|
||||
hintRows int
|
||||
compRows int
|
||||
primaryPrinted bool
|
||||
cursorKnown bool
|
||||
cursorDirty bool
|
||||
commentBegin string
|
||||
commentRegexp *regexp.Regexp
|
||||
|
||||
// UI components
|
||||
keys *core.Keys
|
||||
@@ -69,7 +73,10 @@ func Init(e *Engine, highlighter func([]rune) string) {
|
||||
// Refresh recomputes and redisplays the entire readline interface, except
|
||||
// the first lines of the primary prompt when the latter is a multiline one.
|
||||
func (e *Engine) Refresh() {
|
||||
fmt.Print(term.HideCursor)
|
||||
term.BeginBuffer()
|
||||
defer term.EndBuffer()
|
||||
|
||||
term.WriteString(term.HideCursor)
|
||||
|
||||
// Go back to the first column, and if the primary prompt
|
||||
// was not printed yet, back up to the line's beginning row.
|
||||
@@ -82,6 +89,12 @@ func (e *Engine) Refresh() {
|
||||
// Print either all or the last line of the prompt.
|
||||
e.prompt.LastPrint()
|
||||
|
||||
// Flush prompt output before querying the cursor position.
|
||||
if term.ShouldQueryCursorPos() {
|
||||
term.EndBuffer()
|
||||
term.BeginBuffer()
|
||||
}
|
||||
|
||||
// Get all positions required for the redisplay to come:
|
||||
// prompt end (thus indentation), cursor positions, etc.
|
||||
e.computeCoordinates(true)
|
||||
@@ -99,7 +112,7 @@ func (e *Engine) Refresh() {
|
||||
} else {
|
||||
e.lineEndToCursorPos()
|
||||
}
|
||||
fmt.Print(term.ShowCursor)
|
||||
term.WriteString(term.ShowCursor)
|
||||
}
|
||||
|
||||
// PrintPrimaryPrompt redraws the primary prompt.
|
||||
@@ -108,12 +121,13 @@ func (e *Engine) Refresh() {
|
||||
func (e *Engine) PrintPrimaryPrompt() {
|
||||
e.prompt.PrimaryPrint()
|
||||
e.primaryPrinted = true
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// ClearHelpers clears the hint and completion sections below the line.
|
||||
func (e *Engine) ClearHelpers() {
|
||||
e.CursorBelowLine()
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
term.MoveCursorUp(1)
|
||||
term.MoveCursorUp(e.lineRows)
|
||||
@@ -140,14 +154,15 @@ func (e *Engine) AcceptLine() {
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorDown(e.lineRows)
|
||||
term.MoveCursorForwards(e.lineCol)
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Reprint the right-side prompt if it's not a tooltip one.
|
||||
e.prompt.RightPrint(e.lineCol, false)
|
||||
|
||||
// Go below this non-suggested line and clear everything.
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// RefreshTransient goes back to the first line of the input buffer
|
||||
@@ -164,7 +179,8 @@ func (e *Engine) RefreshTransient() {
|
||||
// And redisplay the transient/primary/line.
|
||||
e.prompt.TransientPrint()
|
||||
e.displayLine()
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
e.MarkCursorDirty()
|
||||
}
|
||||
|
||||
// CursorToLineStart moves the cursor just after the primary prompt.
|
||||
@@ -183,7 +199,7 @@ func (e *Engine) CursorToLineStart() {
|
||||
func (e *Engine) CursorBelowLine() {
|
||||
term.MoveCursorUp(e.cursorRow)
|
||||
term.MoveCursorDown(e.lineRows)
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
}
|
||||
|
||||
// lineStartToCursorPos can be used if the cursor is currently
|
||||
@@ -202,6 +218,11 @@ func (e *Engine) cursorHintToLineStart() {
|
||||
e.CursorToLineStart()
|
||||
}
|
||||
|
||||
// MarkCursorDirty forces the next refresh to query the cursor position.
|
||||
func (e *Engine) MarkCursorDirty() {
|
||||
e.cursorDirty = true
|
||||
}
|
||||
|
||||
func (e *Engine) computeCoordinates(suggested bool) {
|
||||
// Get the new input line and auto-suggested one.
|
||||
e.line, e.cursor = e.completer.Line()
|
||||
@@ -211,17 +232,31 @@ func (e *Engine) computeCoordinates(suggested bool) {
|
||||
e.suggested = e.histories.Suggest(e.line)
|
||||
}
|
||||
|
||||
// Get the position of the line's beginning by querying
|
||||
// the terminal for the cursor position.
|
||||
e.startCols, e.startRows = e.keys.GetCursorPos()
|
||||
// Get the position of the line's beginning. When possible, reuse the
|
||||
// cached cursor row to avoid the expensive terminal query.
|
||||
if term.ShouldQueryCursorPos() && (!e.cursorKnown || e.cursorDirty) {
|
||||
e.startCols, e.startRows = e.keys.GetCursorPos()
|
||||
|
||||
if e.startCols > 0 {
|
||||
e.startCols--
|
||||
}
|
||||
if e.startCols > 0 {
|
||||
e.startCols--
|
||||
}
|
||||
|
||||
// Cursor position might be misleading if invalid (negative).
|
||||
if e.startCols == -1 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
// Cursor position might be misleading if invalid (negative).
|
||||
if e.startCols == -1 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
}
|
||||
|
||||
e.cursorKnown = true
|
||||
e.cursorDirty = false
|
||||
} else {
|
||||
e.startCols = e.prompt.LastCols()
|
||||
if e.startCols == 0 {
|
||||
e.startCols = e.prompt.LastUsed()
|
||||
}
|
||||
if !e.cursorKnown {
|
||||
e.startRows = 0
|
||||
e.cursorKnown = true
|
||||
}
|
||||
}
|
||||
|
||||
e.cursorCol, e.cursorRow = core.CoordinatesCursor(e.cursor, e.startCols)
|
||||
@@ -269,8 +304,8 @@ func (e *Engine) displayLine() {
|
||||
|
||||
// Adjust the cursor if the line fits exactly in the terminal width.
|
||||
if e.lineCol == 0 {
|
||||
fmt.Print(term.NewlineReturn)
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +345,7 @@ func (e *Engine) displayHelpers() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Print(term.NewlineReturn)
|
||||
term.WriteString(term.NewlineReturn)
|
||||
|
||||
prevHintRows := e.hintRows
|
||||
prevCompRows := e.compRows
|
||||
@@ -327,7 +362,7 @@ func (e *Engine) displayHelpers() bool {
|
||||
}
|
||||
|
||||
if e.hintRows+e.compRows < prevHintRows+prevCompRows {
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
}
|
||||
|
||||
// Go back to the first line below the input line.
|
||||
|
||||
+28
-15
@@ -19,29 +19,41 @@ func (e *Engine) highlightLine(line []rune, selection core.Selection) string {
|
||||
sorted := sortHighlights(selection)
|
||||
colors := e.getHighlights(line, sorted)
|
||||
|
||||
var highlighted string
|
||||
var highlighted strings.Builder
|
||||
|
||||
// And apply highlighting before each rune.
|
||||
for i, r := range line {
|
||||
if highlight, found := colors[i]; found {
|
||||
highlighted += string(highlight)
|
||||
highlighted.WriteString(highlight)
|
||||
}
|
||||
|
||||
highlighted += string(r)
|
||||
highlighted.WriteRune(r)
|
||||
}
|
||||
|
||||
// Finally, highlight comments using a regex.
|
||||
result := highlighted.String()
|
||||
|
||||
// Finally, highlight comments using a cached regex.
|
||||
comment := strings.Trim(e.opts.GetString("comment-begin"), "\"")
|
||||
commentPattern := fmt.Sprintf(`(^|\s)%s.*`, comment)
|
||||
if comment != e.commentBegin {
|
||||
e.commentBegin = comment
|
||||
e.commentRegexp = nil
|
||||
|
||||
if commentsMatch, err := regexp.Compile(commentPattern); err == nil {
|
||||
commentColor := color.SGRStart + color.Fg + "244" + color.SGREnd
|
||||
highlighted = commentsMatch.ReplaceAllString(highlighted, fmt.Sprintf("%s${0}%s", commentColor, color.Reset))
|
||||
if comment != "" {
|
||||
commentPattern := fmt.Sprintf(`(^|\s)%s.*`, comment)
|
||||
if commentsMatch, err := regexp.Compile(commentPattern); err == nil {
|
||||
e.commentRegexp = commentsMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlighted += color.Reset
|
||||
if e.commentRegexp != nil {
|
||||
commentColor := color.SGRStart + color.Fg + "244" + color.SGREnd
|
||||
result = e.commentRegexp.ReplaceAllString(result, fmt.Sprintf("%s${0}%s", commentColor, color.Reset))
|
||||
}
|
||||
|
||||
return highlighted
|
||||
result += color.Reset
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func sortHighlights(vhl core.Selection) []core.Selection {
|
||||
@@ -95,15 +107,16 @@ func sortHighlights(vhl core.Selection) []core.Selection {
|
||||
return sorted
|
||||
}
|
||||
|
||||
func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int][]rune {
|
||||
highlights := make(map[int][]rune)
|
||||
var colorSeqRegexp = regexp.MustCompile(`\x1b\[[0-9;]+m`)
|
||||
|
||||
func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int]string {
|
||||
highlights := make(map[int]string)
|
||||
|
||||
// Find any highlighting already applied on the line,
|
||||
// and keep the indexes so that we can skip those.
|
||||
var colors [][]int
|
||||
|
||||
colorMatch := regexp.MustCompile(`\x1b\[[0-9;]+m`)
|
||||
colors = colorMatch.FindAllStringIndex(string(line), -1)
|
||||
colors = colorSeqRegexp.FindAllStringIndex(string(line), -1)
|
||||
|
||||
// marks that started highlighting, but not done yet.
|
||||
regions := make([]core.Selection, 0)
|
||||
@@ -149,7 +162,7 @@ func (e *Engine) getHighlights(line []rune, sorted []core.Selection) map[int][]r
|
||||
// Add to the line, with the raw index since
|
||||
// we must take into account embedded colors.
|
||||
if len(posHl) > 0 {
|
||||
highlights[rawIndex] = posHl
|
||||
highlights[rawIndex] = string(posHl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-21
@@ -1,7 +1,6 @@
|
||||
package keymap
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/reeflective/readline/inputrc"
|
||||
@@ -180,32 +179,19 @@ func (m *Engine) matchBind(keys []byte, binds map[string]inputrc.Bind) (inputrc.
|
||||
var match inputrc.Bind
|
||||
var prefixed []inputrc.Bind
|
||||
|
||||
// Make a sorted list with all keys in the binds map.
|
||||
var sequences []string
|
||||
for sequence := range binds {
|
||||
sequences = append(sequences, sequence)
|
||||
}
|
||||
keysStr := string(keys)
|
||||
|
||||
// Sort the list of sequences by alphabetical order and length.
|
||||
sort.Slice(sequences, func(i, j int) bool {
|
||||
if len(sequences[i]) == len(sequences[j]) {
|
||||
return sequences[i] < sequences[j]
|
||||
}
|
||||
|
||||
return len(sequences[i]) < len(sequences[j])
|
||||
})
|
||||
|
||||
// Iterate over the sorted list of sequences and find all binds
|
||||
// Iterate over the list of sequences and find all binds
|
||||
// that match the sequence either by prefix or exactly.
|
||||
for _, sequence := range sequences {
|
||||
for sequence, bind := range binds {
|
||||
seq := strutil.ConvertMeta([]rune(sequence))
|
||||
|
||||
if len(string(keys)) < len(seq) && strings.HasPrefix(seq, string(keys)) {
|
||||
prefixed = append(prefixed, binds[sequence])
|
||||
if len(keysStr) < len(seq) && strings.HasPrefix(seq, keysStr) {
|
||||
prefixed = append(prefixed, bind)
|
||||
}
|
||||
|
||||
if string(keys) == seq {
|
||||
match = binds[sequence]
|
||||
if keysStr == seq {
|
||||
match = bind
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -11,11 +11,13 @@ const (
|
||||
ClearScreen = "\x1b[2J" // Clears screen, preserving scroll buffer
|
||||
ClearDisplay = "\x1b[3J" // Clears screen fully, wipes the scroll buffer
|
||||
|
||||
CursorTopLeft = "\x1b[H"
|
||||
SaveCursorPos = "\x1b7"
|
||||
RestoreCursorPos = "\x1b8"
|
||||
HideCursor = "\x1b[?25l"
|
||||
ShowCursor = "\x1b[?25h"
|
||||
CursorTopLeft = "\x1b[H"
|
||||
SaveCursorPos = "\x1b7"
|
||||
RestoreCursorPos = "\x1b8"
|
||||
HideCursor = "\x1b[?25l"
|
||||
ShowCursor = "\x1b[?25h"
|
||||
BracketedPasteEnable = "\x1b[?2004h"
|
||||
BracketedPasteDisable = "\x1b[?2004l"
|
||||
)
|
||||
|
||||
// Some core keys needed by some stuff.
|
||||
|
||||
+80
-4
@@ -1,8 +1,11 @@
|
||||
package term
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Those variables are very important to realine low-level code: all virtual terminal
|
||||
@@ -16,13 +19,19 @@ var (
|
||||
|
||||
func init() {
|
||||
stdoutTerm = os.Stdout
|
||||
stdoutTerm = os.Stderr
|
||||
stderrTerm = os.Stdin
|
||||
stdinTerm = os.Stdin
|
||||
stderrTerm = os.Stderr
|
||||
}
|
||||
|
||||
// fallback terminal width when we can't get it through query.
|
||||
var defaultTermWidth = 80
|
||||
|
||||
var (
|
||||
outputMu sync.Mutex
|
||||
outputDepth int
|
||||
outputBuf *bufio.Writer
|
||||
)
|
||||
|
||||
// GetWidth returns the width of Stdout or 80 if the width cannot be established.
|
||||
func GetWidth() (termWidth int) {
|
||||
var err error
|
||||
@@ -50,6 +59,73 @@ func GetLength() int {
|
||||
}
|
||||
|
||||
func printf(format string, a ...interface{}) {
|
||||
s := fmt.Sprintf(format, a...)
|
||||
fmt.Print(s)
|
||||
Printf(format, a...)
|
||||
}
|
||||
|
||||
// BeginBuffer enables buffered terminal writes until EndBuffer is called.
|
||||
func BeginBuffer() {
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
outputDepth++
|
||||
if outputDepth == 1 {
|
||||
outputBuf = bufio.NewWriterSize(stdoutTerm, 64*1024)
|
||||
}
|
||||
}
|
||||
|
||||
// EndBuffer flushes buffered terminal writes when leaving the outermost buffer.
|
||||
func EndBuffer() {
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
if outputDepth == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
outputDepth--
|
||||
if outputDepth == 0 && outputBuf != nil {
|
||||
_ = outputBuf.Flush()
|
||||
outputBuf = nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteString writes to the terminal, using the buffer when enabled.
|
||||
func WriteString(s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
outputMu.Lock()
|
||||
defer outputMu.Unlock()
|
||||
|
||||
if outputDepth > 0 && outputBuf != nil {
|
||||
_, _ = outputBuf.WriteString(s)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = stdoutTerm.WriteString(s)
|
||||
}
|
||||
|
||||
// Printf formats and writes to the terminal, using the buffer when enabled.
|
||||
func Printf(format string, a ...interface{}) {
|
||||
WriteString(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// ShouldQueryCursorPos reports whether querying the terminal cursor position is allowed.
|
||||
// Some terminals respond slowly or cause visible flicker when queried frequently.
|
||||
func ShouldQueryCursorPos() bool {
|
||||
if v := strings.TrimSpace(os.Getenv("READLINE_CURSOR_POS")); v != "" {
|
||||
switch strings.ToLower(v) {
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("TERM_PROGRAM") == "iTerm.app" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/reeflective/readline/internal/color"
|
||||
@@ -78,7 +77,7 @@ func DisplayHint(hint *Hint) {
|
||||
|
||||
if len(hint.text) == 0 && len(hint.persistent) == 0 {
|
||||
if hint.cleanup {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
|
||||
hint.cleanup = false
|
||||
@@ -95,7 +94,7 @@ func DisplayHint(hint *Hint) {
|
||||
text += term.ClearLineAfter + color.Reset
|
||||
|
||||
if len(text) > 0 {
|
||||
fmt.Print(text)
|
||||
term.WriteString(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-18
@@ -20,9 +20,10 @@ const (
|
||||
// Prompt stores all prompt rendering/generation functions and is
|
||||
// in charge of displaying them, as well as computing their offsets.
|
||||
type Prompt struct {
|
||||
primaryF func() string
|
||||
primaryRows int
|
||||
primaryCols int
|
||||
primaryF func() string
|
||||
primaryRows int
|
||||
primaryCols int
|
||||
primaryColsRaw int
|
||||
|
||||
secondaryF func() string
|
||||
transientF func() string
|
||||
@@ -108,14 +109,15 @@ func (p *Prompt) PrimaryPrint() {
|
||||
|
||||
// Print the various lines.
|
||||
if prompt != "" {
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
}
|
||||
|
||||
fmt.Print(lastPrompt)
|
||||
term.WriteString(lastPrompt)
|
||||
|
||||
// And compute coordinates
|
||||
p.primaryRows = strings.Count(prompt, "\n")
|
||||
p.primaryCols = strutil.RealLength(lastPrompt)
|
||||
p.primaryColsRaw = strutil.RealLength(lastPrompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
@@ -149,9 +151,10 @@ func (p *Prompt) LastPrint() {
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
|
||||
p.primaryCols = strutil.RealLength(prompt)
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
}
|
||||
@@ -175,7 +178,8 @@ func (p *Prompt) LastUsed() int {
|
||||
}
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
p.primaryCols = strutil.RealLength(prompt)
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
p.primaryCols = p.primaryColsRaw
|
||||
|
||||
if p.primaryCols > 0 {
|
||||
p.primaryCols--
|
||||
@@ -184,15 +188,36 @@ func (p *Prompt) LastUsed() int {
|
||||
return p.primaryCols
|
||||
}
|
||||
|
||||
// LastCols returns the number of terminal columns used by the last prompt line.
|
||||
func (p *Prompt) LastCols() int {
|
||||
if p.primaryF == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if p.primaryColsRaw != 0 {
|
||||
return p.primaryColsRaw
|
||||
}
|
||||
|
||||
lines := strings.Split(p.primaryF(), "\n")
|
||||
if len(lines) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt := p.formatLastPrompt(lines[len(lines)-1])
|
||||
p.primaryColsRaw = strutil.RealLength(prompt)
|
||||
|
||||
return p.primaryColsRaw
|
||||
}
|
||||
|
||||
// SecondaryPrint prints the last cursor in secondary prompt mode,
|
||||
// which is always activated when the current input line is a multiline one.
|
||||
func (p *Prompt) SecondaryPrint() {
|
||||
if p.secondaryF != nil {
|
||||
fmt.Print(p.secondaryF())
|
||||
term.WriteString(p.secondaryF())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Print(secondaryPromptDefault)
|
||||
term.WriteString(secondaryPromptDefault)
|
||||
}
|
||||
|
||||
// MultilineColumnPrint prints the multiline editor column status indicator.
|
||||
@@ -209,7 +234,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += fmt.Sprintf("\n\x1b[1;30m%d\x1b[0m", pos+2)
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
|
||||
case len(custom) > 0:
|
||||
column := ""
|
||||
@@ -217,7 +242,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += fmt.Sprintf("\n%s\x1b[0m", custom)
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
|
||||
case defaultCol:
|
||||
column := ""
|
||||
@@ -225,7 +250,7 @@ func (p *Prompt) MultilineColumnPrint() {
|
||||
column += "\n" + multilineColumnDefault
|
||||
}
|
||||
|
||||
fmt.Print(column)
|
||||
term.WriteString(column)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +274,9 @@ func (p *Prompt) RightPrint(startColumn int, force bool) {
|
||||
}
|
||||
|
||||
if prompt, canPrint := p.formatRightPrompt(rprompt, startColumn); canPrint {
|
||||
fmt.Print(prompt)
|
||||
term.WriteString(prompt)
|
||||
} else {
|
||||
fmt.Print(term.ClearLineAfter)
|
||||
term.WriteString(term.ClearLineAfter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +289,10 @@ func (p *Prompt) TransientPrint() {
|
||||
// Clean everything below where the prompt will be printed.
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorUp(p.primaryRows)
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// And print the prompt
|
||||
fmt.Print(p.transientF())
|
||||
term.WriteString(p.transientF())
|
||||
}
|
||||
|
||||
// Refreshing returns true if the prompt is currently redisplaying
|
||||
|
||||
+3
-1
@@ -56,11 +56,13 @@ func (rl *Shell) Readline() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
defer term.Restore(descriptor, state)
|
||||
defer term.WriteString(term.BracketedPasteDisable)
|
||||
term.WriteString(term.BracketedPasteEnable)
|
||||
|
||||
// Prompts and cursor styles
|
||||
rl.Display.PrintPrimaryPrompt()
|
||||
defer rl.Display.RefreshTransient()
|
||||
defer fmt.Print(keymap.CursorStyle("default"))
|
||||
defer term.WriteString(keymap.CursorStyle("default").String())
|
||||
|
||||
rl.init()
|
||||
|
||||
|
||||
+4
-4
@@ -145,13 +145,13 @@ func (rl *Shell) Printf(msg string, args ...any) (n int, err error) {
|
||||
// and clear everything below (hints and completions).
|
||||
rl.Display.CursorBelowLine()
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Skip a line, and print the formatted message.
|
||||
n, err = fmt.Printf(msg+"\n", args...)
|
||||
|
||||
// Redisplay the prompt, input line and active helpers.
|
||||
rl.Prompt.PrimaryPrint()
|
||||
rl.Display.PrintPrimaryPrompt()
|
||||
rl.Display.Refresh()
|
||||
|
||||
return
|
||||
@@ -165,13 +165,13 @@ func (rl *Shell) PrintTransientf(msg string, args ...any) (n int, err error) {
|
||||
rl.Display.CursorToLineStart()
|
||||
term.MoveCursorBackwards(term.GetWidth())
|
||||
term.MoveCursorUp(rl.Prompt.PrimaryUsed())
|
||||
fmt.Print(term.ClearScreenBelow)
|
||||
term.WriteString(term.ClearScreenBelow)
|
||||
|
||||
// Print the logged message.
|
||||
n, err = fmt.Printf(msg+"\n", args...)
|
||||
|
||||
// Redisplay the prompt, input line and active helpers.
|
||||
rl.Prompt.PrimaryPrint()
|
||||
rl.Display.PrintPrimaryPrompt()
|
||||
rl.Display.Refresh()
|
||||
|
||||
return
|
||||
|
||||
Vendored
+6
-5
@@ -540,7 +540,7 @@ github.com/moloch--/go-keystone
|
||||
# github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e
|
||||
## explicit; go 1.17
|
||||
github.com/moloch--/memmod
|
||||
# github.com/moloch--/sgn v0.0.4
|
||||
# github.com/moloch--/sgn v0.0.5
|
||||
## explicit; go 1.23
|
||||
github.com/moloch--/sgn/pkg
|
||||
# github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
@@ -581,15 +581,15 @@ github.com/pmezard/go-difflib/difflib
|
||||
# github.com/prometheus-community/pro-bing v0.4.0
|
||||
## explicit; go 1.19
|
||||
github.com/prometheus-community/pro-bing
|
||||
# github.com/reeflective/console v0.1.25
|
||||
## explicit; go 1.23.6
|
||||
# github.com/reeflective/console v0.1.25 => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
|
||||
## explicit; go 1.24.0
|
||||
github.com/reeflective/console
|
||||
github.com/reeflective/console/commands/readline
|
||||
github.com/reeflective/console/internal/completion
|
||||
github.com/reeflective/console/internal/line
|
||||
github.com/reeflective/console/internal/strutil
|
||||
github.com/reeflective/console/internal/ui
|
||||
# github.com/reeflective/readline v1.1.4 => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
# github.com/reeflective/readline v1.1.4 => github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a
|
||||
## explicit; go 1.23.6
|
||||
github.com/reeflective/readline
|
||||
github.com/reeflective/readline/inputrc
|
||||
@@ -1397,4 +1397,5 @@ tailscale.com/wgengine/wgcfg/nmcfg
|
||||
tailscale.com/wgengine/wgint
|
||||
tailscale.com/wgengine/wglog
|
||||
# github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
|
||||
# github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260104185423-d3ac1d6c561d
|
||||
# github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a
|
||||
# github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
|
||||
Reference in new issue
Block a user