Feat: conditional permissions behavior (#28)
* Fix: don't need sudo if we're root + other aesthetics
* Heavy refactoring, see PR #28
* Fix: avoid silent fatalities
demo: https://tcp.ac/i/JMSUc.gif
* Fix: Inverse check on `IsRoot`
* D.R.Y: check for permissions error in `common.ErrorCheck`
Reduce cognitive complexity.
* Fix: Issue with copying
* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646535918
* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105
* Revert "Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105"
This reverts commit ce15213009
.
* Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646730751
This commit is contained in:
parent
4d0086df41
commit
6c48a35180
21 changed files with 604 additions and 255 deletions
|
@ -3,12 +3,15 @@ package command
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
|
||||
"github.com/HikariKnight/quickpassthrough/internal/common"
|
||||
"github.com/HikariKnight/quickpassthrough/internal/logger"
|
||||
)
|
||||
|
||||
// Run a command and return STDOUT
|
||||
|
@ -27,14 +30,14 @@ func Run(binary string, args ...string) ([]string, error) {
|
|||
output, _ := io.ReadAll(&stdout)
|
||||
|
||||
// Get the output
|
||||
outputs := []string{}
|
||||
outputs := make([]string, 0, 1)
|
||||
outputs = append(outputs, string(output))
|
||||
|
||||
// Return our list of items
|
||||
return outputs, err
|
||||
}
|
||||
|
||||
// This function is just like command.Run() but also returns STDERR
|
||||
// RunErr is just like command.Run() but also returns STDERR
|
||||
func RunErr(binary string, args ...string) ([]string, []string, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
|
@ -59,8 +62,18 @@ func RunErr(binary string, args ...string) ([]string, []string, error) {
|
|||
return outputs, outerrs, err
|
||||
}
|
||||
|
||||
// This functions runs the command "sudo -Sk -- echo", this forces sudo
|
||||
// to re-authenticate and lets us enter the password to STDIN
|
||||
func RunErrSudo(isRoot bool, binary string, args ...string) ([]string, []string, error) {
|
||||
if !isRoot && binary != "sudo" {
|
||||
args = append([]string{binary}, args...)
|
||||
binary = "sudo"
|
||||
}
|
||||
logger.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
|
||||
fmt.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " "))
|
||||
return RunErr(binary, args...)
|
||||
}
|
||||
|
||||
// Elevate elevates this functions runs the command "sudo -Sk -- echo",
|
||||
// this forces sudo to re-authenticate and lets us enter the password to STDIN
|
||||
// giving us the ability to run sudo commands
|
||||
func Elevate(password string) {
|
||||
// Do a simple sudo command to just authenticate with sudo
|
||||
|
@ -71,29 +84,83 @@ func Elevate(password string) {
|
|||
|
||||
// Open STDIN
|
||||
stdin, err := cmd.StdinPipe()
|
||||
errorcheck.ErrorCheck(err, "\nFailed to get sudo STDIN")
|
||||
common.ErrorCheck(err, "\nFailed to get sudo STDIN")
|
||||
|
||||
// Start the authentication
|
||||
cmd.Start()
|
||||
err = cmd.Start()
|
||||
common.ErrorCheck(err, "\nFailed to start sudo command")
|
||||
|
||||
// Get the passed password
|
||||
pw, _ := base64.StdEncoding.DecodeString(password)
|
||||
_, err = stdin.Write([]byte(string(pw) + "\n"))
|
||||
errorcheck.ErrorCheck(err, "\nFailed at typing to STDIN")
|
||||
common.ErrorCheck(err, "\nFailed at typing to STDIN")
|
||||
// Clear the password
|
||||
pw = nil
|
||||
password = ""
|
||||
|
||||
stdin.Close()
|
||||
_ = stdin.Close()
|
||||
|
||||
// Wait for the sudo prompt (If the correct password was given, it will not stay behind)
|
||||
err = cmd.Wait()
|
||||
errorcheck.ErrorCheck(err, "\nError, password given was wrong")
|
||||
common.ErrorCheck(err, "\nError, password given was wrong")
|
||||
}
|
||||
|
||||
// Function to just clear the terminal
|
||||
// Clear clears the terminal.
|
||||
func Clear() {
|
||||
c := exec.Command("clear")
|
||||
c.Stdout = os.Stdout
|
||||
c.Run()
|
||||
_ = c.Run()
|
||||
}
|
||||
|
||||
// ExecAndLogSudo executes an elevated command and logs the output.
|
||||
//
|
||||
// * if we're root, the command is executed directly
|
||||
// * if we're not root, the command is prefixed with "sudo"
|
||||
//
|
||||
// - noisy determines if we should print the command to the user
|
||||
// noisy isn't set to true by our copy caller, as it logs differently,
|
||||
// but other callers set it.
|
||||
func ExecAndLogSudo(isRoot, noisy bool, exe string, args ...string) error {
|
||||
if !isRoot && exe != "sudo" {
|
||||
og := exe
|
||||
exe = "sudo"
|
||||
newArgs := make([]string, 0)
|
||||
newArgs = append(newArgs, og)
|
||||
newArgs = append(newArgs, args...)
|
||||
args = newArgs
|
||||
}
|
||||
|
||||
// Write to logger
|
||||
logger.Printf("Executing (elevated): %s %s\n", exe, strings.Join(args, " "))
|
||||
|
||||
if noisy {
|
||||
// Print to the user
|
||||
fmt.Printf("Executing (elevated): %s %s\nSee debug.log for detailed output\n", exe, strings.Join(args, " "))
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := exec.Command(exe, args...)
|
||||
r.Dir = wd
|
||||
|
||||
cmdCombinedOut, err := r.CombinedOutput()
|
||||
outStr := string(cmdCombinedOut)
|
||||
|
||||
// Write to logger, tabulate output
|
||||
// tabulation denotes it's hierarchy as a child of the command
|
||||
outStr = strings.ReplaceAll(outStr, "\n", "\n\t")
|
||||
logger.Printf("\t" + string(cmdCombinedOut) + "\n")
|
||||
if noisy {
|
||||
// Print to the user
|
||||
fmt.Printf("%s\n", outStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to execute %s: %w\n%s", exe, err, outStr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
61
pkg/command/command_test.go
Normal file
61
pkg/command/command_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const fakeSudo = `#!/bin/sh
|
||||
"$@" -qptest`
|
||||
|
||||
const fakeUtil = `#!/bin/sh
|
||||
echo "$@"
|
||||
if [ "$4" = "-qptest" ]; then exit 0; else exit 1; fi`
|
||||
|
||||
func setupExecTestEnv(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
fakeSudoPath := filepath.Join(tmpDir, "sudo")
|
||||
fakeUtilPath := filepath.Join(tmpDir, "util")
|
||||
|
||||
if err := os.WriteFile(fakeSudoPath, []byte(fakeSudo), 0755); err != nil {
|
||||
t.Fatalf("failed to write fake sudo stub: %s", err.Error())
|
||||
}
|
||||
if err := os.WriteFile(fakeUtilPath, []byte(fakeUtil), 0755); err != nil {
|
||||
t.Fatalf("failed to write fake util stub: %s", err.Error())
|
||||
}
|
||||
t.Setenv("PATH", tmpDir+":"+os.Getenv("PATH"))
|
||||
|
||||
return fakeSudoPath, fakeUtilPath
|
||||
}
|
||||
|
||||
func TestExecAndLogSudo(t *testing.T) {
|
||||
_, fakeUtilPath := setupExecTestEnv(t)
|
||||
|
||||
args := []string{"i am a string with spaces", "i came to ruin parsers and chew bubble gum", "and I'm all out of bubblegum."}
|
||||
|
||||
t.Run("is_not_root", func(t *testing.T) {
|
||||
if err := ExecAndLogSudo(false, false, "util", args...); err != nil {
|
||||
t.Errorf("unexpected error: %s", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("is_root", func(t *testing.T) {
|
||||
newFakeUtil := strings.Replace(fakeUtil, "exit 1", "exit 0", 1)
|
||||
newFakeUtil = strings.Replace(newFakeUtil, "exit 0", "exit 1", 1)
|
||||
if err := os.WriteFile(fakeUtilPath, []byte(newFakeUtil), 0755); err != nil {
|
||||
t.Fatalf("failed to overwrite fake util with modified stub: %s", err.Error())
|
||||
}
|
||||
if err := ExecAndLogSudo(false, false, "util", args...); err == nil {
|
||||
t.Errorf("expected error when using modified util with sudo, got nil")
|
||||
}
|
||||
|
||||
if err := ExecAndLogSudo(true, true, "util", args...); err != nil {
|
||||
t.Errorf("unexpected error: %s", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
|
@ -7,29 +7,31 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
|
||||
"github.com/HikariKnight/quickpassthrough/internal/common"
|
||||
)
|
||||
|
||||
/*
|
||||
* This just implements repetetive tasks I have to do with files
|
||||
*/
|
||||
|
||||
// Creates a file and appends the content to the file (ending newline must be supplied with content string)
|
||||
// AppendContent creates a file and appends the content to the file.
|
||||
// (ending newline must be supplied with content string)
|
||||
func AppendContent(content string, fileName string) {
|
||||
// Open the file
|
||||
f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
|
||||
errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening \"%s\" for writing", fileName))
|
||||
|
||||
common.ErrorCheck(err, fmt.Sprintf("Error opening \"%s\" for writing", fileName))
|
||||
defer f.Close()
|
||||
|
||||
// Write the content
|
||||
_, err = f.WriteString(content)
|
||||
errorcheck.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName))
|
||||
common.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName))
|
||||
}
|
||||
|
||||
// Reads the file and returns a stringlist with each line
|
||||
// ReadLines reads the file and returns a stringlist with each line.
|
||||
func ReadLines(fileName string) []string {
|
||||
content, err := os.Open(fileName)
|
||||
errorcheck.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName))
|
||||
common.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName))
|
||||
defer content.Close()
|
||||
|
||||
// Make a list of lines
|
||||
|
@ -46,54 +48,55 @@ func ReadLines(fileName string) []string {
|
|||
|
||||
}
|
||||
|
||||
// Reads a file and returns all the content as a string
|
||||
// ReadFile reads a file and returns all the content as a string.
|
||||
func ReadFile(fileName string) string {
|
||||
// Read the whole file
|
||||
content, err := os.ReadFile(fileName)
|
||||
errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName))
|
||||
common.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName))
|
||||
|
||||
// Return all the lines as one string
|
||||
return string(content)
|
||||
|
||||
}
|
||||
|
||||
// Checks if a file exists and returns a bool
|
||||
func FileExist(fileName string) bool {
|
||||
// FileExist checks if a file exists and returns a bool and any error that isn't os.ErrNotExist.
|
||||
func FileExist(fileName string) (bool, error) {
|
||||
var exist bool
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(fileName); !errors.Is(err, os.ErrNotExist) {
|
||||
// Set the value to true
|
||||
_, err := os.Stat(fileName)
|
||||
switch {
|
||||
case err == nil:
|
||||
exist = true
|
||||
} else {
|
||||
// Set the value to false
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// Set the value to true
|
||||
exist = false
|
||||
err = nil
|
||||
}
|
||||
|
||||
// Return if the file exists
|
||||
return exist
|
||||
return exist, err
|
||||
}
|
||||
|
||||
// Copies a FILE from source to dest
|
||||
// FileCopy copies a FILE from source to dest.
|
||||
func FileCopy(sourceFile, destFile string) {
|
||||
// Get the file info
|
||||
filestat, err := os.Stat(sourceFile)
|
||||
errorcheck.ErrorCheck(err, "Error getting fileinfo of: %s", sourceFile)
|
||||
common.ErrorCheck(err, "Error getting fileinfo of: %s", sourceFile)
|
||||
|
||||
// If the file is a regular file
|
||||
if filestat.Mode().IsRegular() {
|
||||
// Open the source file for reading
|
||||
source, err := os.Open(sourceFile)
|
||||
errorcheck.ErrorCheck(err, "Error opening %s for copying", sourceFile)
|
||||
common.ErrorCheck(err, "Error opening %s for copying", sourceFile)
|
||||
defer source.Close()
|
||||
|
||||
// Create the destination file
|
||||
dest, err := os.Create(destFile)
|
||||
errorcheck.ErrorCheck(err, "Error creating %s", destFile)
|
||||
common.ErrorCheck(err, "Error creating %s", destFile)
|
||||
defer dest.Close()
|
||||
|
||||
// Copy the contents of source to dest using io
|
||||
_, err = io.Copy(dest, source)
|
||||
errorcheck.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile)
|
||||
common.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
|
||||
"github.com/gookit/color"
|
||||
|
||||
"github.com/HikariKnight/quickpassthrough/internal/common"
|
||||
)
|
||||
|
||||
func ManualInput(msg string, format string) []string {
|
||||
|
@ -18,7 +19,7 @@ func ManualInput(msg string, format string) []string {
|
|||
// Get the user input
|
||||
var input string
|
||||
_, err := fmt.Scan(&input)
|
||||
errorcheck.ErrorCheck(err)
|
||||
common.ErrorCheck(err)
|
||||
|
||||
input_list := strings.Split(input, ",")
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/HikariKnight/ls-iommu/pkg/errorcheck"
|
||||
"github.com/HikariKnight/quickpassthrough/internal/common"
|
||||
)
|
||||
|
||||
// Slightly modified from source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07
|
||||
|
@ -17,7 +17,7 @@ import (
|
|||
// creating the file structure at 'dst' along the way, and writing any files
|
||||
func Untar(dst string, fileName string) error {
|
||||
r, err := os.Open(fileName)
|
||||
errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName))
|
||||
common.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName))
|
||||
defer r.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(r)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue