feat: add greywall check and greywall setup commands
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.
This commit is contained in:
126
internal/proxy/detect.go
Normal file
126
internal/proxy/detect.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user