mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 04:42:33 +02:00

* feat: idle sleep for proxmox LXCs * refactor: replace deprecated docker api types * chore(api): remove debug task list endpoint * refactor: move servemux to gphttp/servemux; favicon.go to v1/favicon * refactor: introduce Pool interface, move agent_pool to agent module * refactor: simplify api code * feat: introduce debug api * refactor: remove net.URL and net.CIDR types, improved unmarshal handling * chore: update Makefile for debug build tag, update README * chore: add gperr.Unwrap method * feat: relative time and duration formatting * chore: add ROOT_DIR environment variable, refactor * migration: move homepage override and icon cache to $BASE_DIR/data, add migration code * fix: nil dereference on marshalling service health * fix: wait for route deletion * chore: enhance tasks debuggability * feat: stdout access logger and MultiWriter * fix(agent): remove agent properly on verify error * fix(metrics): disk exclusion logic and added corresponding tests * chore: update schema and prettify, fix package.json and Makefile * fix: I/O buffer not being shrunk before putting back to pool * feat: enhanced error handling module * chore: deps upgrade * feat: better value formatting and handling --------- Co-authored-by: yusing <yusing@6uo.me>
201 lines
4.6 KiB
Go
201 lines
4.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
|
"github.com/yusing/go-proxy/internal/gperr"
|
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
"github.com/yusing/go-proxy/pkg"
|
|
)
|
|
|
|
type AgentConfig struct {
|
|
Addr string
|
|
|
|
httpClient *http.Client
|
|
tlsConfig *tls.Config
|
|
name string
|
|
}
|
|
|
|
const (
|
|
EndpointVersion = "/version"
|
|
EndpointName = "/name"
|
|
EndpointProxyHTTP = "/proxy/http"
|
|
EndpointHealth = "/health"
|
|
EndpointLogs = "/logs"
|
|
EndpointSystemInfo = "/system_info"
|
|
|
|
AgentHost = CertsDNSName
|
|
|
|
APIEndpointBase = "/godoxy/agent"
|
|
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
|
|
|
DockerHost = "https://" + AgentHost
|
|
|
|
FakeDockerHostPrefix = "agent://"
|
|
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
|
|
)
|
|
|
|
var (
|
|
AgentURL, _ = url.Parse(APIBaseURL)
|
|
HTTPProxyURL, _ = url.Parse(APIBaseURL + EndpointProxyHTTP)
|
|
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
|
)
|
|
|
|
// TestAgentConfig is a helper function to create an AgentConfig for testing purposes.
|
|
// Not used in production.
|
|
func TestAgentConfig(name string, addr string) *AgentConfig {
|
|
return &AgentConfig{
|
|
name: name,
|
|
Addr: addr,
|
|
}
|
|
}
|
|
|
|
func IsDockerHostAgent(dockerHost string) bool {
|
|
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
|
}
|
|
|
|
func GetAgentAddrFromDockerHost(dockerHost string) string {
|
|
return dockerHost[FakeDockerHostPrefixLen:]
|
|
}
|
|
|
|
// Key implements pool.Object
|
|
func (cfg *AgentConfig) Key() string {
|
|
return cfg.Addr
|
|
}
|
|
|
|
func (cfg *AgentConfig) FakeDockerHost() string {
|
|
return FakeDockerHostPrefix + cfg.Addr
|
|
}
|
|
|
|
func (cfg *AgentConfig) Parse(addr string) error {
|
|
cfg.Addr = addr
|
|
return nil
|
|
}
|
|
|
|
func withoutBuildTime(version string) string {
|
|
return strings.Split(version, "-")[0]
|
|
}
|
|
|
|
func checkVersion(a, b string) bool {
|
|
return withoutBuildTime(a) == withoutBuildTime(b)
|
|
}
|
|
|
|
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
|
clientCert, err := tls.X509KeyPair(crt, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create tls config
|
|
caCertPool := x509.NewCertPool()
|
|
ok := caCertPool.AppendCertsFromPEM(ca)
|
|
if !ok {
|
|
return gperr.New("invalid ca certificate")
|
|
}
|
|
|
|
cfg.tlsConfig = &tls.Config{
|
|
Certificates: []tls.Certificate{clientCert},
|
|
RootCAs: caCertPool,
|
|
ServerName: CertsDNSName,
|
|
}
|
|
|
|
// create transport and http client
|
|
cfg.httpClient = cfg.NewHTTPClient()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
// check agent version
|
|
version, _, err := cfg.Fetch(ctx, EndpointVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
versionStr := string(version)
|
|
// skip version check for dev versions
|
|
if strings.HasPrefix(versionStr, "v") && !checkVersion(versionStr, pkg.GetVersion().String()) {
|
|
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), versionStr)
|
|
}
|
|
|
|
// get agent name
|
|
name, _, err := cfg.Fetch(ctx, EndpointName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.name = string(name)
|
|
return nil
|
|
}
|
|
|
|
func (cfg *AgentConfig) Init(ctx context.Context) gperr.Error {
|
|
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
|
if !ok {
|
|
return gperr.New("invalid agent host").Subject(cfg.Addr)
|
|
}
|
|
|
|
certData, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return gperr.Wrap(err, "failed to read agent certs")
|
|
}
|
|
|
|
ca, crt, key, err := certs.ExtractCert(certData)
|
|
if err != nil {
|
|
return gperr.Wrap(err, "failed to extract agent certs")
|
|
}
|
|
|
|
return gperr.Wrap(cfg.InitWithCerts(ctx, ca, crt, key))
|
|
}
|
|
|
|
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
|
return &http.Client{
|
|
Transport: cfg.Transport(),
|
|
}
|
|
}
|
|
|
|
func (cfg *AgentConfig) Transport() *http.Transport {
|
|
return &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
if addr != AgentHost+":443" {
|
|
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
|
}
|
|
if network != "tcp" {
|
|
return nil, &net.OpError{Op: "dial", Net: network, Source: nil, Addr: nil}
|
|
}
|
|
return cfg.DialContext(ctx)
|
|
},
|
|
TLSClientConfig: cfg.tlsConfig,
|
|
}
|
|
}
|
|
|
|
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
|
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
|
|
}
|
|
|
|
func (cfg *AgentConfig) IsInitialized() bool {
|
|
return cfg.name != ""
|
|
}
|
|
|
|
func (cfg *AgentConfig) Name() string {
|
|
return cfg.name
|
|
}
|
|
|
|
func (cfg *AgentConfig) String() string {
|
|
return cfg.name + "@" + cfg.Addr
|
|
}
|
|
|
|
// MarshalMap implements pool.Object
|
|
func (cfg *AgentConfig) MarshalMap() map[string]any {
|
|
return map[string]any{
|
|
"name": cfg.Name(),
|
|
"addr": cfg.Addr,
|
|
}
|
|
}
|