Add diagnostic and setup commands so users can verify their environment and install greyproxy without leaving greywall: - `greywall check`: shows version, platform deps, security features, and greyproxy installation/running status (absorbs old --version output) - `greywall setup`: downloads greyproxy from GitHub releases and shells out to `greyproxy install`, or auto-starts if already installed - `--version` simplified to single-line output for scripting New `internal/proxy/` package handles greyproxy detection (LookPath + /api/health endpoint), GitHub release fetching, tar.gz extraction, and service lifecycle management.
127 lines
3.8 KiB
Go
127 lines
3.8 KiB
Go
// Package proxy provides greyproxy detection, installation, and lifecycle management.
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
healthURL = "http://localhost:43080/api/health"
|
|
healthTimeout = 2 * time.Second
|
|
cmdTimeout = 5 * time.Second
|
|
)
|
|
|
|
// GreyproxyStatus holds the detected state of greyproxy.
|
|
type GreyproxyStatus struct {
|
|
Installed bool // found via exec.LookPath
|
|
Path string // full path from LookPath
|
|
Version string // parsed version (e.g. "0.1.1")
|
|
Running bool // health endpoint responded with valid greyproxy response
|
|
RunningErr error // error from the running check (for diagnostics)
|
|
}
|
|
|
|
// healthResponse is the expected JSON from GET /api/health.
|
|
type healthResponse struct {
|
|
Service string `json:"service"`
|
|
Version string `json:"version"`
|
|
Status string `json:"status"`
|
|
Ports map[string]int `json:"ports"`
|
|
}
|
|
|
|
var versionRegex = regexp.MustCompile(`^greyproxy\s+(\S+)`)
|
|
|
|
// Detect checks greyproxy installation status, version, and whether it's running.
|
|
// This function never returns an error; all detection failures are captured
|
|
// in the GreyproxyStatus fields so the caller can present them diagnostically.
|
|
func Detect() *GreyproxyStatus {
|
|
s := &GreyproxyStatus{}
|
|
|
|
// 1. Check if installed
|
|
s.Path, s.Installed = checkInstalled()
|
|
|
|
// 2. Check if running (via health endpoint)
|
|
running, ver, err := checkRunning()
|
|
s.Running = running
|
|
s.RunningErr = err
|
|
if running && ver != "" {
|
|
s.Version = ver
|
|
}
|
|
|
|
// 3. Version fallback: if installed but version not yet known, parse from CLI
|
|
if s.Installed && s.Version == "" {
|
|
s.Version, _ = checkVersion(s.Path)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// checkInstalled uses exec.LookPath to find greyproxy on PATH.
|
|
func checkInstalled() (path string, found bool) {
|
|
p, err := exec.LookPath("greyproxy")
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return p, true
|
|
}
|
|
|
|
// checkVersion runs "greyproxy -V" and parses the output.
|
|
// Expected format: "greyproxy 0.1.1 (go1.x linux/amd64)"
|
|
func checkVersion(binaryPath string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), cmdTimeout)
|
|
defer cancel()
|
|
|
|
out, err := exec.CommandContext(ctx, binaryPath, "-V").Output() //nolint:gosec // binaryPath comes from exec.LookPath
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to run greyproxy -V: %w", err)
|
|
}
|
|
|
|
matches := versionRegex.FindStringSubmatch(strings.TrimSpace(string(out)))
|
|
if len(matches) < 2 {
|
|
return "", fmt.Errorf("unexpected version output: %s", strings.TrimSpace(string(out)))
|
|
}
|
|
return matches[1], nil
|
|
}
|
|
|
|
// checkRunning hits GET http://localhost:43080/api/health and verifies
|
|
// the response is from greyproxy (not some other service on that port).
|
|
// Returns running status, version string from health response, and any error.
|
|
func checkRunning() (bool, string, error) {
|
|
client := &http.Client{Timeout: healthTimeout}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), healthTimeout)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req) //nolint:gosec // healthURL is a hardcoded localhost constant
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("health check failed: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return false, "", fmt.Errorf("health check returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var health healthResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
|
|
return false, "", fmt.Errorf("failed to parse health response: %w", err)
|
|
}
|
|
|
|
if health.Service != "greyproxy" {
|
|
return false, "", fmt.Errorf("unexpected service: %q (expected greyproxy)", health.Service)
|
|
}
|
|
|
|
return true, health.Version, nil
|
|
}
|