Initial vibeproxy implementation
Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owen/vibeproxy/internal/certificates"
|
||||
"github.com/owen/vibeproxy/internal/database"
|
||||
"github.com/owen/vibeproxy/internal/models"
|
||||
)
|
||||
|
||||
func (a *API) listCertificates(w http.ResponseWriter, r *http.Request) {
|
||||
certs, err := database.ListCertificates(a.db)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, certs)
|
||||
}
|
||||
|
||||
func (a *API) getCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
c, err := database.GetCertificate(a.db, id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, c)
|
||||
}
|
||||
|
||||
type issueCertRequest struct {
|
||||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"`
|
||||
Email string `json:"email"`
|
||||
Staging bool `json:"staging"`
|
||||
ChallengeType models.ChallengeType `json:"challenge_type"`
|
||||
DNSProvider string `json:"dns_provider"`
|
||||
}
|
||||
|
||||
func (a *API) issueCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
var req issueCertRequest
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.Name == "" || len(req.Domains) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "name and domains are required")
|
||||
return
|
||||
}
|
||||
if req.ChallengeType == "" {
|
||||
req.ChallengeType = models.ChallengeHTTP01
|
||||
}
|
||||
settings := a.store.Current().Settings
|
||||
email := req.Email
|
||||
if email == "" {
|
||||
email = settings.ACMEEmail
|
||||
}
|
||||
cert, err := a.certs.IssueLetsEncrypt(r.Context(), certOpts(req, email))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
audit(a.db, r, "certificate_issued", cert.Name, fmt.Sprintf("domains=%v challenge=%s", cert.Domains, cert.ChallengeType))
|
||||
writeJSON(w, http.StatusCreated, cert)
|
||||
}
|
||||
|
||||
func certOpts(req issueCertRequest, email string) certificates.IssueLetsEncryptOptions {
|
||||
return certificates.IssueLetsEncryptOptions{
|
||||
Name: req.Name,
|
||||
Domains: req.Domains,
|
||||
Email: email,
|
||||
Staging: req.Staging,
|
||||
ChallengeType: req.ChallengeType,
|
||||
DNSProvider: req.DNSProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) uploadCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"`
|
||||
CertPEM string `json:"cert_pem"`
|
||||
KeyPEM string `json:"key_pem"`
|
||||
}
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.CertPEM == "" || req.KeyPEM == "" {
|
||||
writeError(w, http.StatusBadRequest, "name, cert_pem, and key_pem are required")
|
||||
return
|
||||
}
|
||||
cert, err := a.certs.UploadCustom(req.Name, req.Domains, []byte(req.CertPEM), []byte(req.KeyPEM))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
audit(a.db, r, "certificate_issued", cert.Name, "custom certificate uploaded")
|
||||
writeJSON(w, http.StatusCreated, cert)
|
||||
}
|
||||
|
||||
func (a *API) renewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
c, err := database.GetCertificate(a.db, id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "certificate not found")
|
||||
return
|
||||
}
|
||||
if c.Type != models.CertTypeLetsEncrypt {
|
||||
writeError(w, http.StatusBadRequest, "only Let's Encrypt certificates can be renewed")
|
||||
return
|
||||
}
|
||||
settings := a.store.Current().Settings
|
||||
updated, err := a.certs.IssueLetsEncrypt(r.Context(), certOptsFromModel(*c, settings.ACMEEmail, settings.ACMEStaging))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
audit(a.db, r, "certificate_renewed", updated.Name, "")
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
func certOptsFromModel(c models.Certificate, email string, staging bool) certificates.IssueLetsEncryptOptions {
|
||||
return certificates.IssueLetsEncryptOptions{
|
||||
Name: c.Name,
|
||||
Domains: c.Domains,
|
||||
Email: email,
|
||||
Staging: staging,
|
||||
ChallengeType: c.ChallengeType,
|
||||
DNSProvider: c.DNSProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) deleteCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := database.DeleteCertificate(a.db, id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
a.certs.LoadAll()
|
||||
audit(a.db, r, "certificate_changed", fmt.Sprintf("id=%d", id), "deleted")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user