Compare commits

...
16 Commits
Author SHA1 Message Date
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
28 changed files with 987 additions and 182 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.
+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)
}
}
+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=
+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