[SSL证书]免费证书申请增加HTTP认证方式

This commit is contained in:
GoEdgeLab
2020-12-03 18:19:22 +08:00
parent b8bad79859
commit 0df86527c8
20 changed files with 369 additions and 57 deletions

View File

@@ -0,0 +1,3 @@
package acme
type AuthCallback func(domain, token, keyAuth string)

View File

@@ -0,0 +1,23 @@
package acme
type HTTPProvider struct {
onAuth AuthCallback
}
func NewHTTPProvider(onAuth AuthCallback) *HTTPProvider {
return &HTTPProvider{
onAuth: onAuth,
}
}
func (this *HTTPProvider) Present(domain, token, keyAuth string) error {
if this.onAuth != nil {
this.onAuth(domain, token, keyAuth)
}
//http01.ChallengePath()
return nil
}
func (this *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}

View File

@@ -14,7 +14,8 @@ import (
type Request struct { type Request struct {
debug bool debug bool
task *Task task *Task
onAuth AuthCallback
} }
func NewRequest(task *Task) *Request { func NewRequest(task *Task) *Request {
@@ -27,7 +28,23 @@ func (this *Request) Debug() {
this.debug = true this.debug = true
} }
func (this *Request) OnAuth(onAuth AuthCallback) {
this.onAuth = onAuth
}
func (this *Request) Run() (certData []byte, keyData []byte, err error) { func (this *Request) Run() (certData []byte, keyData []byte, err error) {
switch this.task.AuthType {
case AuthTypeDNS:
return this.runDNS()
case AuthTypeHTTP:
return this.runHTTP()
default:
err = errors.New("invalid task type '" + this.task.AuthType + "'")
return
}
}
func (this *Request) runDNS() (certData []byte, keyData []byte, err error) {
if !this.debug { if !this.debug {
acmelog.Logger = log.New(ioutil.Discard, "", log.LstdFlags) acmelog.Logger = log.New(ioutil.Discard, "", log.LstdFlags)
} }
@@ -92,3 +109,57 @@ func (this *Request) Run() (certData []byte, keyData []byte, err error) {
return certResource.Certificate, certResource.PrivateKey, nil return certResource.Certificate, certResource.PrivateKey, nil
} }
func (this *Request) runHTTP() (certData []byte, keyData []byte, err error) {
if !this.debug {
acmelog.Logger = log.New(ioutil.Discard, "", log.LstdFlags)
}
if this.task.User == nil {
err = errors.New("'user' must not be nil")
return
}
config := lego.NewConfig(this.task.User)
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
return nil, nil, err
}
// 注册用户
resource := this.task.User.GetRegistration()
if resource != nil {
resource, err = client.Registration.QueryRegistration()
if err != nil {
return nil, nil, err
}
} else {
resource, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, nil, err
}
err = this.task.User.Register(resource)
if err != nil {
return nil, nil, err
}
}
err = client.Challenge.SetHTTP01Provider(NewHTTPProvider(this.onAuth))
if err != nil {
return nil, nil, err
}
// 申请证书
request := certificate.ObtainRequest{
Domains: this.task.Domains,
Bundle: true,
}
certResource, err := client.Certificate.Obtain(request)
if err != nil {
return nil, nil, err
}
return certResource.Certificate, certResource.PrivateKey, nil
}

View File

@@ -11,7 +11,7 @@ import (
"testing" "testing"
) )
func TestNewRequest(t *testing.T) { func TestRequest_Run_DNS(t *testing.T) {
privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd") privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -39,6 +39,7 @@ func TestNewRequest(t *testing.T) {
req := NewRequest(&Task{ req := NewRequest(&Task{
User: user, User: user,
Type: TaskTypeDNS,
DNSProvider: dnsProvider, DNSProvider: dnsProvider,
DNSDomain: "yun4s.cn", DNSDomain: "yun4s.cn",
Domains: []string{"yun4s.cn"}, Domains: []string{"yun4s.cn"},
@@ -51,6 +52,40 @@ func TestNewRequest(t *testing.T) {
t.Log("key:", string(keyData)) t.Log("key:", string(keyData))
} }
func TestRequest_Run_HTTP(t *testing.T) {
privateKey, err := ParsePrivateKeyFromBase64("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD3xxDXP4YVqHCfub21Yi3QL1Kvgow23J8CKJ7vU3L4+hRANCAARRl5ZKAlgGRc5RETSMYFCTXvjnePDgjALWgtgfClQGLB2rGyRecJvlesAM6Q7LQrDxVxvxdSQQmPGRqJGiBtjd")
if err != nil {
t.Fatal(err)
}
user := NewUser("19644627@qq.com", privateKey, func(resource *registration.Resource) error {
resourceJSON, err := json.Marshal(resource)
if err != nil {
return err
}
t.Log(string(resourceJSON))
return nil
})
regResource := []byte(`{"body":{"status":"valid","contact":["mailto:19644627@qq.com"]},"uri":"https://acme-v02.api.letsencrypt.org/acme/acct/103672877"}`)
err = user.SetRegistration(regResource)
if err != nil {
t.Fatal(err)
}
req := NewRequest(&Task{
User: user,
Type: TaskTypeHTTP,
Domains: []string{"teaos.cn", "www.teaos.cn", "meloy.cn"},
})
certData, keyData, err := req.runHTTP()
if err != nil {
t.Fatal(err)
}
t.Log(string(certData))
t.Log(string(keyData))
}
func testDNSPodProvider() (dnsclients.ProviderInterface, error) { func testDNSPodProvider() (dnsclients.ProviderInterface, error) {
db, err := dbs.Default() db, err := dbs.Default()
if err != nil { if err != nil {

View File

@@ -2,9 +2,19 @@ package acme
import "github.com/TeaOSLab/EdgeAPI/internal/dnsclients" import "github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
type AuthType = string
const (
AuthTypeDNS AuthType = "dns"
AuthTypeHTTP AuthType = "http"
)
type Task struct { type Task struct {
User *User User *User
AuthType AuthType
Domains []string
// DNS相关
DNSProvider dnsclients.ProviderInterface DNSProvider dnsclients.ProviderInterface
DNSDomain string DNSDomain string
Domains []string
} }

View File

@@ -1,7 +1,7 @@
package teaconst package teaconst
const ( const (
Version = "0.0.4" Version = "0.0.5"
ProductName = "Edge API" ProductName = "Edge API"
ProcessName = "edge-api" ProcessName = "edge-api"

View File

@@ -0,0 +1,54 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
)
type ACMEAuthenticationDAO dbs.DAO
func NewACMEAuthenticationDAO() *ACMEAuthenticationDAO {
return dbs.NewDAO(&ACMEAuthenticationDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeACMEAuthentications",
Model: new(ACMEAuthentication),
PkName: "id",
},
}).(*ACMEAuthenticationDAO)
}
var SharedACMEAuthenticationDAO *ACMEAuthenticationDAO
func init() {
dbs.OnReady(func() {
SharedACMEAuthenticationDAO = NewACMEAuthenticationDAO()
})
}
// 创建认证信息
func (this *ACMEAuthenticationDAO) CreateAuth(taskId int64, domain string, token string, key string) error {
op := NewACMEAuthenticationOperator()
op.TaskId = taskId
op.Domain = domain
op.Token = token
op.Key = key
_, err := this.Save(op)
return err
}
// 根据令牌查找认证信息
func (this *ACMEAuthenticationDAO) FindAuthWithToken(token string) (*ACMEAuthentication, error) {
one, err := this.Query().
Attr("token", token).
DescPk().
Find()
if err != nil {
return nil, err
}
if one == nil {
return nil, nil
}
return one.(*ACMEAuthentication), nil
}

View File

@@ -0,0 +1,5 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
)

View File

@@ -0,0 +1,24 @@
package models
// ACME认证
type ACMEAuthentication struct {
Id uint64 `field:"id"` // ID
TaskId uint64 `field:"taskId"` // 任务ID
Domain string `field:"domain"` // 域名
Token string `field:"token"` // 令牌
Key string `field:"key"` // 密钥
CreatedAt uint64 `field:"createdAt"` // 创建时间
}
type ACMEAuthenticationOperator struct {
Id interface{} // ID
TaskId interface{} // 任务ID
Domain interface{} // 域名
Token interface{} // 令牌
Key interface{} // 密钥
CreatedAt interface{} // 创建时间
}
func NewACMEAuthenticationOperator() *ACMEAuthenticationOperator {
return &ACMEAuthenticationOperator{}
}

View File

@@ -0,0 +1 @@
package models

View File

@@ -115,10 +115,11 @@ func (this *ACMETaskDAO) ListEnabledACMETasks(adminId int64, userId int64, offse
} }
// 创建任务 // 创建任务
func (this *ACMETaskDAO) CreateACMETask(adminId int64, userId int64, acmeUserId int64, dnsProviderId int64, dnsDomain string, domains []string, autoRenew bool) (int64, error) { func (this *ACMETaskDAO) CreateACMETask(adminId int64, userId int64, authType acme.AuthType, acmeUserId int64, dnsProviderId int64, dnsDomain string, domains []string, autoRenew bool) (int64, error) {
op := NewACMETaskOperator() op := NewACMETaskOperator()
op.AdminId = adminId op.AdminId = adminId
op.UserId = userId op.UserId = userId
op.AuthType = authType
op.AcmeUserId = acmeUserId op.AcmeUserId = acmeUserId
op.DnsProviderId = dnsProviderId op.DnsProviderId = dnsProviderId
op.DnsDomain = dnsDomain op.DnsDomain = dnsDomain
@@ -255,37 +256,55 @@ func (this *ACMETaskDAO) runTaskWithoutLog(taskId int64) (isOk bool, errMsg stri
} }
} }
// DNS服务商 var acmeTask *acme.Task = nil
dnsProvider, err := SharedDNSProviderDAO.FindEnabledDNSProvider(int64(task.DnsProviderId)) if task.AuthType == acme.AuthTypeDNS {
if err != nil { // DNS服务商
errMsg = "查找DNS服务商账号信息时出错" + err.Error() dnsProvider, err := SharedDNSProviderDAO.FindEnabledDNSProvider(int64(task.DnsProviderId))
return if err != nil {
} errMsg = "查找DNS服务商账号信息时出错" + err.Error()
if dnsProvider == nil { return
errMsg = "找不到DNS服务商账号" }
return if dnsProvider == nil {
} errMsg = "找不到DNS服务商账号"
providerInterface := dnsclients.FindProvider(dnsProvider.Type) return
if providerInterface == nil { }
errMsg = "暂不支持此类型的DNS服务商 '" + dnsProvider.Type + "'" providerInterface := dnsclients.FindProvider(dnsProvider.Type)
return if providerInterface == nil {
} errMsg = "暂不支持此类型的DNS服务商 '" + dnsProvider.Type + "'"
apiParams, err := dnsProvider.DecodeAPIParams() return
if err != nil { }
errMsg = "解析DNS服务商API参数时出错" + err.Error() apiParams, err := dnsProvider.DecodeAPIParams()
return if err != nil {
} errMsg = "解析DNS服务商API参数时出错" + err.Error()
err = providerInterface.Auth(apiParams) return
if err != nil { }
errMsg = "校验DNS服务商API参数时出错" + err.Error() err = providerInterface.Auth(apiParams)
return if err != nil {
errMsg = "校验DNS服务商API参数时出错" + err.Error()
return
}
acmeTask = &acme.Task{
User: remoteUser,
AuthType: acme.AuthTypeDNS,
DNSProvider: providerInterface,
DNSDomain: task.DnsDomain,
Domains: task.DecodeDomains(),
}
} else if task.AuthType == acme.AuthTypeHTTP {
acmeTask = &acme.Task{
User: remoteUser,
AuthType: acme.AuthTypeHTTP,
Domains: task.DecodeDomains(),
}
} }
acmeRequest := acme.NewRequest(&acme.Task{ acmeRequest := acme.NewRequest(acmeTask)
User: remoteUser, acmeRequest.OnAuth(func(domain, token, keyAuth string) {
DNSProvider: providerInterface, err := SharedACMEAuthenticationDAO.CreateAuth(taskId, domain, token, keyAuth)
DNSDomain: task.DnsDomain, if err != nil {
Domains: task.DecodeDomains(), logs.Println("[ACME]write authentication to database error: " + err.Error())
}
}) })
certData, keyData, err := acmeRequest.Run() certData, keyData, err := acmeRequest.Run()
if err != nil { if err != nil {

View File

@@ -14,6 +14,7 @@ type ACMETask struct {
State uint8 `field:"state"` // 状态 State uint8 `field:"state"` // 状态
CertId uint64 `field:"certId"` // 生成的证书ID CertId uint64 `field:"certId"` // 生成的证书ID
AutoRenew uint8 `field:"autoRenew"` // 是否自动更新 AutoRenew uint8 `field:"autoRenew"` // 是否自动更新
AuthType string `field:"authType"` // 认证类型
} }
type ACMETaskOperator struct { type ACMETaskOperator struct {
@@ -29,6 +30,7 @@ type ACMETaskOperator struct {
State interface{} // 状态 State interface{} // 状态
CertId interface{} // 生成的证书ID CertId interface{} // 生成的证书ID
AutoRenew interface{} // 是否自动更新 AutoRenew interface{} // 是否自动更新
AuthType interface{} // 认证类型
} }
func NewACMETaskOperator() *ACMETaskOperator { func NewACMETaskOperator() *ACMETaskOperator {

View File

@@ -637,8 +637,7 @@ func (this *NodeDAO) FindEnabledNodeDNS(nodeId int64) (*Node, error) {
one, err := this.Query(). one, err := this.Query().
State(NodeStateEnabled). State(NodeStateEnabled).
Pk(nodeId). Pk(nodeId).
Attr("isOn", true). Result("id", "name", "dnsRoutes", "clusterId", "isOn").
Result("id", "name", "dnsRoutes", "clusterId").
Find() Find()
if err != nil || one == nil { if err != nil || one == nil {
return nil, err return nil, err

View File

@@ -199,6 +199,7 @@ func (this *APINode) listenRPC(listener net.Listener, tlsConfig *tls.Config) err
pb.RegisterDNSServiceServer(rpcServer, &services.DNSService{}) pb.RegisterDNSServiceServer(rpcServer, &services.DNSService{})
pb.RegisterACMEUserServiceServer(rpcServer, &services.ACMEUserService{}) pb.RegisterACMEUserServiceServer(rpcServer, &services.ACMEUserService{})
pb.RegisterACMETaskServiceServer(rpcServer, &services.ACMETaskService{}) pb.RegisterACMETaskServiceServer(rpcServer, &services.ACMETaskService{})
pb.RegisterACMEAuthenticationServiceServer(rpcServer, &services.ACMEAuthenticationService{})
err := rpcServer.Serve(listener) err := rpcServer.Serve(listener)
if err != nil { if err != nil {
return errors.New("[API_NODE]start rpc failed: " + err.Error()) return errors.New("[API_NODE]start rpc failed: " + err.Error())

View File

@@ -0,0 +1,33 @@
package services
import (
"context"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
// ACME认证相关
type ACMEAuthenticationService struct {
BaseService
}
// 获取Key
func (this *ACMEAuthenticationService) FindACMEAuthenticationKeyWithToken(ctx context.Context, req *pb.FindACMEAuthenticationKeyWithTokenRequest) (*pb.FindACMEAuthenticationKeyWithTokenResponse, error) {
_, err := this.ValidateNode(ctx)
if err != nil {
return nil, err
}
if len(req.Token) == 0 {
return nil, errors.New("'token' should not be empty")
}
auth, err := models.SharedACMEAuthenticationDAO.FindAuthWithToken(req.Token)
if err != nil {
return nil, err
}
if auth == nil {
return &pb.FindACMEAuthenticationKeyWithTokenResponse{Key: ""}, nil
}
return &pb.FindACMEAuthenticationKeyWithTokenResponse{Key: auth.Key}, nil
}

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"github.com/TeaOSLab/EdgeAPI/internal/acme"
"github.com/TeaOSLab/EdgeAPI/internal/db/models" "github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients" "github.com/TeaOSLab/EdgeAPI/internal/dnsclients"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -87,19 +88,22 @@ func (this *ACMETaskService) ListEnabledACMETasks(ctx context.Context, req *pb.L
CreatedAt: int64(acmeUser.CreatedAt), CreatedAt: int64(acmeUser.CreatedAt),
} }
// DNS var pbProvider *pb.DNSProvider
provider, err := models.SharedDNSProviderDAO.FindEnabledDNSProvider(int64(task.DnsProviderId)) if task.AuthType == acme.AuthTypeDNS {
if err != nil { // DNS
return nil, err provider, err := models.SharedDNSProviderDAO.FindEnabledDNSProvider(int64(task.DnsProviderId))
} if err != nil {
if provider == nil { return nil, err
continue }
} if provider == nil {
pbProvider := &pb.DNSProvider{ continue
Id: int64(provider.Id), }
Name: provider.Name, pbProvider = &pb.DNSProvider{
Type: provider.Type, Id: int64(provider.Id),
TypeName: dnsclients.FindProviderTypeName(provider.Type), Name: provider.Name,
Type: provider.Type,
TypeName: dnsclients.FindProviderTypeName(provider.Type),
}
} }
// 证书 // 证书
@@ -147,6 +151,7 @@ func (this *ACMETaskService) ListEnabledACMETasks(ctx context.Context, req *pb.L
DnsProvider: pbProvider, DnsProvider: pbProvider,
SslCert: pbCert, SslCert: pbCert,
LatestACMETaskLog: pbTaskLog, LatestACMETaskLog: pbTaskLog,
AuthType: task.AuthType,
}) })
} }
@@ -159,7 +164,12 @@ func (this *ACMETaskService) CreateACMETask(ctx context.Context, req *pb.CreateA
if err != nil { if err != nil {
return nil, err return nil, err
} }
taskId, err := models.SharedACMETaskDAO.CreateACMETask(adminId, userId, req.AcmeUserId, req.DnsProviderId, req.DnsDomain, req.Domains, req.AutoRenew)
if len(req.AuthType) == 0 {
req.AuthType = acme.AuthTypeDNS
}
taskId, err := models.SharedACMETaskDAO.CreateACMETask(adminId, userId, req.AuthType, req.AcmeUserId, req.DnsProviderId, req.DnsDomain, req.Domains, req.AutoRenew)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -298,5 +308,6 @@ func (this *ACMETaskService) FindEnabledACMETask(ctx context.Context, req *pb.Fi
AutoRenew: task.AutoRenew == 1, AutoRenew: task.AutoRenew == 1,
DnsProvider: pbProvider, DnsProvider: pbProvider,
AcmeUser: pbACMEUser, AcmeUser: pbACMEUser,
AuthType: task.AuthType,
}}, nil }}, nil
} }

View File

@@ -57,6 +57,12 @@ func (this *BaseService) ValidateAdminAndUser(ctx context.Context, reqUserId int
return return
} }
// 校验节点
func (this *BaseService) ValidateNode(ctx context.Context) (nodeId int64, err error) {
_, nodeId, err = rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode)
return
}
// 返回成功 // 返回成功
func (this *BaseService) Success() (*pb.RPCSuccess, error) { func (this *BaseService) Success() (*pb.RPCSuccess, error) {
return &pb.RPCSuccess{}, nil return &pb.RPCSuccess{}, nil

View File

@@ -1067,6 +1067,11 @@ func (this *NodeService) FindEnabledNodeDNS(ctx context.Context, req *pb.FindEna
return &pb.FindEnabledNodeDNSResponse{Node: nil}, nil return &pb.FindEnabledNodeDNSResponse{Node: nil}, nil
} }
ipAddr, err := models.SharedNodeIPAddressDAO.FindFirstNodeIPAddress(int64(node.Id))
if err != nil {
return nil, err
}
clusterId := int64(node.ClusterId) clusterId := int64(node.ClusterId)
clusterDNS, err := models.SharedNodeClusterDAO.FindClusterDNSInfo(clusterId) clusterDNS, err := models.SharedNodeClusterDAO.FindClusterDNSInfo(clusterId)
if err != nil { if err != nil {
@@ -1101,11 +1106,6 @@ func (this *NodeService) FindEnabledNodeDNS(ctx context.Context, req *pb.FindEna
} }
} }
ipAddr, err := models.SharedNodeIPAddressDAO.FindFirstNodeIPAddress(int64(node.Id))
if err != nil {
return nil, err
}
return &pb.FindEnabledNodeDNSResponse{ return &pb.FindEnabledNodeDNSResponse{
Node: &pb.NodeDNSInfo{ Node: &pb.NodeDNSInfo{
Id: int64(node.Id), Id: int64(node.Id),

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
package setup package setup
import ( import (
"github.com/TeaOSLab/EdgeAPI/internal/acme"
"github.com/TeaOSLab/EdgeAPI/internal/errors" "github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/iwind/TeaGo/dbs" "github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types" "github.com/iwind/TeaGo/types"
@@ -16,6 +17,9 @@ var upgradeFuncs = []*upgradeVersion{
{ {
"0.0.3", upgradeV0_0_3, "0.0.3", upgradeV0_0_3,
}, },
{
"0.0.5", upgradeV0_0_5,
},
} }
// 升级SQL数据 // 升级SQL数据
@@ -89,3 +93,14 @@ func upgradeV0_0_3(db *dbs.DB) error {
return nil return nil
} }
// v0.0.4
func upgradeV0_0_5(db *dbs.DB) error {
// 升级edgeACMETasks
_, err := db.Exec("UPDATE edgeACMETasks SET authType=? WHERE authType IS NULL OR LENGTH(authType)=0", acme.AuthTypeDNS)
if err != nil {
return err
}
return nil
}