Compare commits

...
Author SHA1 Message Date
moloch-- f65f98bf65 Improve websites content-type sniffing issue #2033 2026-01-28 18:14:27 -08:00
moloch-- cee2cc76c4 Convert tui-settings to yaml 2026-01-28 18:06:41 -08:00
moloch-- daa33b6301 Convert server configs to yaml syntax, with backwards compatibility 2026-01-28 17:59:34 -08:00
moloch-- ef52973536 Convert configs to yml 2026-01-28 17:39:14 -08:00
moloch-- 6a7bd0803a Update --save help 2026-01-28 17:21:35 -08:00
moloch-- 6c70c1c1e6 Add signature validation logic 2026-01-28 17:20:24 -08:00
moloch-- ddc222b8a2 Improvements to update 2026-01-28 17:07:47 -08:00
moloch-- 5506a582ac Add SliverPublicKey 2026-01-28 16:45:32 -08:00
Joe 63d872c98b Merge pull request #2142 from BishopFox/fix/sgn
Fix/sgn
2026-01-28 17:31:46 -07:00
moloch-- ef9f5dde0d Fix unit tests 2026-01-28 07:21:38 -08:00
moloch-- e35b7fdb97 Update sgn to v0.0.5 2026-01-27 21:28:21 -08:00
moloch-- efbbf382eb Merge branch 'master' into fix/sgn 2026-01-27 21:26:42 -08:00
moloch-- 0b8d00900d v1.6.7 2026-01-27 21:06:39 -08:00
moloch-- 9077596f25 Damn it Claude 2026-01-27 21:04:30 -08:00
moloch-- 0a8104a32e Damn it Claude 2026-01-27 21:03:25 -08:00
moloch-- 15a0ba4c1e Damn it Claude 2026-01-27 21:01:55 -08:00
moloch-- d452c8a676 Write .bin even if sgn fails 2026-01-27 20:49:28 -08:00
Joe fb74b491fa Merge pull request #2141 from BishopFox/fix/console-inputs
Update readline/console deps
2026-01-27 21:38:20 -07:00
moloch-- 2af6e11adc Update readline/console deps 2026-01-27 19:55:18 -08:00
Joe a3a98858fd Merge pull request #2135 from BishopFox/fix/sqlite
Add sqlite pragmas to db config
2026-01-21 22:28:56 -07:00
42 changed files with 1900 additions and 309 deletions

No files matched your search

+3
View File
@@ -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)"
+1
View File
@@ -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
View File
@@ -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()
}
+27
View File
@@ -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
View File
@@ -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
}
+115
View File
@@ -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)
}
}
+7
View File
@@ -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
+6 -1
View File
@@ -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
}
+2 -1
View File
@@ -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
View File
@@ -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)
}
+9
View File
@@ -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
View File
@@ -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
}
+5 -3
View File
@@ -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-20260128035102-9f373ad18989
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260128035037-680800942770
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
+6 -12
View File
@@ -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-20260128035037-680800942770 h1:JtfE7Od0O5ISD4b9r9AX7FTlAf8sT6O4qr5HV6Ey5U0=
github.com/moloch--/console v0.0.0-20260128035037-680800942770/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-20260128035102-9f373ad18989 h1:g0EfxAWzDGYarN0RAPibsjz2M/f0pAqYNiEpCPWRDok=
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989/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=
+1
View File
@@ -136,6 +136,7 @@ var rootCmd = &cobra.Command{
cryptography.AgeServerKeyPair()
cryptography.MinisignServerPrivateKey()
c2.SetupDefaultC2Profiles()
_, _ = configs.LoadCrackConfig()
serverConfig := configs.GetServerConfig()
listenerJobs, err := db.ListenerJobs()
+2
View File
@@ -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
View File
@@ -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
}
+101
View File
@@ -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
View File
@@ -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
}
+129
View File
@@ -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)
}
}
+27
View File
@@ -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
View File
@@ -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",
+194
View File
@@ -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)
}
}
+6 -1
View File
@@ -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,
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+78 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+2 -2
View File
@@ -145,7 +145,7 @@ 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...)
@@ -165,7 +165,7 @@ 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...)
+6 -5
View File
@@ -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-20260128035037-680800942770
## 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-20260128035102-9f373ad18989
## 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-20260128035102-9f373ad18989
# github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260128035037-680800942770