First commit

This commit is contained in:
Lilleman auf Larv 2024-02-03 11:13:20 +01:00
commit fbb6440c77
9 changed files with 232 additions and 0 deletions

4
.env.example Normal file
View File

@ -0,0 +1,4 @@
LOG_LEVEL=Verbose
LOG_LOCATION=Europe/Stockholm
PORT=4200
URLS_CMDS_JSON_PATH=./urlstocmd.example.json

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
bin

27
README.md Normal file
View File

@ -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).

5
build.sh Executable file
View File

@ -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

7
go.mod Normal file
View File

@ -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

12
go.sum Normal file
View File

@ -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=

157
main.go Normal file
View File

@ -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())
}
}

5
urlstocmd.example.json Normal file
View File

@ -0,0 +1,5 @@
{
"/docker": "docker ps",
"/hello": "echo \"hello world\"",
"/ls": "ls -l"
}

13
urltocmd.service Normal file
View File

@ -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