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
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
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)
|
|
}
|