-
-
Save jvns/edf78e7775fea8888685a9a2956bc477 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"math/rand" | |
"net/http" | |
"os" | |
"os/exec" | |
"strings" | |
"syscall" | |
"time" | |
) | |
type RunRequest struct { | |
NginxConfig string `json:"nginx_config"` | |
Command string `json:"command"` | |
} | |
type RunResponse struct { | |
Result string `json:"result"` | |
} | |
func main() { | |
rand.Seed(time.Now().UnixNano()) | |
http.Handle("/", wrapLogger(Handler{runHandler})) | |
log.Fatal(http.ListenAndServe(":8080", nil)) | |
} | |
func runHandler(w http.ResponseWriter, r *http.Request) error { | |
w.Header().Add("Access-Control-Allow-Origin", "*") | |
w.Header().Add("Access-Control-Allow-Headers", "*") | |
if r.Method != "POST" { | |
// OPTIONS request | |
return nil | |
} | |
body, err := ioutil.ReadAll(r.Body) | |
if err != nil { | |
return fmt.Errorf("failed to read body: %s", err) | |
} | |
var req RunRequest | |
json.Unmarshal([]byte(body), &req) | |
// write config | |
file, err := os.CreateTemp("/tmp", "nginx_config") | |
errorFile, err := os.CreateTemp("/tmp", "nginx_errors") | |
if err != nil { | |
return fmt.Errorf("failed to create temp file, %s", err) | |
} | |
file.WriteString(req.NginxConfig) | |
file.Close() | |
defer os.Remove(file.Name()) | |
defer os.Remove(errorFile.Name()) | |
// set up network namespace | |
namespace := "ns_" + randSeq(16) | |
if err := exec.Command("ip", "netns", "add", namespace).Run(); err != nil { | |
return fmt.Errorf("failed to create network namespace: %s", err) | |
} | |
defer exec.Command("ip", "netns", "delete", namespace).Run() | |
if err := exec.Command("ip", "netns", "exec", namespace, "ip", "link", "set", "dev", "lo", "up").Run(); err != nil { | |
return fmt.Errorf("failed to create network namespace: %s", err) | |
} | |
// start httpbin | |
httpbin_cmd := exec.Command("ip", "netns", "exec", namespace, "go-httpbin", "-port", "7777") | |
if err := httpbin_cmd.Start(); err != nil { | |
return fmt.Errorf("failed to start go-httpbin: %s", err) | |
} | |
defer kill(httpbin_cmd) | |
// start nginx | |
nginx_cmd := exec.Command("ip", "netns", "exec", namespace, "nginx", "-c", file.Name(), "-e", errorFile.Name(), "-g", "daemon off;") | |
if err != nil { | |
return fmt.Errorf("failed to get pipe: %s", err) | |
} | |
ch := make(chan error) | |
go func() { | |
ch <- nginx_cmd.Run() | |
}() | |
// Check for errors | |
select { | |
case <-ch: | |
logs, _ := os.ReadFile(errorFile.Name()) | |
return fmt.Errorf("nginx failed to start. Error logs:\n\n %s", string(logs)) | |
case <-time.After(100 * time.Millisecond): | |
defer term(nginx_cmd) | |
break | |
} | |
// run curl | |
curlArgs := strings.Split(strings.TrimSpace(req.Command), " ") | |
if curlArgs[0] != "curl" && curlArgs[0] != "http" { | |
return fmt.Errorf("command must start with 'curl' or 'http'") | |
} | |
curlCommand := append([]string{"netns", "exec", namespace}, curlArgs...) | |
output, _ := exec.Command("ip", curlCommand...).CombinedOutput() | |
// return response | |
resp := RunResponse{ | |
Result: string(output), | |
} | |
response, err := json.Marshal(&resp) | |
if err != nil { | |
return fmt.Errorf("failed to marshal json, %s", err) | |
} | |
w.Header().Add("Content-Type", "application/json") | |
w.Write(response) | |
return nil | |
} | |
func wrapLogger(handler http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
rw := &responseWrapper{w, 200} | |
start := time.Now() | |
handler.ServeHTTP(rw, r) | |
elapsed := time.Since(start) | |
log.Printf("%s %d %s %s %s", r.RemoteAddr, rw.status, r.Method, r.URL.Path, elapsed) | |
}) | |
} | |
func term(cmd *exec.Cmd) { | |
if cmd.Process != nil { | |
cmd.Process.Signal(syscall.SIGTERM) | |
} | |
} | |
func kill(cmd *exec.Cmd) { | |
if cmd.Process != nil { | |
cmd.Process.Kill() | |
} | |
} | |
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") | |
func randSeq(n int) string { | |
b := make([]rune, n) | |
for i := range b { | |
b[i] = letters[rand.Intn(len(letters))] | |
} | |
return string(b) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment