Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ require (
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.21.0
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
h12.io/socks v1.0.3
software.sslmate.com/src/go-pkcs12 v0.5.0
)

require (
Expand All @@ -29,12 +33,8 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect
)

replace github.com/jcmturner/gokrb5/v8 => github.com/Macmod/gokrb5/v8 v8.4.5-0.20240428143821-ea9a660f0f44
Expand Down
93 changes: 92 additions & 1 deletion godap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package main
import (
"fmt"
"log"
"os"
"strings"

"github.com/Macmod/godap/v2/pkg/debug"
"github.com/Macmod/godap/v2/tui"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/term"
)

var acceptableAuthFlagSets = []map[string]bool{
{"username": true},
{"username": true, "password": true},
{"username": true, "passfile": true},
{"username": true, "hash": true},
Expand Down Expand Up @@ -78,12 +83,71 @@ func main() {
Short: "A complete TUI for LDAP.",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := validateFlagSet(cmd)
// Apply GODAP_PASSWD env var when no explicit password flag was provided.
if !cmd.Flags().Changed("password") && !cmd.Flags().Changed("passfile") {
if envPw := os.Getenv("GODAP_PASSWD"); envPw != "" {
tui.LdapPassword = envPw
}
}

// Apply GODAP_SSH_PASSWORD env var when no explicit SSH password flag was provided.
if !cmd.Flags().Changed("ssh-password") && !cmd.Flags().Changed("ssh-passfile") {
if envPw := os.Getenv("GODAP_SSH_PASSWORD"); envPw != "" {
tui.SSHTunnelPassword = envPw
}
}

// --ssh-passfile: read password from file or prompt on "-".
if cmd.Flags().Changed("ssh-passfile") {
pw, err := tui.ReadFileOrStdin(tui.SSHTunnelPasswordFile, "SSH Password: ")
if err != nil {
log.Fatalf("Failed to read SSH password file: %v", err)
}
tui.SSHTunnelPassword = strings.TrimSpace(pw)
}

// Infer SSH auth method from flags; explicit --ssh-auth is honoured only as a fallback.
sshAgentSet := tui.SSHTunnelAgentAuth
sshKeySet := cmd.Flags().Changed("ssh-key")
sshPassSet := tui.SSHTunnelPassword != ""
switch {
case sshAgentSet && sshKeySet:
log.Fatal("Conflicting SSH auth flags: --ssh-agent and --ssh-key cannot both be set")
case sshAgentSet && sshPassSet:
log.Fatal("Conflicting SSH auth flags: --ssh-agent and --ssh-password/--ssh-passfile cannot both be set")
case sshKeySet && sshPassSet:
log.Fatal("Conflicting SSH auth flags: --ssh-key and --ssh-password/--ssh-passfile cannot both be set")
case sshAgentSet:
tui.SSHTunnelAuthMethod = "agent"
case sshKeySet:
tui.SSHTunnelAuthMethod = "key"
case sshPassSet:
tui.SSHTunnelAuthMethod = "password"
}

err := validateFlagSet(cmd)
if err != nil {
log.Fatalf(fmt.Sprint(err))
}

// Prompt for LDAP password when username is set but no password method was provided.
if tui.LdapUsername != "" &&
tui.LdapPassword == "" &&
tui.LdapPasswordFile == "" &&
tui.NtlmHash == "" &&
tui.NtlmHashFile == "" &&
!tui.Kerberos &&
tui.CertFile == "" &&
tui.PfxFile == "" {
fmt.Print("LDAP Password: ")
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
log.Fatalf("Failed to read password: %v", err)
}
tui.LdapPassword = string(passwordBytes)
}

tui.LdapServer = args[0]

if tui.LdapPort == 0 {
Expand All @@ -94,6 +158,20 @@ func main() {
}
}

// A non-empty --ssh-host implicitly enables the tunnel.
if tui.SSHTunnelHost != "" {
tui.SSHTunnelEnabled = true
}

// Initialize debug log if requested.
if tui.DebugLogPath != "" {
if err := debug.Init(tui.DebugLogPath); err != nil {
log.Printf("Warning: could not open debug log %q: %v", tui.DebugLogPath, err)
} else {
defer debug.Close()
}
}

tui.SetupApp()
},
}
Expand Down Expand Up @@ -132,6 +210,19 @@ func main() {
rootCmd.Flags().StringVarP(&tui.ExportDir, "exportdir", "", "data", "Custom directory to save godap exports taken with Ctrl+S")
rootCmd.Flags().StringVarP(&tui.BackendFlavor, "backend", "b", "msad", "LDAP backend flavor (msad, basic or auto)")

// SSH tunnel flags
rootCmd.Flags().StringVar(&tui.SSHTunnelHost, "ssh-host", "", "SSH tunnel host (also enables the tunnel when non-empty)")
rootCmd.Flags().IntVar(&tui.SSHTunnelPort, "ssh-port", 22, "SSH tunnel port")
rootCmd.Flags().StringVar(&tui.SSHTunnelUser, "ssh-user", os.Getenv("USER"), "SSH tunnel username")
rootCmd.Flags().StringVar(&tui.SSHTunnelAuthMethod, "ssh-auth", "password", "SSH auth method: password, key, or agent (deprecated: inferred automatically from other flags)")
rootCmd.Flags().StringVar(&tui.SSHTunnelPassword, "ssh-password", "", "SSH tunnel password")
rootCmd.Flags().StringVar(&tui.SSHTunnelPasswordFile, "ssh-passfile", "", "Path to a file containing the SSH tunnel password (or - for stdin)")
rootCmd.Flags().BoolVar(&tui.SSHTunnelAgentAuth, "ssh-agent", false, "Use SSH agent for tunnel authentication")
rootCmd.Flags().StringVar(&tui.SSHTunnelKeyFile, "ssh-key", "", "Path to SSH private key file")
rootCmd.Flags().StringVar(&tui.SSHTunnelKeyPassphrase, "ssh-key-passphrase", "", "Passphrase for SSH private key")
rootCmd.Flags().BoolVar(&tui.SSHTunnelInsecure, "ssh-ignore-host-key", false, "Skip SSH host key verification (insecure)")
rootCmd.Flags().StringVar(&tui.DebugLogPath, "debug-log", "", "Path to debug log file")

versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version number of the application",
Expand Down
38 changes: 38 additions & 0 deletions pkg/debug/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package debug

import (
"fmt"
"log"
"os"
)

var logger *log.Logger
var logFile *os.File

// Init opens path for append-write and installs the logger.
func Init(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return err
}
logFile = f
logger = log.New(f, "", log.Ldate|log.Ltime|log.Lmicroseconds)
return nil
}

// Log writes a timestamped line to the debug log if Init was called.
func Log(format string, args ...any) {
if logger == nil {
return
}
logger.Output(2, fmt.Sprintf(format, args...))
}

// Close flushes and closes the underlying log file.
func Close() {
if logFile != nil {
logFile.Close()
logFile = nil
logger = nil
}
}
Loading