From fbb6440c7771bf87ba4706574ad30512a7539ca0 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sat, 3 Feb 2024 11:13:20 +0100 Subject: [PATCH] First commit --- .env.example | 4 ++ .gitignore | 2 + README.md | 27 +++++++ build.sh | 5 ++ go.mod | 7 ++ go.sum | 12 ++++ main.go | 157 +++++++++++++++++++++++++++++++++++++++++ urlstocmd.example.json | 5 ++ urltocmd.service | 13 ++++ 9 files changed, 232 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build.sh create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 urlstocmd.example.json create mode 100644 urltocmd.service diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..91bc546 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +LOG_LEVEL=Verbose +LOG_LOCATION=Europe/Stockholm +PORT=4200 +URLS_CMDS_JSON_PATH=./urlstocmd.example.json \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..915d6c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +bin diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee01091 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# URL to CMD app + +A tiny web server that runs a specific command on the server when a specific URL is reached. A use case can be to update production code on a server, but without giving arbitrary command access to the server to external parties. + +## Dependencies and installation + +To build a new binary, you need to have Go 1.21 or later installed and then run `./build.sh`. All binaries lands in the `bin` folder. + +To "install" this app, simply copy the binary to a folder in your $path, or run it directly. + +There is no dependencies except for a supported architecture for the binary. If your architecture is not present, please add it and make a PR. + +## Running and configuration + +Either copy `.env.example` to `.env` and modify the settings to your likings, or modify the ENV in your shell according to `.env.example`. Modify `urlstocmd.example.json` to your liking and rename it to match your ENV `URLS_CMDS_JSON_PATH`. Then run the application. + +## Installing as a system.d service in compatible Linux systems (Debian etc) + +1. Configure the app according to the above instructions somewhere on your filesystem. +2. Copy `urltocmd.service` to `/etc/systemd/system/urltocmd.service`. +3. Modify `/etc/systemd/system/urltocmd.service` to match your needs (especially check User, ExecStart and WorkingDirectory) +4. `sudo systemctl daemon-reload && sudo systemctl enable urltocmd && sudo service urltocmd start` +5. Profit! + +You can watch the log from urltocmd like this: `sudo journalctl -u urltocmd -f`. + +See more information about how to work with system.d services [here](https://wiki.ubuntu.com/SystemdForUpstartUsers). diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c40f0dd --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/darwin_arm64/urltocmd +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/darwin_amd64/urltocmd +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/linux_amd64/urltocmd diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d7442be --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module tw/urltocmd + +go 1.21.0 + +require gitea.larvit.se/pwrpln/go_log v0.3.0 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a4f11f1 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +gitea.larvit.se/pwrpln/go_log v0.3.0 h1:zxCQ4vPU8gqsYT35DIrGm1qntd88oDbzd7SvzAmNi1s= +gitea.larvit.se/pwrpln/go_log v0.3.0/go.mod h1:CVZH9ge+rbQyhc+HYSI+B1A3j54wnQxzAcFgFMqsfLw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..96ea4c4 --- /dev/null +++ b/main.go @@ -0,0 +1,157 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "os" + "os/exec" + "sync" + "time" + + "gitea.larvit.se/pwrpln/go_log" + "github.com/joho/godotenv" +) + +func main() { + // Load ENV from .env file + err := godotenv.Load() + if err != nil { + tmpLog := go_log.GetLog() + tmpLog.Info("Failed to load .env file!") + } + + // Setting up logger + log := go_log.GetLog() + log.MinLogLvl = go_log.LogLvlFromStr(os.Getenv("LOG_LEVEL")) + loc, err := time.LoadLocation(os.Getenv("LOG_LOCATION")) + if err != nil { + panic(err) + } + log.TimeLocation = loc + + // Open the JSON file for reading + log.Info("Reading URLs and CMDs from file", "urls.json path", os.Getenv("URLS_CMDS_JSON_PATH")) + file, err := os.Open(os.Getenv("URLS_CMDS_JSON_PATH")) + if err != nil { + log.Error("Error opening URLs and CMDs JSON file", "err", err.Error()) + panic(err) + } + + // Read the JSON data from the file + jsonStr, err := io.ReadAll(file) + if err != nil { + log.Error("Error reading JSON string from file", "err", err.Error()) + panic(err) + } + file.Close() + + var urlsToCmds map[string]string + err = json.Unmarshal([]byte(jsonStr), &urlsToCmds) + if err != nil { + log.Error("Error unmarshalling JSON", "err", err.Error) + panic(err) + } + + http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + cmdStr, exists := urlsToCmds[req.URL.Path] + + if !exists { + log.Verbose("Call to undefined URL", "URL", req.URL.Path) + res.WriteHeader(404) + res.Write([]byte("Not Found")) + return + } + + log.Verbose("Executing command", "cmdStr", cmdStr) + + // Set the response headers for SSE + res.Header().Set("Content-Type", "text/event-stream") + res.Header().Set("Cache-Control", "no-cache") + res.Header().Set("Connection", "keep-alive") + + // Create the command + cmd := exec.Command("/bin/sh", "-c", cmdStr) + + // Create a pipes to capture the command's output + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + http.Error(res, "Internal Server Error", 500) + log.Error("Could not create stdout pipe from command", "err", err.Error()) + return + } + stderrPipe, err := cmd.StderrPipe() + if err != nil { + http.Error(res, "Internal Server Error", 500) + log.Error("Could not create stderr pipe from command", "err", err.Error()) + return + } + + // Start the command + err = cmd.Start() + if err != nil { + http.Error(res, "Internal Server Error", 500) + log.Error("Could not start the command", "err", err.Error()) + return + } + + defer cmd.Wait() + defer stdoutPipe.Close() + defer stderrPipe.Close() + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + buf := make([]byte, 1024) + for { + n, err := stdoutPipe.Read(buf) + if err == io.EOF { + break + } + if err != nil { + log.Error("Error reading stdout from command", "err", err.Error()) + break + } + log.Debug("std", "out", string(buf[:n])) + res.Write(buf[:n]) + flusher, ok := res.(http.Flusher) + if ok { + flusher.Flush() + } + } + wg.Done() + }() + + wg.Add(1) + go func() { + buf := make([]byte, 1024) + for { + n, err := stderrPipe.Read(buf) + if err == io.EOF { + break + } + if err != nil { + log.Error("Error reading stderr from command", "err", err.Error()) + break + } + log.Debug("std", "err", string(buf[:n])) + res.Write([]byte("STDERR: ")) + res.Write(buf[:n]) + flusher, ok := res.(http.Flusher) + if ok { + flusher.Flush() + } + } + wg.Done() + }() + + wg.Wait() + }) + + log.Info("Starting web server", "PORT", os.Getenv("PORT")) + err = http.ListenAndServe(":"+os.Getenv("PORT"), nil) + if err != nil { + log.Error("Web server crashed", "err", err.Error()) + } +} diff --git a/urlstocmd.example.json b/urlstocmd.example.json new file mode 100644 index 0000000..7ccaf6d --- /dev/null +++ b/urlstocmd.example.json @@ -0,0 +1,5 @@ +{ + "/docker": "docker ps", + "/hello": "echo \"hello world\"", + "/ls": "ls -l" +} \ No newline at end of file diff --git a/urltocmd.service b/urltocmd.service new file mode 100644 index 0000000..03d4d20 --- /dev/null +++ b/urltocmd.service @@ -0,0 +1,13 @@ +[Unit] +Description=A tiny web server launching local commands upon specific URLs being called + +[Service] +Type=simple +Restart=always +RestartSec=1 +User=root +ExecStart=/root/urltocmd/urltocmd +WorkingDirectory=/root/urltocmd + +[Install] +WantedBy=mult-user.target