@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"gpo-distributor/internal/agent"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
defaultConfig := filepath.Join(os.Getenv("ProgramData"), "GPO-Distributor", "agent.json")
|
||||
if runtime.GOOS != "windows" && os.Getenv("ProgramData") == "" {
|
||||
defaultConfig = "/etc/gpo-distributor/agent.json"
|
||||
}
|
||||
configFile := flag.String("config", defaultConfig, "agent configuration JSON")
|
||||
once := flag.Bool("once", false, "synchronize once and exit")
|
||||
showVersion := flag.Bool("version", false, "print version")
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
log.Fatal("gpo-agent can apply policies only on Windows")
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout, "gpo-agent ", log.LstdFlags|log.LUTC)
|
||||
cfg, err := agent.LoadConfig(*configFile)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
runner, err := agent.New(cfg, version, logger)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
if err := runner.Run(ctx, *once); err != nil && err != context.Canceled {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gpo-distributor/internal/model"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
type clientConfig struct {
|
||||
server string
|
||||
token string
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "upload":
|
||||
err = upload(os.Args[2:])
|
||||
case "profile-set":
|
||||
err = profileSet(os.Args[2:])
|
||||
case "policies":
|
||||
err = getList(os.Args[2:], "/api/v1/admin/policies")
|
||||
case "profiles":
|
||||
err = getList(os.Args[2:], "/api/v1/admin/profiles")
|
||||
case "clients":
|
||||
err = getList(os.Args[2:], "/api/v1/admin/clients")
|
||||
case "version":
|
||||
fmt.Println(version)
|
||||
return
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func common(fs *flag.FlagSet) (*string, *string, *bool) {
|
||||
server := fs.String("server", os.Getenv("GPO_SERVER_URL"), "server base URL")
|
||||
token := fs.String("token", os.Getenv("GPO_ADMIN_TOKEN"), "admin bearer token")
|
||||
insecure := fs.Bool("insecure-skip-verify", false, "skip TLS certificate validation (test only)")
|
||||
return server, token, insecure
|
||||
}
|
||||
|
||||
func upload(args []string) error {
|
||||
fs := flag.NewFlagSet("upload", flag.ContinueOnError)
|
||||
server, token, insecure := common(fs)
|
||||
policy := fs.String("policy", "", "policy name")
|
||||
file := fs.String("file", "", "GPO backup ZIP")
|
||||
note := fs.String("note", "", "version note")
|
||||
force := fs.Bool("force", false, "create a new version even when the semantic hash already exists")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := validateClient(*server, *token, *insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *policy == "" || *file == "" {
|
||||
return errors.New("-policy and -file are required")
|
||||
}
|
||||
f, err := os.Open(*file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
mw := multipart.NewWriter(pw)
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
part, err := mw.CreateFormFile("bundle", filepath.Base(*file))
|
||||
if err == nil {
|
||||
_, err = io.Copy(part, f)
|
||||
}
|
||||
if err == nil && *note != "" {
|
||||
err = mw.WriteField("note", *note)
|
||||
}
|
||||
if err == nil && *force {
|
||||
err = mw.WriteField("force", "true")
|
||||
}
|
||||
closeErr := mw.Close()
|
||||
if err != nil {
|
||||
_ = pw.CloseWithError(err)
|
||||
} else if closeErr != nil {
|
||||
_ = pw.CloseWithError(closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
endpoint := strings.TrimRight(cfg.server, "/") + "/api/v1/admin/policies/" + url.PathEscape(*policy) + "/versions"
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.token)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
return doAndPrint(cfg, req)
|
||||
}
|
||||
|
||||
func profileSet(args []string) error {
|
||||
fs := flag.NewFlagSet("profile-set", flag.ContinueOnError)
|
||||
server, token, insecure := common(fs)
|
||||
name := fs.String("name", "", "profile name")
|
||||
var policies multiFlag
|
||||
fs.Var(&policies, "policy", "ordered policy reference name@latest or name@version; repeatable")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := validateClient(*server, *token, *insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *name == "" || len(policies) == 0 {
|
||||
return errors.New("-name and at least one -policy are required")
|
||||
}
|
||||
refs := make([]model.ProfilePolicy, 0, len(policies))
|
||||
for _, value := range policies {
|
||||
policy, ver, ok := strings.Cut(value, "@")
|
||||
if !ok || policy == "" || ver == "" {
|
||||
return fmt.Errorf("invalid policy reference %q; expected name@latest or name@version", value)
|
||||
}
|
||||
refs = append(refs, model.ProfilePolicy{Policy: policy, Version: ver})
|
||||
}
|
||||
body, _ := json.Marshal(map[string]any{"policies": refs})
|
||||
endpoint := strings.TrimRight(cfg.server, "/") + "/api/v1/admin/profiles/" + url.PathEscape(*name)
|
||||
req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return doAndPrint(cfg, req)
|
||||
}
|
||||
|
||||
func getList(args []string, path string) error {
|
||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||
server, token, insecure := common(fs)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := validateClient(*server, *token, *insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, strings.TrimRight(cfg.server, "/")+path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.token)
|
||||
return doAndPrint(cfg, req)
|
||||
}
|
||||
|
||||
func validateClient(server, token string, insecure bool) (clientConfig, error) {
|
||||
if server == "" || token == "" {
|
||||
return clientConfig{}, errors.New("server and token are required")
|
||||
}
|
||||
u, err := url.Parse(server)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return clientConfig{}, errors.New("server must be an absolute URL")
|
||||
}
|
||||
if u.Scheme != "https" && !insecure {
|
||||
return clientConfig{}, errors.New("server must use https (or -insecure-skip-verify for tests)")
|
||||
}
|
||||
return clientConfig{server: server, token: token, insecure: insecure}, nil
|
||||
}
|
||||
|
||||
func doAndPrint(cfg clientConfig, req *http.Request) error {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: cfg.insecure} // #nosec G402: explicit test option.
|
||||
client := &http.Client{Transport: tr, Timeout: 15 * time.Minute}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 20<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
var pretty bytes.Buffer
|
||||
if json.Indent(&pretty, body, "", " ") == nil {
|
||||
fmt.Println(pretty.String())
|
||||
} else {
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type multiFlag []string
|
||||
|
||||
func (m *multiFlag) String() string { return strings.Join(*m, ",") }
|
||||
func (m *multiFlag) Set(v string) error { *m = append(*m, v); return nil }
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `Usage:
|
||||
gpoctl upload -server https://host:8443 -token TOKEN -policy NAME -file BACKUP.zip [-note TEXT] [-force]
|
||||
gpoctl profile-set -server URL -token TOKEN -name PROFILE -policy NAME@latest [-policy NAME@VERSION]
|
||||
gpoctl policies|profiles|clients -server URL -token TOKEN
|
||||
gpoctl version`)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gpo-distributor/internal/httpapi"
|
||||
"gpo-distributor/internal/store"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
listen := flag.String("listen", env("GPO_SERVER_LISTEN", ":8443"), "listen address")
|
||||
dataDir := flag.String("data", env("GPO_SERVER_DATA", "./data"), "data directory")
|
||||
adminToken := flag.String("admin-token", os.Getenv("GPO_SERVER_ADMIN_TOKEN"), "admin bearer token")
|
||||
clientToken := flag.String("client-token", os.Getenv("GPO_SERVER_CLIENT_TOKEN"), "client bearer token")
|
||||
signingKey := flag.String("signing-key", os.Getenv("GPO_SERVER_SIGNING_KEY"), "manifest HMAC signing key")
|
||||
tlsCert := flag.String("tls-cert", os.Getenv("GPO_SERVER_TLS_CERT"), "TLS certificate path")
|
||||
tlsKey := flag.String("tls-key", os.Getenv("GPO_SERVER_TLS_KEY"), "TLS private key path")
|
||||
maxUpload := flag.Int64("max-upload", 512<<20, "maximum upload size in bytes")
|
||||
showVersion := flag.Bool("version", false, "print version")
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout, "gpo-server ", log.LstdFlags|log.LUTC)
|
||||
st, err := store.Open(*dataDir)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
api, err := httpapi.New(st, httpapi.Config{
|
||||
AdminToken: *adminToken, ClientToken: *clientToken, SigningKey: *signingKey,
|
||||
ServerVersion: version, MaxUpload: *maxUpload, Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *listen, Handler: api.Handler(),
|
||||
ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 15 * time.Minute,
|
||||
WriteTimeout: 15 * time.Minute, IdleTimeout: 2 * time.Minute,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Printf("version=%s listen=%s data=%s", version, *listen, *dataDir)
|
||||
var err error
|
||||
if *tlsCert != "" || *tlsKey != "" {
|
||||
if *tlsCert == "" || *tlsKey == "" {
|
||||
logger.Fatal("both -tls-cert and -tls-key are required")
|
||||
}
|
||||
err = srv.ListenAndServeTLS(*tlsCert, *tlsKey)
|
||||
} else {
|
||||
logger.Printf("WARNING: TLS disabled; use only behind a TLS reverse proxy or in a test network")
|
||||
err = srv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.Printf("shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func env(name, fallback string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user