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,104 @@
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// storage manages certificate/key material on disk. Private keys are
|
||||
// written with 0600 permissions and their directories with 0700; nothing
|
||||
// under here is ever written to a log.
|
||||
type storage struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func newStorage(root string) (*storage, error) {
|
||||
if err := os.MkdirAll(filepath.Join(root, "accounts"), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, "certs"), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &storage{root: root}, nil
|
||||
}
|
||||
|
||||
func (s *storage) certDir(name string) string {
|
||||
return filepath.Join(s.root, "certs", safeName(name))
|
||||
}
|
||||
|
||||
func safeName(name string) string {
|
||||
out := make([]rune, 0, len(name))
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == '.':
|
||||
out = append(out, r)
|
||||
default:
|
||||
out = append(out, '_')
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return "cert"
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// SavePair writes fullchain.pem and privkey.pem for the given logical
|
||||
// certificate name and returns their paths.
|
||||
func (s *storage) SavePair(name string, certPEM, keyPEM []byte) (certPath, keyPath string, err error) {
|
||||
dir := s.certDir(name)
|
||||
if err = os.MkdirAll(dir, 0o700); err != nil {
|
||||
return
|
||||
}
|
||||
certPath = filepath.Join(dir, "fullchain.pem")
|
||||
keyPath = filepath.Join(dir, "privkey.pem")
|
||||
if err = os.WriteFile(certPath, certPEM, 0o644); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *storage) accountKeyPath() string {
|
||||
return filepath.Join(s.root, "accounts", "account.key")
|
||||
}
|
||||
|
||||
func loadTLSCertificate(certPath, keyPath string) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cert.Certificate) == 0 {
|
||||
return nil, errors.New("empty certificate chain")
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Leaf = leaf
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ValidateKeyPair checks that certPEM/keyPEM parse and form a matching
|
||||
// pair before anything is written to disk.
|
||||
func ValidateKeyPair(certPEM, keyPEM []byte) (*x509.Certificate, error) {
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate/key do not match: %w", err)
|
||||
}
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return nil, errors.New("no PEM data found in certificate")
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return leaf, nil
|
||||
}
|
||||
Reference in New Issue
Block a user