mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-23 09:31:02 +01:00
initial autocert support, readme update
This commit is contained in:
228
src/go-proxy/autocert.go
Normal file
228
src/go-proxy/autocert.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
)
|
||||
|
||||
type AutoCertConfig struct {
|
||||
Email string
|
||||
Domains []string `yaml:",flow"`
|
||||
Provider string
|
||||
Options map[string]string `yaml:",flow"`
|
||||
}
|
||||
|
||||
type AutoCertUser struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (u *AutoCertUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
func (u *AutoCertUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
func (u *AutoCertUser) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
type AutoCertProvider interface {
|
||||
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
GetName() string
|
||||
GetExpiry() time.Time
|
||||
LoadCert() bool
|
||||
ObtainCert() error
|
||||
|
||||
needRenew() bool
|
||||
}
|
||||
|
||||
func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) {
|
||||
if len(cfg.Domains) == 0 {
|
||||
return nil, fmt.Errorf("no domains specified")
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
return nil, fmt.Errorf("no provider specified")
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
return nil, fmt.Errorf("no email specified")
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate private key: %v", err)
|
||||
}
|
||||
user := &AutoCertUser{
|
||||
Email: cfg.Email,
|
||||
key: privKey,
|
||||
}
|
||||
legoCfg := lego.NewConfig(user)
|
||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||
legoClient, err := lego.NewClient(legoCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create lego client: %v", err)
|
||||
}
|
||||
base := &AutoCertProviderBase{
|
||||
name: cfg.Provider,
|
||||
cfg: cfg,
|
||||
user: user,
|
||||
legoCfg: legoCfg,
|
||||
client: legoClient,
|
||||
}
|
||||
switch cfg.Provider {
|
||||
case "cloudflare":
|
||||
return NewAutoCertCFProvider(base, cfg.Options)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown provider: %s", cfg.Provider)
|
||||
}
|
||||
|
||||
type AutoCertProviderBase struct {
|
||||
name string
|
||||
cfg AutoCertConfig
|
||||
user *AutoCertUser
|
||||
legoCfg *lego.Config
|
||||
client *lego.Client
|
||||
|
||||
tlsCert *tls.Certificate
|
||||
expiry time.Time
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if p.tlsCert == nil {
|
||||
aclog.Fatal("no certificate available")
|
||||
}
|
||||
if p.needRenew() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
if p.needRenew() {
|
||||
err := p.ObtainCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return p.tlsCert, nil
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) GetExpiry() time.Time {
|
||||
return p.expiry
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) ObtainCert() error {
|
||||
client := p.client
|
||||
if p.user.Registration == nil {
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.user.Registration = reg
|
||||
}
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: p.cfg.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.saveCert(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.tlsCert = &tlsCert
|
||||
x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[len(tlsCert.Certificate)-1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.expiry = x509Cert.NotAfter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) saveCert(cert *certificate.Resource) error {
|
||||
err := os.WriteFile(keyFileDefault, cert.PrivateKey, 0600) // -rw-------
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(certFileDefault, cert.Certificate, 0644) // -rw-r--r--
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) needRenew() bool {
|
||||
return p.expiry.Before(time.Now().Add(24 * time.Hour))
|
||||
}
|
||||
|
||||
func (p *AutoCertProviderBase) LoadCert() bool {
|
||||
cert, err := tls.LoadX509KeyPair(certFileDefault, keyFileDefault)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[len(cert.Certificate)-1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
p.tlsCert = &cert
|
||||
p.expiry = x509Cert.NotAfter
|
||||
return true
|
||||
}
|
||||
|
||||
type AutoCertCFProvider struct {
|
||||
*AutoCertProviderBase
|
||||
*cloudflare.Config
|
||||
}
|
||||
|
||||
func NewAutoCertCFProvider(base *AutoCertProviderBase, opt map[string]string) (*AutoCertCFProvider, error) {
|
||||
p := &AutoCertCFProvider{
|
||||
base,
|
||||
cloudflare.NewDefaultConfig(),
|
||||
}
|
||||
err := setOptions(p.Config, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
legoProvider, err := cloudflare.NewDNSProviderConfig(p.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create cloudflare provider: %v", err)
|
||||
}
|
||||
err = p.client.Challenge.SetDNS01Provider(legoProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to set challenge provider: %v", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func setOptions[T interface{}](cfg *T, opt map[string]string) error {
|
||||
for k, v := range opt {
|
||||
err := SetFieldFromSnake(cfg, k, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type Config interface {
|
||||
// Load() error
|
||||
MustLoad()
|
||||
GetAutoCertProvider() (AutoCertProvider, error)
|
||||
// MustReload()
|
||||
// Reload() error
|
||||
StartProviders()
|
||||
@@ -64,6 +65,10 @@ func (cfg *config) MustLoad() {
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *config) GetAutoCertProvider() (AutoCertProvider, error) {
|
||||
return cfg.AutoCert.GetProvider()
|
||||
}
|
||||
|
||||
func (cfg *config) Reload() error {
|
||||
return cfg.Load()
|
||||
}
|
||||
@@ -97,6 +102,7 @@ func (cfg *config) StopWatching() {
|
||||
|
||||
type config struct {
|
||||
Providers map[string]*Provider `yaml:",flow"`
|
||||
AutoCert AutoCertConfig `yaml:",flow"`
|
||||
watcher Watcher
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
@@ -63,11 +63,6 @@ const (
|
||||
ProviderKind_File = "file"
|
||||
)
|
||||
|
||||
const (
|
||||
certPath = "certs/cert.crt"
|
||||
keyPath = "certs/priv.key"
|
||||
)
|
||||
|
||||
// TODO: default + per proxy
|
||||
var (
|
||||
transport = &http.Transport{
|
||||
@@ -92,11 +87,13 @@ const wildcardLabelPrefix = "proxy.*."
|
||||
const clientUrlFromEnv = "FROM_ENV"
|
||||
|
||||
const (
|
||||
configPath = "config.yml"
|
||||
templatePath = "templates/panel.html"
|
||||
certFileDefault = "certs/cert.crt"
|
||||
keyFileDefault = "certs/priv.key"
|
||||
configPath = "config.yml"
|
||||
templatePath = "templates/panel.html"
|
||||
)
|
||||
|
||||
const StreamStopListenTimeout = 2 * time.Second
|
||||
const StreamStopListenTimeout = 1 * time.Second
|
||||
|
||||
const udpBufferSize = 1500
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,12 +16,7 @@ import (
|
||||
func (p *Provider) setConfigField(c *ProxyConfig, label string, value string, prefix string) error {
|
||||
if strings.HasPrefix(label, prefix) {
|
||||
field := strings.TrimPrefix(label, prefix)
|
||||
field = utils.snakeToCamel(field)
|
||||
prop := reflect.ValueOf(c).Elem().FieldByName(field)
|
||||
if prop.Kind() == 0 {
|
||||
return fmt.Errorf("ignoring unknown field %s", field)
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
SetFieldFromSnake(c, field, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -172,7 +166,7 @@ func (p *Provider) getDockerProxyConfigs() ([]*ProxyConfig, error) {
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
containerSlice, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to list containers: %v", err)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func isValidProxyPathMode(mode string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func redirectToTLS(w http.ResponseWriter, r *http.Request) {
|
||||
func redirectToTLSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Redirect to the same host but with HTTPS
|
||||
var redirectCode int
|
||||
if r.Method == http.MethodGet {
|
||||
@@ -155,7 +155,7 @@ func findHTTPRoute(host string, path string) (*HTTPRoute, error) {
|
||||
return nil, fmt.Errorf("no matching route for subdomain %s", subdomain)
|
||||
}
|
||||
|
||||
func httpProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
route, err := findHTTPRoute(r.Host, r.URL.Path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("request failed %s %s%s, error: %v",
|
||||
|
||||
@@ -8,3 +8,4 @@ var cfgl = logrus.WithField("component", "config")
|
||||
var hrlog = logrus.WithField("component", "http_proxy")
|
||||
var srlog = logrus.WithField("component", "stream")
|
||||
var wlog = logrus.WithField("component", "watcher")
|
||||
var aclog = logrus.WithField("component", "autocert")
|
||||
@@ -7,66 +7,80 @@ import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
|
||||
// flag.Parse()
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
DisableColors: false,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
InitFSWatcher()
|
||||
InitDockerWatcher()
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.MustLoad()
|
||||
|
||||
autoCertProvider, err := cfg.GetAutoCertProvider()
|
||||
|
||||
if err != nil {
|
||||
aclog.Warn(err)
|
||||
autoCertProvider = nil
|
||||
}
|
||||
|
||||
var httpProxyHandler http.Handler
|
||||
var httpPanelHandler http.Handler
|
||||
|
||||
var proxyServer *Server
|
||||
var panelServer *Server
|
||||
|
||||
if redirectHTTP {
|
||||
httpProxyHandler = http.HandlerFunc(redirectToTLSHandler)
|
||||
httpPanelHandler = http.HandlerFunc(redirectToTLSHandler)
|
||||
} else {
|
||||
httpProxyHandler = http.HandlerFunc(proxyHandler)
|
||||
httpPanelHandler = http.HandlerFunc(panelHandler)
|
||||
}
|
||||
|
||||
if autoCertProvider != nil {
|
||||
ok := autoCertProvider.LoadCert()
|
||||
if !ok {
|
||||
err := autoCertProvider.ObtainCert()
|
||||
if err != nil {
|
||||
aclog.Fatal("error obtaining certificate ", err)
|
||||
}
|
||||
}
|
||||
aclog.Infof("certificate will be expired at %v and get renewed", autoCertProvider.GetExpiry())
|
||||
|
||||
}
|
||||
proxyServer = NewServer(
|
||||
"proxy",
|
||||
autoCertProvider,
|
||||
":80",
|
||||
httpProxyHandler,
|
||||
":443",
|
||||
http.HandlerFunc(proxyHandler),
|
||||
)
|
||||
panelServer = NewServer(
|
||||
"panel",
|
||||
autoCertProvider,
|
||||
":8080",
|
||||
httpPanelHandler,
|
||||
":8443",
|
||||
http.HandlerFunc(panelHandler),
|
||||
)
|
||||
|
||||
proxyServer.Start()
|
||||
panelServer.Start()
|
||||
|
||||
InitFSWatcher()
|
||||
InitDockerWatcher()
|
||||
|
||||
cfg.StartProviders()
|
||||
cfg.WatchChanges()
|
||||
|
||||
var certAvailable = utils.fileOK(certPath) && utils.fileOK(keyPath)
|
||||
|
||||
go func() {
|
||||
log.Info("starting http server on port 80")
|
||||
if certAvailable && redirectHTTP {
|
||||
err = http.ListenAndServe(":80", http.HandlerFunc(redirectToTLS))
|
||||
} else {
|
||||
err = http.ListenAndServe(":80", http.HandlerFunc(httpProxyHandler))
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal("http server error: ", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
log.Infof("starting http panel on port 8080")
|
||||
err = http.ListenAndServe(":8080", http.HandlerFunc(panelHandler))
|
||||
if err != nil {
|
||||
log.Warning("http panel error: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if certAvailable {
|
||||
go func() {
|
||||
log.Info("starting https server on port 443")
|
||||
err = http.ListenAndServeTLS(":443", certPath, keyPath, http.HandlerFunc(httpProxyHandler))
|
||||
if err != nil {
|
||||
log.Fatal("https server error: ", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
log.Info("starting https panel on port 8443")
|
||||
err := http.ListenAndServeTLS(":8443", certPath, keyPath, http.HandlerFunc(panelHandler))
|
||||
if err != nil {
|
||||
log.Warning("http panel error: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT)
|
||||
signal.Notify(sig, syscall.SIGTERM)
|
||||
@@ -74,7 +88,9 @@ func main() {
|
||||
|
||||
<-sig
|
||||
cfg.StopWatching()
|
||||
cfg.StopProviders()
|
||||
StopFSWatcher()
|
||||
StopDockerWatcher()
|
||||
cfg.StopProviders()
|
||||
panelServer.Stop()
|
||||
proxyServer.Stop()
|
||||
}
|
||||
|
||||
97
src/go-proxy/server.go
Normal file
97
src/go-proxy/server.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Name string
|
||||
KeyFile string
|
||||
CertFile string
|
||||
CertProvider AutoCertProvider
|
||||
http *http.Server
|
||||
https *http.Server
|
||||
httpStarted bool
|
||||
httpsStarted bool
|
||||
}
|
||||
|
||||
func NewServer(name string, provider AutoCertProvider, httpAddr string, httpHandler http.Handler, httpsAddr string, httpsHandler http.Handler) *Server {
|
||||
if provider != nil {
|
||||
return &Server{
|
||||
Name: name,
|
||||
CertProvider: provider,
|
||||
http: &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: httpHandler,
|
||||
},
|
||||
https: &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: httpsHandler,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: provider.GetCert,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return &Server{
|
||||
Name: name,
|
||||
KeyFile: keyFileDefault,
|
||||
CertFile: certFileDefault,
|
||||
http: &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: httpHandler,
|
||||
},
|
||||
https: &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: httpsHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
if s.http != nil {
|
||||
s.httpStarted = true
|
||||
logrus.Printf("starting http %s server on %s", s.Name, s.http.Addr)
|
||||
go func() {
|
||||
err := s.http.ListenAndServe()
|
||||
s.handleErr("http", err)
|
||||
}()
|
||||
}
|
||||
|
||||
if s.https != nil && (s.CertProvider != nil || utils.fileOK(s.CertFile) && utils.fileOK(s.KeyFile)) {
|
||||
s.httpsStarted = true
|
||||
logrus.Printf("starting https %s server on %s", s.Name, s.https.Addr)
|
||||
go func() {
|
||||
err := s.https.ListenAndServeTLS(s.CertFile, s.KeyFile)
|
||||
s.handleErr("https", err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
|
||||
if s.httpStarted {
|
||||
errHTTP := s.http.Shutdown(ctx)
|
||||
s.handleErr("http", errHTTP)
|
||||
}
|
||||
|
||||
if s.httpsStarted {
|
||||
errHTTPS := s.https.Shutdown(ctx)
|
||||
s.handleErr("https", errHTTPS)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleErr(scheme string, err error) {
|
||||
switch err {
|
||||
case nil, http.ErrServerClosed:
|
||||
return
|
||||
default:
|
||||
logrus.Fatalf("failed to start %s %s server: %v", scheme, s.Name, err)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -91,7 +92,7 @@ func (*Utils) healthCheckStream(scheme, host string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Utils) snakeToCamel(s string) string {
|
||||
func (*Utils) snakeToPascal(s string) string {
|
||||
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
|
||||
return strings.ReplaceAll(toHyphenCamel, "-", "")
|
||||
}
|
||||
@@ -192,3 +193,13 @@ func (*Utils) fileOK(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func SetFieldFromSnake[T interface{}, VT interface{}](obj *T, field string, value VT) error {
|
||||
field = utils.snakeToPascal(field)
|
||||
prop := reflect.ValueOf(obj).Elem().FieldByName(field)
|
||||
if prop.Kind() == 0 {
|
||||
return fmt.Errorf("unknown field %s", field)
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user