mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-18 06:29:42 +02:00
refactor(api): restructured API for type safety, maintainability and docs generation
- These changes makes the API incombatible with previous versions - Added new types for error handling, success responses, and health checks. - Updated health check logic to utilize the new types for better clarity and structure. - Refactored existing handlers to improve response consistency and error handling. - Updated Makefile to include a new target for generating API types from Swagger. - Updated "new agent" API to respond an encrypted cert pair
This commit is contained in:
67
internal/api/v1/agent/common.go
Normal file
67
internal/api/v1/agent/common.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
)
|
||||
|
||||
type PEMPairResponse struct {
|
||||
Cert string `json:"cert" format:"base64"`
|
||||
Key string `json:"key" format:"base64"`
|
||||
} // @name PEMPairResponse
|
||||
|
||||
var encryptionKey atomic.Value
|
||||
|
||||
const rotateKeyInterval = 15 * time.Minute
|
||||
|
||||
func init() {
|
||||
if err := rotateKey(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to generate encryption key")
|
||||
}
|
||||
go func() {
|
||||
for range time.Tick(rotateKeyInterval) {
|
||||
if err := rotateKey(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to rotate encryption key")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func getEncryptionKey() []byte {
|
||||
return encryptionKey.Load().([]byte)
|
||||
}
|
||||
|
||||
func rotateKey() error {
|
||||
// generate a random 32 bytes key
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey.Store(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func toPEMPairResponse(encPEMPair agent.PEMPair) PEMPairResponse {
|
||||
return PEMPairResponse{
|
||||
Cert: base64.StdEncoding.EncodeToString(encPEMPair.Cert),
|
||||
Key: base64.StdEncoding.EncodeToString(encPEMPair.Key),
|
||||
}
|
||||
}
|
||||
|
||||
func fromEncryptedPEMPairResponse(pemPair PEMPairResponse) (agent.PEMPair, error) {
|
||||
encCert, err := base64.StdEncoding.DecodeString(pemPair.Cert)
|
||||
if err != nil {
|
||||
return agent.PEMPair{}, err
|
||||
}
|
||||
encKey, err := base64.StdEncoding.DecodeString(pemPair.Key)
|
||||
if err != nil {
|
||||
return agent.PEMPair{}, err
|
||||
}
|
||||
pair := agent.PEMPair{Cert: encCert, Key: encKey}
|
||||
return pair.Decrypt(getEncryptionKey())
|
||||
}
|
||||
103
internal/api/v1/agent/create.go
Normal file
103
internal/api/v1/agent/create.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
)
|
||||
|
||||
type NewAgentRequest struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
Host string `form:"host" validate:"required"`
|
||||
Port int `form:"port" validate:"required,min=1,max=65535"`
|
||||
Type string `form:"type" validate:"required,oneof=docker system"`
|
||||
Nightly bool `form:"nightly" validate:"omitempty"`
|
||||
} // @name NewAgentRequest
|
||||
|
||||
type NewAgentResponse struct {
|
||||
Compose string `json:"compose"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
} // @name NewAgentResponse
|
||||
|
||||
// @x-id "create"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Create a new agent
|
||||
// @Description Create a new agent and return the docker compose file, encrypted CA and client PEMs
|
||||
// @Description The returned PEMs are encrypted with a random key and will be used for verification when adding a new agent
|
||||
// @Tags agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body NewAgentRequest true "Request"
|
||||
// @Success 200 {object} NewAgentResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 409 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /agent/create [post]
|
||||
func Create(c *gin.Context) {
|
||||
var request NewAgentRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
hostport := fmt.Sprintf("%s:%d", request.Host, request.Port)
|
||||
if _, ok := agent.GetAgent(hostport); ok {
|
||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
var image string
|
||||
if request.Nightly {
|
||||
image = agent.DockerImageNightly
|
||||
} else {
|
||||
image = agent.DockerImageProduction
|
||||
}
|
||||
|
||||
ca, srv, client, err := agent.NewAgent()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create agent"))
|
||||
return
|
||||
}
|
||||
|
||||
var cfg agent.Generator = &agent.AgentEnvConfig{
|
||||
Name: request.Name,
|
||||
Port: request.Port,
|
||||
CACert: ca.String(),
|
||||
SSLCert: srv.String(),
|
||||
}
|
||||
if request.Type == "docker" {
|
||||
cfg = &agent.AgentComposeConfig{
|
||||
Image: image,
|
||||
AgentEnvConfig: cfg.(*agent.AgentEnvConfig),
|
||||
}
|
||||
}
|
||||
template, err := cfg.Generate()
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to generate agent config"))
|
||||
return
|
||||
}
|
||||
|
||||
key := getEncryptionKey()
|
||||
encCA, err := ca.Encrypt(key)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt CA PEMs"))
|
||||
return
|
||||
}
|
||||
encClient, err := client.Encrypt(key)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt client PEMs"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, NewAgentResponse{
|
||||
Compose: template,
|
||||
CA: toPEMPairResponse(encCA),
|
||||
Client: toPEMPairResponse(encClient),
|
||||
})
|
||||
}
|
||||
32
internal/api/v1/agent/list.go
Normal file
32
internal/api/v1/agent/list.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
||||
)
|
||||
|
||||
// @x-id "list"
|
||||
// @BasePath /api/v1
|
||||
// @Summary List agents
|
||||
// @Description List agents
|
||||
// @Tags agent,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} Agent
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /agent/list [get]
|
||||
func List(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
||||
return agent.ListAgents(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, agent.ListAgents())
|
||||
}
|
||||
}
|
||||
76
internal/api/v1/agent/verify.go
Normal file
76
internal/api/v1/agent/verify.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||
. "github.com/yusing/go-proxy/internal/api/types"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
)
|
||||
|
||||
type VerifyNewAgentRequest struct {
|
||||
Host string `json:"host"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
} // @name VerifyNewAgentRequest
|
||||
|
||||
// @x-id "verify"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Verify a new agent
|
||||
// @Description Verify a new agent and return the number of routes added
|
||||
// @Tags agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body VerifyNewAgentRequest true "Request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 403 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /agent/verify [post]
|
||||
func Verify(c *gin.Context) {
|
||||
var request VerifyNewAgentRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
filename, ok := certs.AgentCertsFilepath(request.Host)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid host", nil))
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := fromEncryptedPEMPairResponse(request.CA)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid CA", err))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := fromEncryptedPEMPairResponse(request.Client)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid client", err))
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
c.Error(InternalServerError(err, "failed to zip certs"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, zip, 0o600); err != nil {
|
||||
c.Error(InternalServerError(err, "failed to write certs"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||
}
|
||||
Reference in New Issue
Block a user