package acme import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "io/ioutil" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" "imuslab.com/zoraxy/mod/utils" ) // You'll need a user or account type that implements acme.User type ACMEUser struct { Email string Registration *registration.Resource key crypto.PrivateKey } func (u *ACMEUser) GetEmail() string { return u.Email } func (u ACMEUser) GetRegistration() *registration.Resource { return u.Registration } func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey { return u.key } type ACMEHandler struct { email string acmeServer string port string } func NewACME(email string, acmeServer string, port string) *ACMEHandler { return &ACMEHandler{ email: email, acmeServer: acmeServer, port: port, } } func (a *ACMEHandler) ObtainCert(domains []string, certificateName string) (bool, error) { log.Println("Obtaining certificate...") privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Println(err) return false, err } log.Println(a.acmeServer) adminUser := ACMEUser{ Email: a.email, key: privateKey, } config := lego.NewConfig(&adminUser) config.CADirURL = a.acmeServer config.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(config) if err != nil { log.Println(err) return false, err } err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.port)) if err != nil { log.Println(err) return false, err } // New users will need to register reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { log.Println(err) return false, err } adminUser.Registration = reg request := certificate.ObtainRequest{ Domains: domains, Bundle: true, } certificates, err := client.Certificate.Obtain(request) if err != nil { log.Println(err) return false, err } // Each certificate comes back with the cert bytes, the bytes of the client's // private key, and a certificate URL. SAVE THESE TO DISK. err = ioutil.WriteFile("./certs/"+certificateName+".crt", certificates.Certificate, 0777) if err != nil { log.Println(err) return false, err } err = ioutil.WriteFile("./certs/"+certificateName+".key", certificates.PrivateKey, 0777) if err != nil { log.Println(err) return false, err } return true, nil } // Return a list of domains that is in expired certificates func (a *ACMEHandler) CheckCertificate() []string { filenames, err := os.ReadDir("./certs/") expiredCerts := []string{} if err != nil { log.Println(err) return []string{} } for _, filename := range filenames { certFilepath := filepath.Join("./certs/", filename.Name()) certBtyes, err := os.ReadFile(certFilepath) if err != nil { //Unable to load this file continue } else { //Cert loaded. Check its expire time block, _ := pem.Decode(certBtyes) if block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err == nil { elapsed := time.Since(cert.NotAfter) //approxMonths := -int(elapsed.Hours() / (24 * 30.44)) //approxDays := -int(elapsed.Hours()/24) % 30 if elapsed > 0 { //log.Println("Certificate", certFilepath, " expired") for _, dnsName := range cert.DNSNames { if !contains(expiredCerts, dnsName) { expiredCerts = append(expiredCerts, dnsName) } } if !contains(expiredCerts, cert.Subject.CommonName) { expiredCerts = append(expiredCerts, cert.Subject.CommonName) } } else { //log.Println("Certificate", certFilepath, " will still vaild for the next ", approxMonths, "m", approxDays, "d") } } } } } return expiredCerts } func (a *ACMEHandler) Getport() string { return a.port } func contains(slice []string, str string) bool { for _, s := range slice { if s == str { return true } } return false } func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) { type ExpiredDomains struct { Domain []string `json:"domain"` } info := ExpiredDomains{ Domain: a.CheckCertificate(), } js, _ := json.MarshalIndent(info, "", " ") utils.SendJSONResponse(w, string(js)) } func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) { domainPara, err := utils.GetPara(r, "domains") if err != nil { utils.SendErrorResponse(w, jsonEscape(err.Error())) return } filename, err := utils.GetPara(r, "filename") if err != nil { utils.SendErrorResponse(w, jsonEscape(err.Error())) return } domains := strings.Split(domainPara, ",") result, err := a.ObtainCert(domains, filename) if err != nil { utils.SendErrorResponse(w, jsonEscape(err.Error())) return } utils.SendJSONResponse(w, strconv.FormatBool(result)) } func jsonEscape(i string) string { b, err := json.Marshal(i) if err != nil { panic(err) } s := string(b) return s[1 : len(s)-1] }