Compare commits

...
Author SHA1 Message Date
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
moloch-- d0652f2fd3 Add sqlite pragmas to db config 2026-01-21 16:08:07 -08:00
Joe 936f067363 Merge pull request #2130 from BishopFox/feature/website-info
Feature/website info
2026-01-21 10:25:56 -07:00
moloch-- 041c3c7048 Improve tab completer for websites cmd 2026-01-20 21:29:39 -08:00
moloch-- b48f74af58 Add original file name and sha256 to websites cmd 2026-01-20 19:13:42 -08:00
moloch-- 6ec8849b20 v1.6.6 2026-01-16 09:18:39 -08:00
Joe db7cf1ed62 Merge pull request #2125 from BishopFox/fix/winlsdir
Improved `err` and `nil` checks in `dirListHandler`
2026-01-16 10:17:01 -07:00
moloch-- 16e1e77068 Implement better err and nil checks in dirListHandler issue #2122 2026-01-16 07:06:43 -08:00
moloch-- 1cfa694aa2 Merge branch 'update/deps' into tui/adv-forms 2026-01-16 06:57:06 -08:00
moloch-- 5898877136 Fix --rc scripts 2026-01-16 06:56:41 -08:00
moloch-- a7d8cac565 Update generate form 2026-01-16 06:41:58 -08:00
39 changed files with 1952 additions and 1253 deletions

No files matched your search

