commit fbb6440c7771bf87ba4706574ad30512a7539ca0 Author: lilleman Date: Sat Feb 3 11:13:20 2024 +0100 First commit 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