Skip to content

Instantly share code, notes, and snippets.

@pnck
Last active June 28, 2023 20:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pnck/a33a4a3a1a121ce52a7b52fb0f599e61 to your computer and use it in GitHub Desktop.
Save pnck/a33a4a3a1a121ce52a7b52fb0f599e61 to your computer and use it in GitHub Desktop.
control warp service through web interface
// 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