-1
View File
@@ -43,4 +43,3 @@ 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.
+1 -1
View File
@@ -32,7 +32,7 @@ func PrintCanaries(con *console.SliverClient, canaries []*clientpb.DNSCanary, bu
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Sliver Name",
"Implant Name",
"Domain",
"Triggered",
"First Trigger",
+2 -1
View File
@@ -22,7 +22,8 @@ var (
// GenerateBeaconCmd - The main command used to generate implant binaries
func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateBeaconForm(cmd, con, args) {
result, err := forms.GenerateBeaconForm()
compiler, _ := compilerTargets(con)
result, err := forms.GenerateBeaconForm(compiler)
if err != nil {
if errors.Is(err, forms.ErrUserAborted) {
return
+6 -4
View File
@@ -91,7 +91,8 @@ var (
// GenerateCmd - The main command used to generate implant binaries
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateForm(cmd, con, args) {
result, err := forms.GenerateForm()
compiler, _ := compilerTargets(con)
result, err := forms.GenerateForm(compiler)
if err != nil {
if errors.Is(err, forms.ErrUserAborted) {
return
@@ -118,14 +119,15 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
} else {
_, err := externalBuild(name, config, save, con)
if err != nil {
if err == ErrNoExternalBuilder {
switch err {
case ErrNoExternalBuilder:
con.PrintErrorf("There are no external builders currently connected to the server\n")
con.PrintErrorf("See 'builders' command for more information\n")
} else if err == ErrNoValidBuilders {
case ErrNoValidBuilders:
con.PrintErrorf("There are external builders connected to the server, but none can build the target you specified\n")
con.PrintErrorf("Invalid target %s\n", fmt.Sprintf("%s:%s/%s", config.Format, config.GOOS, config.GOARCH))
con.PrintErrorf("See 'builders' command for more information\n")
} else {
default:
con.PrintErrorf("%s\n", err)
}
return
+1 -1
View File
@@ -124,7 +124,7 @@ func PrintImplantBuilds(builds *clientpb.ImplantBuilds, filters ImplantBuildFilt
config.Format,
strings.Join(c2URLs, "\n"),
fmt.Sprintf("%v", config.Debug),
fmt.Sprintf(config.HTTPC2ConfigName),
fmt.Sprintf("%s", config.HTTPC2ConfigName),
fmt.Sprintf("%v", builds.ResourceIDs[sliverName].Value),
fmt.Sprintf("%v", builds.Staged[sliverName]),
})
+39
View File
@@ -1,10 +1,14 @@
package websites
import (
"context"
"strings"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -26,6 +30,41 @@ func Commands(con *console.SliverClient) []*cobra.Command {
})
carapace.Gen(websitesCmd).PositionalCompletion(WebsiteNameCompleter(con))
websitesListCmd := &cobra.Command{
Use: consts.ListStr,
Short: "List configured websites",
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.ListStr}),
Run: func(cmd *cobra.Command, args []string) {
ListWebsites(cmd, con, args)
},
}
websitesCmd.AddCommand(websitesListCmd)
websitesShowCmd := &cobra.Command{
Use: "show [name]",
Short: "Show contents of a website",
Long: help.GetHelpFor([]string{consts.WebsitesStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ListWebsiteContent(args[0], con)
},
}
carapace.Gen(websitesShowCmd).PositionalCompletion(WebsiteNameCompleter(con))
websitesShowCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ws, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
out := []string{}
for _, w := range ws.Websites {
if strings.HasPrefix(w.Name, toComplete) {
out = append(out, w.Name)
}
}
return out, cobra.ShellCompDirectiveNoFileComp
}
websitesCmd.AddCommand(websitesShowCmd)
websitesRmCmd := &cobra.Command{
Use: consts.RmStr,
Short: "Remove an entire website and all of its contents",
@@ -121,9 +121,10 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
}
web.Contents[webpath] = &clientpb.WebContent{
Path: webpath,
ContentType: contentType,
Content: data,
Path: webpath,
ContentType: contentType,
OriginalFile: filepath.Base(contentPath),
Content: data,
}
return nil
}
+10 -4
View File
@@ -21,7 +21,6 @@ package websites
import (
"context"
"sort"
"strings"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
@@ -58,11 +57,14 @@ func ListWebsites(cmd *cobra.Command, con *console.SliverClient, args []string)
con.PrintInfof("No websites\n")
return
}
con.Println("Websites")
con.Println(strings.Repeat("=", len("Websites")))
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{"Name", "Objects"})
for _, site := range websites.Websites {
con.Printf("%s%s%s - %d page(s)\n", console.Bold, site.Name, console.Normal, len(site.Contents))
tw.AppendRow(table.Row{site.Name, len(site.Contents)})
}
con.Println(tw.Render())
}
// ListWebsiteContent - List the static contents of a website.
@@ -91,6 +93,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
"Path",
"Content-type",
"Size",
"Original File",
"SHA256",
})
sortedContents := []*clientpb.WebContent{}
for _, content := range web.Contents {
@@ -104,6 +108,8 @@ func PrintWebsite(web *clientpb.Website, con *console.SliverClient) {
content.Path,
content.ContentType,
content.Size,
content.OriginalFile,
content.Sha256,
})
}
con.Println(tw.Render())
+18 -7
View File
@@ -263,6 +263,19 @@ func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line
return fmt.Errorf("parse error: %w", err)
}
if con.serverCmds == nil {
con.serverCmds = serverCmds
}
if con.sliverCmds == nil {
con.sliverCmds = sliverCmds
}
menu := con.App.ActiveMenu()
if menu != nil {
// Reset per line to avoid stale roots when rc scripts switch menus.
menu.Command = nil
}
for _, hook := range con.App.PreCmdRunLineHooks {
args, err = hook(args)
if err != nil {
@@ -274,15 +287,13 @@ func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line
return nil
}
if con.serverCmds == nil {
con.serverCmds = serverCmds
menu = con.App.ActiveMenu()
if menu == nil {
return fmt.Errorf("no active menu")
}
if con.sliverCmds == nil {
con.sliverCmds = sliverCmds
if menu.Command == nil {
con.setMenuCommand(menu, args)
}
menu := con.App.ActiveMenu()
con.setMenuCommand(menu, args)
if menu.Command == nil {
return fmt.Errorf("no commands available")
}
+30 -34
View File
@@ -4,6 +4,7 @@ import (
"errors"
"strings"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/util"
"github.com/charmbracelet/huh"
)
@@ -23,62 +24,57 @@ type GenerateFormResult struct {
}
// GenerateForm prompts for core generate flags and returns the collected values.
func GenerateForm() (*GenerateFormResult, error) {
func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
result := &GenerateFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
}
commonPlatformsOnly := true
lastCommonPlatformsOnly := commonPlatformsOnly
compilerTargets := compilerTargetList(compiler)
targetBindings := &targetOptionBindings{
Common: &commonPlatformsOnly,
OS: &result.OS,
Arch: &result.Arch,
}
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Common Platforms Only").
Value(&commonPlatformsOnly),
huh.NewSelect[string]().
Title("Target operating system").
Options(
huh.NewOption("Windows", "windows"),
huh.NewOption("Linux", "linux"),
huh.NewOption("macOS", "darwin"),
).
OptionsFunc(func() []huh.Option[string] {
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonOSOptions()
}
return osOptionsFromTargets(compilerTargets)
}, targetBindings).
Value(&result.OS),
huh.NewSelect[string]().
Title("CPU architecture").
OptionsFunc(func() []huh.Option[string] {
switch result.OS {
case "darwin":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("arm64", "arm64"),
}
case "linux":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
default:
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonArchOptions(result.OS)
}
}, &result.OS).
return archOptionsFromTargets(compilerTargets, result.OS)
}, targetBindings).
Height(3).
Value(&result.Arch),
huh.NewSelect[string]().
Title("Output format").
OptionsFunc(func() []huh.Option[string] {
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonFormatOptions(result.OS)
}
if result.OS == "windows" {
options = append(options,
huh.NewOption("Service", "service"),
huh.NewOption("Shellcode", "shellcode"),
)
}
return options
}, &result.OS).
return formatOptionsFromTargets(compilerTargets, result.OS, result.Arch)
}, targetBindings).
Height(3).
Value(&result.Format),
),
+30 -34
View File
@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/util"
"github.com/charmbracelet/huh"
)
@@ -26,62 +27,57 @@ type GenerateBeaconFormResult struct {
}
// GenerateBeaconForm prompts for generate beacon flags and returns the collected values.
func GenerateBeaconForm() (*GenerateBeaconFormResult, error) {
func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult, error) {
result := &GenerateBeaconFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
}
commonPlatformsOnly := true
lastCommonPlatformsOnly := commonPlatformsOnly
compilerTargets := compilerTargetList(compiler)
targetBindings := &targetOptionBindings{
Common: &commonPlatformsOnly,
OS: &result.OS,
Arch: &result.Arch,
}
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Common Platforms Only").
Value(&commonPlatformsOnly),
huh.NewSelect[string]().
Title("Target operating system").
Options(
huh.NewOption("Windows", "windows"),
huh.NewOption("Linux", "linux"),
huh.NewOption("macOS", "darwin"),
).
OptionsFunc(func() []huh.Option[string] {
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonOSOptions()
}
return osOptionsFromTargets(compilerTargets)
}, targetBindings).
Value(&result.OS),
huh.NewSelect[string]().
Title("CPU architecture").
OptionsFunc(func() []huh.Option[string] {
switch result.OS {
case "darwin":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("arm64", "arm64"),
}
case "linux":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
default:
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonArchOptions(result.OS)
}
}, &result.OS).
return archOptionsFromTargets(compilerTargets, result.OS)
}, targetBindings).
Height(3).
Value(&result.Arch),
huh.NewSelect[string]().
Title("Output format").
OptionsFunc(func() []huh.Option[string] {
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
maybeResetTargets(commonPlatformsOnly, &lastCommonPlatformsOnly, compilerTargets, &result.OS, &result.Arch, &result.Format)
if commonPlatformsOnly || len(compilerTargets) == 0 {
return commonFormatOptions(result.OS)
}
if result.OS == "windows" {
options = append(options,
huh.NewOption("Service", "service"),
huh.NewOption("Shellcode", "shellcode"),
)
}
return options
}, &result.OS).
return formatOptionsFromTargets(compilerTargets, result.OS, result.Arch)
}, targetBindings).
Height(3).
Value(&result.Format),
),
+220
View File
@@ -0,0 +1,220 @@
package forms
import (
"sort"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/charmbracelet/huh"
)
type formatOption struct {
format clientpb.OutputFormat
label string
value string
}
type targetOptionBindings struct {
Common *bool
OS *string
Arch *string
}
var formatOptionOrder = []formatOption{
{format: clientpb.OutputFormat_EXECUTABLE, label: "Executable", value: "exe"},
{format: clientpb.OutputFormat_SHARED_LIB, label: "Shared library", value: "shared"},
{format: clientpb.OutputFormat_SERVICE, label: "Service", value: "service"},
{format: clientpb.OutputFormat_SHELLCODE, label: "Shellcode", value: "shellcode"},
}
func commonOSOptions() []huh.Option[string] {
return []huh.Option[string]{
huh.NewOption("Windows", "windows"),
huh.NewOption("Linux", "linux"),
huh.NewOption("macOS", "darwin"),
}
}
func commonArchOptions(goos string) []huh.Option[string] {
switch goos {
case "darwin":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("arm64", "arm64"),
}
case "linux":
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
default:
return []huh.Option[string]{
huh.NewOption("amd64", "amd64"),
huh.NewOption("386", "386"),
}
}
}
func commonFormatOptions(goos string) []huh.Option[string] {
options := []huh.Option[string]{
huh.NewOption("Executable", "exe"),
huh.NewOption("Shared library", "shared"),
}
if goos == "windows" {
options = append(options,
huh.NewOption("Service", "service"),
huh.NewOption("Shellcode", "shellcode"),
)
}
return options
}
func firstOptionValue(options []huh.Option[string]) (string, bool) {
if len(options) == 0 {
return "", false
}
return options[0].Value, true
}
func maybeResetTargets(commonPlatformsOnly bool, lastCommonPlatformsOnly *bool, compilerTargets []*clientpb.CompilerTarget, goos, goarch, format *string) {
if lastCommonPlatformsOnly == nil || goos == nil || goarch == nil || format == nil {
return
}
if commonPlatformsOnly == *lastCommonPlatformsOnly {
return
}
*lastCommonPlatformsOnly = commonPlatformsOnly
if commonPlatformsOnly {
return
}
osOptions := commonOSOptions()
if len(compilerTargets) > 0 {
osOptions = osOptionsFromTargets(compilerTargets)
if len(osOptions) == 0 {
osOptions = commonOSOptions()
}
}
if value, ok := firstOptionValue(osOptions); ok {
*goos = value
}
archOptions := commonArchOptions(*goos)
if len(compilerTargets) > 0 {
archOptions = archOptionsFromTargets(compilerTargets, *goos)
if len(archOptions) == 0 {
archOptions = commonArchOptions(*goos)
}
}
if value, ok := firstOptionValue(archOptions); ok {
*goarch = value
}
formatOptions := commonFormatOptions(*goos)
if len(compilerTargets) > 0 {
formatOptions = formatOptionsFromTargets(compilerTargets, *goos, *goarch)
if len(formatOptions) == 0 {
formatOptions = commonFormatOptions(*goos)
}
}
if value, ok := firstOptionValue(formatOptions); ok {
*format = value
}
}
func compilerTargetList(compiler *clientpb.Compiler) []*clientpb.CompilerTarget {
if compiler == nil {
return nil
}
targets := make([]*clientpb.CompilerTarget, 0, len(compiler.Targets)+len(compiler.UnsupportedTargets))
targets = append(targets, compiler.Targets...)
targets = append(targets, compiler.UnsupportedTargets...)
return targets
}
func osOptionsFromTargets(targets []*clientpb.CompilerTarget) []huh.Option[string] {
if len(targets) == 0 {
return nil
}
seen := map[string]struct{}{}
for _, target := range targets {
if target.GOOS == "" {
continue
}
seen[target.GOOS] = struct{}{}
}
values := make([]string, 0, len(seen))
for value := range seen {
values = append(values, value)
}
sort.Strings(values)
options := make([]huh.Option[string], 0, len(values))
for _, value := range values {
options = append(options, huh.NewOption(osLabel(value), value))
}
return options
}
func archOptionsFromTargets(targets []*clientpb.CompilerTarget, goos string) []huh.Option[string] {
if len(targets) == 0 {
return nil
}
seen := map[string]struct{}{}
for _, target := range targets {
if goos != "" && target.GOOS != goos {
continue
}
if target.GOARCH == "" {
continue
}
seen[target.GOARCH] = struct{}{}
}
values := make([]string, 0, len(seen))
for value := range seen {
values = append(values, value)
}
sort.Strings(values)
options := make([]huh.Option[string], 0, len(values))
for _, value := range values {
options = append(options, huh.NewOption(value, value))
}
return options
}
func formatOptionsFromTargets(targets []*clientpb.CompilerTarget, goos, goarch string) []huh.Option[string] {
if len(targets) == 0 {
return nil
}
available := map[clientpb.OutputFormat]struct{}{}
for _, target := range targets {
if goos != "" && target.GOOS != goos {
continue
}
if goarch != "" && target.GOARCH != goarch {
continue
}
available[target.Format] = struct{}{}
}
options := make([]huh.Option[string], 0, len(available))
for _, option := range formatOptionOrder {
if _, ok := available[option.format]; ok {
options = append(options, huh.NewOption(option.label, option.value))
}
}
return options
}
func osLabel(value string) string {
switch value {
case "windows":
return "Windows"
case "linux":
return "Linux"
case "darwin":
return "macOS"
default:
return value
}
}
+4 -2
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
@@ -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
+4 -10
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,12 +350,14 @@ 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--/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.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -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=
+26 -17
View File
@@ -166,10 +166,14 @@ func dirListHandler(data []byte, resp RPCResponse) {
dirList.Exists = true
} else {
dirList.Exists = false
dirList.Response = &commonpb.Response{Err: err.Error()}
}
dirList.Files = []*sliverpb.FileInfo{}
rootDirInfo, err := rootDirEntry.Info()
if err == nil && filter == "" {
var rootDirInfo fs.FileInfo
if rootDirEntry != nil {
rootDirInfo, err = rootDirEntry.Info()
}
if err == nil && rootDirInfo != nil && filter == "" {
// We should not get an error because we created the DirEntry object from the FileInfo object
dirList.Files = append(dirList.Files, &sliverpb.FileInfo{
Name: ".", // Cannot use the name from the FileInfo / DirEntry because that is the name of the directory
@@ -193,6 +197,7 @@ func dirListHandler(data []byte, resp RPCResponse) {
if err != nil {
// Then this is a bad filter, and it will be a bad filter
// on every iteration of the loop, so we might as well break now
dirList.Response = &commonpb.Response{Err: err.Error()}
break
}
}
@@ -249,22 +254,26 @@ func getDirList(target string) (string, fs.DirEntry, []fs.DirEntry, error) {
// {{end}}
return "", nil, nil, err
}
if rootInfo, err := os.Stat(dir); !os.IsNotExist(err) {
/*
We could place the entry for the directory itself
at the beginning of the returned slice of DirEntry
objects, but then it is not clear if that is the
root directory or a directory / file in the root
directory with the same name as the root directory.
Using WalkDir is not great here because you cannot
tell it to not be recursive, so we will be wasting
cycles telling it to skip directories and files
*/
files, err := os.ReadDir(dir)
return dir, fs.FileInfoToDirEntry(rootInfo), files, err
rootInfo, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return dir, nil, []fs.DirEntry{}, errors.New("directory does not exist")
}
return dir, nil, []fs.DirEntry{}, err
}
return dir, nil, []fs.DirEntry{}, errors.New("directory does not exist")
/*
We could place the entry for the directory itself
at the beginning of the returned slice of DirEntry
objects, but then it is not clear if that is the
root directory or a directory / file in the root
directory with the same name as the root directory.
Using WalkDir is not great here because you cannot
tell it to not be recursive, so we will be wasting
cycles telling it to skip directories and files
*/
files, err := os.ReadDir(dir)
return dir, fs.FileInfoToDirEntry(rootInfo), files, err
}
func rmHandler(data []byte, resp RPCResponse) {
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -586,6 +586,9 @@ message WebContent {
string ContentType = 4;
uint64 Size = 5 [ jstype = JS_STRING ];
string OriginalFile = 6;
string Sha256 = 7;
bytes Content = 9;
}
@@ -1316,4 +1319,4 @@ message CertificateData {
message CertificateInfo {
repeated CertificateData info = 1;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v6.33.2
// protoc v3.15.8
// source: commonpb/common.proto
package commonpb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v6.33.2
// protoc v3.15.8
// source: dnspb/dns.proto
package dnspb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v6.33.2
// protoc v3.15.8
// source: rpcpb/services.proto
package rpcpb
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v6.33.2
// protoc v3.15.8
// source: sliverpb/sliver.proto
package sliverpb
+71 -2
View File
@@ -26,6 +26,7 @@ import (
"os"
"path"
"path/filepath"
"sort"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/log"
@@ -47,6 +48,13 @@ var (
ErrInvalidDialect = errors.New("invalid SQL Dialect")
databaseConfigLog = log.NamedLogger("config", "database")
defaultSQLitePragmas = map[string]string{
"journal_mode": "WAL", // reduce writer blocking, better concurrency
"busy_timeout": "5000", // wait for locks instead of failing fast (ms)
"synchronous": "NORMAL", // faster WAL syncs while retaining durability
"temp_store": "MEMORY", // keep temp structures off disk for quicker startup
}
)
// GetDatabaseConfigPath - File path to config.json
@@ -66,7 +74,8 @@ type DatabaseConfig struct {
Host string `json:"host"`
Port uint16 `json:"port"`
Params map[string]string `json:"params"`
Params map[string]string `json:"params"`
Pragmas map[string]string `json:"pragmas"`
MaxIdleConns int `json:"max_idle_conns"`
MaxOpenConns int `json:"max_open_conns"`
@@ -80,7 +89,7 @@ func (c *DatabaseConfig) DSN() (string, error) {
switch c.Dialect {
case Sqlite:
filePath := filepath.Join(assets.GetRootAppDir(), "sliver.db")
params := encodeParams(c.Params)
params := encodeSQLiteParams(c.Params, c.Pragmas)
return fmt.Sprintf("file:%s?%s", filePath, params), nil
case MySQL:
user := url.QueryEscape(c.Username)
@@ -112,6 +121,44 @@ func encodeParams(rawParams map[string]string) string {
return params.Encode()
}
func encodeSQLiteParams(rawParams map[string]string, pragmas map[string]string) string {
if rawParams == nil {
rawParams = map[string]string{}
}
params := url.Values{}
// Preserve any user-provided parameters first.
for key, value := range rawParams {
params.Add(key, value)
}
// Apply safer defaults only when the user has not set any custom pragma.
if _, ok := rawParams["_pragma"]; !ok {
selectedPragmas := pragmas
if len(selectedPragmas) == 0 {
selectedPragmas = defaultSQLitePragmas
}
keys := make([]string, 0, len(selectedPragmas))
for key := range selectedPragmas {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
value := selectedPragmas[key]
params.Add("_pragma", fmt.Sprintf("%s(%s)", key, value))
}
}
// Encourage shared cache to play nicer with WAL and multiple connections.
if _, ok := rawParams["cache"]; !ok {
params.Add("cache", "shared")
}
return params.Encode()
}
// Save - Save config file to disk
func (c *DatabaseConfig) Save() error {
configPath := GetDatabaseConfigPath()
@@ -161,6 +208,8 @@ func GetDatabaseConfig() *DatabaseConfig {
config.MaxOpenConns = 1
}
ensureSQLiteDefaults(config)
err := config.Save() // This updates the config with any missing fields
if err != nil {
databaseConfigLog.Errorf("Failed to save default config %s", err)
@@ -171,9 +220,29 @@ func GetDatabaseConfig() *DatabaseConfig {
func getDefaultDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{
Dialect: Sqlite,
Pragmas: defaultSQLitePragmas,
MaxIdleConns: 10,
MaxOpenConns: 100,
LogLevel: "warn",
}
}
func ensureSQLiteDefaults(c *DatabaseConfig) {
if c.Dialect != Sqlite {
return
}
if c.Params == nil {
c.Params = map[string]string{}
}
if _, ok := c.Params["_pragma"]; ok {
// User provided explicit pragmas; leave Pragmas untouched to avoid confusion.
return
}
if len(c.Pragmas) == 0 {
c.Pragmas = defaultSQLitePragmas
}
if _, ok := c.Params["cache"]; !ok {
c.Params["cache"] = "shared"
}
}
+12 -2
View File
@@ -789,8 +789,18 @@ func AddContent(pbWebContent *clientpb.WebContent, webContentDir string) (*clien
return nil, err
}
} else {
dbWebContent.ContentType = pbWebContent.ContentType
dbWebContent.Size = pbWebContent.Size
if pbWebContent.ContentType != "" {
dbWebContent.ContentType = pbWebContent.ContentType
}
if pbWebContent.Size != 0 {
dbWebContent.Size = pbWebContent.Size
}
if pbWebContent.OriginalFile != "" {
dbWebContent.OriginalFile = pbWebContent.OriginalFile
}
if pbWebContent.Sha256 != "" {
dbWebContent.Sha256 = pbWebContent.Sha256
}
dbModelWebContent := models.WebContentFromProtobuf(dbWebContent)
err = Session().Save(&dbModelWebContent).Error
+20 -14
View File
@@ -67,9 +67,11 @@ type WebContent struct {
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
WebsiteID uuid.UUID `gorm:"type:uuid;"`
Path string `gorm:"primaryKey"`
Size uint64
ContentType string
Path string `gorm:"primaryKey"`
Size uint64
ContentType string
OriginalFile string
Sha256 string
}
// BeforeCreate - GORM hook to automatically set values
@@ -81,12 +83,14 @@ func (wc *WebContent) BeforeCreate(tx *gorm.DB) (err error) {
// ToProtobuf - Converts to protobuf object
func (wc *WebContent) ToProtobuf(content *[]byte) *clientpb.WebContent {
return &clientpb.WebContent{
ID: wc.ID.String(),
WebsiteID: wc.WebsiteID.String(),
Path: wc.Path,
Size: uint64(wc.Size),
ContentType: wc.ContentType,
Content: *content,
ID: wc.ID.String(),
WebsiteID: wc.WebsiteID.String(),
Path: wc.Path,
Size: uint64(wc.Size),
ContentType: wc.ContentType,
OriginalFile: wc.OriginalFile,
Sha256: wc.Sha256,
Content: *content,
}
}
@@ -95,10 +99,12 @@ func WebContentFromProtobuf(pbWebContent *clientpb.WebContent) WebContent {
websiteUUID, _ := uuid.FromString(pbWebContent.WebsiteID)
return WebContent{
ID: siteUUID,
WebsiteID: websiteUUID,
Path: pbWebContent.Path,
Size: pbWebContent.Size,
ContentType: pbWebContent.ContentType,
ID: siteUUID,
WebsiteID: websiteUUID,
Path: pbWebContent.Path,
Size: pbWebContent.Size,
ContentType: pbWebContent.ContentType,
OriginalFile: pbWebContent.OriginalFile,
Sha256: pbWebContent.Sha256,
}
}
+21 -3
View File
@@ -19,6 +19,8 @@ package website
*/
import (
"crypto/sha256"
"encoding/hex"
"net/url"
"os"
"path/filepath"
@@ -87,14 +89,30 @@ func AddContent(name string, pbWebContent *clientpb.WebContent) error {
pbWebContent.WebsiteID = website.ID
}
if pbWebContent.Size == 0 && len(pbWebContent.Content) > 0 {
pbWebContent.Size = uint64(len(pbWebContent.Content))
}
if pbWebContent.OriginalFile == "" {
pbWebContent.OriginalFile = filepath.Base(pbWebContent.Path)
}
if len(pbWebContent.Content) > 0 {
sha := sha256.Sum256(pbWebContent.Content)
pbWebContent.Sha256 = hex.EncodeToString(sha[:])
}
webContent, err := db.AddContent(pbWebContent, webContentDir)
if err != nil {
return err
}
// Write content to disk
webContentPath := filepath.Join(webContentDir, webContent.ID)
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
// Write content to disk when provided (metadata-only updates skip disk writes)
if len(pbWebContent.Content) > 0 {
webContentPath := filepath.Join(webContentDir, webContent.ID)
return os.WriteFile(webContentPath, pbWebContent.Content, 0600)
}
return nil
}
// RemoveContent - Remove website content for a path
+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...)
+5 -4
View File
@@ -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