Last active
June 28, 2023 20:39
-
-
Save pnck/a33a4a3a1a121ce52a7b52fb0f599e61 to your computer and use it in GitHub Desktop.
control warp service through web interface
This file contains hidden or 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
// Codes in this file is almost written by copilot, which brings lot of fun (it wants to fill "lots of bugs" here :)") | |
// I only give it prompts, as you can see in the comments | |
// ---- ACTUAL CODES STARTS HERE ---- | |
// a typical http server program's main file | |
// use html template to return a interactive page | |
package main | |
import ( | |
"context" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"html/template" | |
"math/rand" | |
"net" | |
"net/http" | |
"os" | |
"strings" | |
"sync/atomic" | |
"time" | |
"github.com/coreos/go-systemd/v22/dbus" | |
) | |
// the template page is a system control page | |
// including: | |
// a summary line, showing the usage which comes from `getBandwidthUsage` function | |
// a button (80px width), showing service status comes from `getServiceStatus` function in its text | |
// the service status title and the button is shown in a same line. | |
// the button send Ajax request to toggles the backend service when clicked | |
const frontPageTempl = ` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>System Control</title> | |
</head> | |
<body> | |
<h1>Super Simple Control Page</h1> | |
<p>Bandwidth Usage: {{.BandwidthUsage}}</p> | |
<p>{{.ServiceStatusTitle}}: <button id="serviceStatus" style="width:80px" onclick="toggleService()">{{.ServiceStatus}}</button></p> | |
<script> | |
function toggleService() { | |
const req = new XMLHttpRequest(); | |
req.open("POST", "/toggleService",false); | |
req.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); | |
req.send(JSON.stringify({"csrf":{{.CSRF}}})); | |
if (req.status === 200 && req.responseText === "OK") { | |
window.location.reload(); | |
} | |
} | |
</script> | |
</body> | |
</html> | |
` | |
// the data structure for template | |
type frontPageData struct { | |
BandwidthUsage string | |
ServiceStatusTitle string | |
ServiceStatus string | |
CSRF uint32 | |
} | |
// an atomic csrf code to ensure the request leagal | |
var currentCSRFCode atomic.Uint32 | |
// functions for template | |
// get bandwidth usage | |
func getBandwidthUsage() string { | |
// fetch a http api to get the bandwidth usage info | |
client := &http.Client{ | |
Timeout: 5 * time.Second, | |
Transport: &http.Transport{ | |
DialContext: (&net.Dialer{ | |
Timeout: 5 * time.Second, | |
}).DialContext, | |
}, | |
} | |
r, err := client.Get(fmt.Sprintf("https://api.64clouds.com/v1/getServiceInfo?veid=%s&api_key=%s", os.Getenv("VEID"), os.Getenv("API_KEY"))) | |
if err != nil { | |
panic(err) | |
} | |
defer r.Body.Close() | |
// parse the json response | |
var respJson map[string]interface{} | |
err = json.NewDecoder(r.Body).Decode(&respJson) | |
if err != nil { | |
panic(err) | |
} | |
respErr := int(respJson["error"].(float64)) | |
if respErr != 0 { | |
panic(fmt.Errorf("fetch error (%d): %s", respErr, respJson["message"])) | |
} | |
// calculate the usage | |
used := respJson["monthly_data_multiplier"].(float64) * respJson["data_counter"].(float64) | |
total := respJson["plan_monthly_data"].(float64) | |
// return gb used and ratio | |
usageStr := fmt.Sprintf("%.2fGB/%.2fGB (%.2f%%)", used/1024/1024/1024, total/1024/1024/1024, used/total*100) | |
fmt.Printf("bandwidth usage: %s\n", usageStr) | |
return usageStr | |
} | |
// get service status | |
func getServiceStatus() string { | |
ctx := context.Background() | |
conn, err := dbus.NewWithContext(ctx) | |
if err != nil { | |
panic(err) | |
} | |
defer conn.Close() | |
ps, err := conn.GetUnitPropertiesContext(ctx, "wg-quick.target") | |
if err != nil { | |
panic(err) | |
} | |
services := ps["ConsistsOf"].([]string) | |
allActive := true | |
for _, service := range services { | |
_timeLimitedCtx, c := context.WithTimeout(ctx, 5*time.Second) | |
a, err := conn.GetUnitPropertyContext(_timeLimitedCtx, service, "ActiveState") | |
if err != nil { | |
c() | |
panic(err) | |
} | |
allActive = allActive && (strings.Trim(a.Value.String(), " \"") == "active") | |
fmt.Printf("%s -> %#v\n", service, a.Value.String()) | |
if !allActive { | |
c() | |
break | |
} | |
c() | |
} | |
if allActive { | |
return "Running" | |
} else { | |
return "Stopped" | |
} | |
} | |
// the template | |
var t = template.Must(template.New("frontPage").Parse(frontPageTempl)) | |
// rootHandler echoes the Path component of the request URL r. | |
func rootHandler(w http.ResponseWriter, _ *http.Request) { | |
//set csrf code to a random number initialzed by current time | |
//currentCSRFCode.Store(123456) | |
currentCSRFCode.Store(rand.Uint32()) | |
pageData := frontPageData{ | |
BandwidthUsage: "Unknown", | |
ServiceStatusTitle: "Wireguard (cloudflare warp) status", | |
ServiceStatus: "-", | |
CSRF: currentCSRFCode.Load(), | |
} | |
_writeErr := func(e error) { | |
w.WriteHeader(http.StatusInternalServerError) | |
w.Write([]byte(e.Error())) | |
fmt.Printf("%s\n", e.Error()) | |
} | |
// try get service status | |
err := func() (err error) { | |
defer func() { | |
r := recover() | |
if r != nil { | |
err = fmt.Errorf("getServiceStatus() error: %w", r.(error)) | |
} | |
}() | |
pageData.ServiceStatus = getServiceStatus() | |
return nil | |
}() | |
// write error if any | |
if err != nil { | |
_writeErr(err) | |
return | |
} | |
// try get bandwidth usage | |
err = func() (err error) { | |
defer func() { | |
r := recover() | |
if r != nil { | |
err = fmt.Errorf("getBandwidthUsage() error: %w", r.(error)) | |
} | |
}() | |
pageData.BandwidthUsage = getBandwidthUsage() | |
return nil | |
}() | |
// keep bandwidth usage unknown if failed to fetch | |
if err != nil { | |
fmt.Println(err.Error()) | |
} | |
// apply template | |
err = t.Execute(w, pageData) | |
if err != nil { | |
_writeErr(err) | |
return | |
} | |
} | |
func toggleServiceHandler(w http.ResponseWriter, req *http.Request) { | |
// check csrf code | |
if req.Header.Get("Content-Type") != "application/json;charset=UTF-8" { | |
w.WriteHeader(http.StatusBadRequest) | |
w.Write([]byte("Bad Request")) | |
return | |
} | |
// unmarshal json | |
var reqJson map[string]uint32 | |
err := json.NewDecoder(req.Body).Decode(&reqJson) | |
if err != nil { | |
fmt.Printf("toggle request decode error: %s\n", err.Error()) | |
w.WriteHeader(http.StatusBadRequest) | |
w.Write([]byte("Bad Request")) | |
return | |
} | |
// reject if csrf code not match | |
if reqJson["csrf"] != currentCSRFCode.Load() { | |
w.WriteHeader(http.StatusForbidden) | |
w.Write([]byte("Forbidden")) | |
return | |
} | |
_writeErr := func(e error) { | |
w.WriteHeader(http.StatusInternalServerError) | |
w.Write([]byte(e.Error())) | |
} | |
ctx := context.Background() | |
conn, err := dbus.NewWithContext(ctx) | |
if err != nil { | |
_writeErr(err) | |
return | |
} | |
defer conn.Close() | |
ps, err := conn.GetUnitPropertiesContext(ctx, "wg-quick.target") | |
if err != nil { | |
_writeErr(err) | |
return | |
} | |
services := ps["ConsistsOf"].([]string) | |
opResults := make(chan string, len(services)) | |
allActive := true | |
for _, service := range services { | |
_timeLimitedCtx, c := context.WithTimeout(ctx, 5*time.Second) | |
a, err := conn.GetUnitPropertyContext(_timeLimitedCtx, service, "ActiveState") | |
fmt.Printf("%s -> %#v\n", service, a.Value.String()) | |
if err != nil { | |
c() | |
_writeErr(err) | |
return | |
} | |
allActive = allActive && (strings.Trim(a.Value.String(), " \"") == "active") | |
if !allActive { | |
c() | |
break | |
} | |
c() | |
} | |
for _, service := range services { | |
_timeLimitedCtx, c := context.WithTimeout(ctx, 5*time.Second) | |
if allActive { | |
_, err = conn.StopUnitContext(_timeLimitedCtx, service, "replace", opResults) | |
} else { | |
_, err = conn.StartUnitContext(_timeLimitedCtx, service, "replace", opResults) | |
} | |
if err != nil { | |
fmt.Printf("%v -> error: %v\n", service, err) | |
opResults <- "failed" | |
} | |
c() | |
} | |
_checkAllSuccess := func(w http.ResponseWriter) bool { | |
allSuccess := true | |
for i := 0; i < len(services); i++ { | |
r := strings.Trim(<-opResults, " \"") | |
fmt.Printf("toggle of %s result: %#v\n", services[i], r) | |
allSuccess = allSuccess && (r == "done") | |
} | |
if !allSuccess { | |
w.WriteHeader(http.StatusInternalServerError) | |
w.Write([]byte("Finished with errors, please refresh the page")) | |
return false | |
} | |
return true | |
} | |
if _checkAllSuccess(w) { | |
w.WriteHeader(http.StatusOK) | |
w.Write([]byte("OK")) | |
} | |
} | |
func main() { | |
// listen at a address / port / unix socket from argument | |
// use flag pkg to parse arguments | |
// flag vars | |
var addr string | |
var port uint | |
flag.StringVar(&addr, "addr", "", "ip address or unix socket file to listen") | |
flag.UintVar(&port, "port", 0, "port to listen") | |
flag.Parse() | |
// print help and exit if no argument provided | |
if args := os.Args; len(args) == 1 || (addr == "" && port == 0) { | |
flag.Usage() | |
return | |
} | |
listenAt := "" | |
var listener net.Listener | |
var err error | |
// if addr starts with "unix:" then ignore port | |
if strings.HasPrefix(addr, "unix:") { | |
listenAt = strings.TrimPrefix(addr, "unix:") | |
_ = os.Remove(listenAt) | |
listener, err = net.Listen("unix", listenAt) | |
if err == nil { | |
err = os.Chmod(listenAt, 0777) | |
} | |
} else { | |
if port == 0 { | |
port = 8080 | |
} | |
listenAt = fmt.Sprintf("%s:%d", addr, port) | |
listener, err = net.Listen("tcp", listenAt) | |
} | |
if err != nil { | |
fmt.Printf("error: %v\n", err) | |
return | |
} | |
http.HandleFunc("/", rootHandler) | |
http.HandleFunc("/toggleService", toggleServiceHandler) | |
fmt.Printf("listening at %s\n", listenAt) | |
err = http.Serve(listener, nil) | |
if err != nil { | |
fmt.Printf("error: %v\n", err) | |
return | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment