First commit
This commit is contained in:
commit
fbb6440c77
4
.env.example
Normal file
4
.env.example
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
bin
|
27
README.md
Normal file
27
README.md
Normal 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
5
build.sh
Executable 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
7
go.mod
Normal 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
12
go.sum
Normal 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
157
main.go
Normal 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
5
urlstocmd.example.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"/docker": "docker ps",
|
||||||
|
"/hello": "echo \"hello world\"",
|
||||||
|
"/ls": "ls -l"
|
||||||
|
}
|
13
urltocmd.service
Normal file
13
urltocmd.service
Normal 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
|
Loading…
Reference in New Issue
Block a user