Skip to content

Commit 3918ea7

Browse files
committed
🍹 api, tlscerts: support reloading TLS certificates via API
1 parent 5d21637 commit 3918ea7

File tree

6 files changed

+310
-30
lines changed

6 files changed

+310
-30
lines changed

api/api.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"github.com/database64128/shadowsocks-go"
15+
"github.com/database64128/shadowsocks-go/api/certmgr"
1516
"github.com/database64128/shadowsocks-go/api/internal/restapi"
1617
"github.com/database64128/shadowsocks-go/api/ssm"
1718
"github.com/database64128/shadowsocks-go/conn"
@@ -189,12 +190,11 @@ func (c *Config) NewServer(
189190
var tlsConfig tls.Config
190191

191192
if lnc.CertList != "" {
192-
certs, getCert, ok := tlsCertStore.GetCertList(lnc.CertList)
193+
certList, ok := tlsCertStore.GetCertList(lnc.CertList)
193194
if !ok {
194195
return nil, fmt.Errorf("certificate list %q not found", lnc.CertList)
195196
}
196-
tlsConfig.Certificates = certs
197-
tlsConfig.GetCertificate = getCert
197+
tlsConfig.Certificates, tlsConfig.GetCertificate = certList.GetCertificateFunc()
198198
}
199199

200200
if lnc.ClientCAs != "" {
@@ -259,6 +259,14 @@ func (c *Config) NewServer(
259259
mux.Handle(pattern, realIP(logAPIRequests(logger, handler)))
260260
})
261261

262+
// /api/tlscerts/v1
263+
apiTLSCertsV1Path := joinPatternPath(basePath, "/api/tlscerts/v1")
264+
cm := certmgr.NewCertificateManager(tlsCertStore)
265+
cm.RegisterHandlers(func(method, path string, handler restapi.HandlerFunc) {
266+
pattern := method + " " + joinPatternPath(apiTLSCertsV1Path, path)
267+
mux.Handle(pattern, realIP(logAPIRequests(logger, handler)))
268+
})
269+
262270
if c.StaticPath != "" {
263271
mux.Handle("GET /", realIP(logFileServerRequests(logger, http.FileServer(http.Dir(c.StaticPath)))))
264272
}

api/certmgr/certmgr.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Package certmgr provides a REST API for managing TLS certificates.
2+
package certmgr
3+
4+
import (
5+
"net/http"
6+
7+
"github.com/database64128/shadowsocks-go/api/internal/restapi"
8+
"github.com/database64128/shadowsocks-go/tlscerts"
9+
)
10+
11+
// StandardError is the standard error response.
12+
type StandardError struct {
13+
Message string `json:"error"`
14+
}
15+
16+
// CertificateManager handles TLS certificate management API requests.
17+
type CertificateManager struct {
18+
store *tlscerts.Store
19+
}
20+
21+
// NewCertificateManager returns a new certificate manager.
22+
func NewCertificateManager(store *tlscerts.Store) *CertificateManager {
23+
return &CertificateManager{
24+
store: store,
25+
}
26+
}
27+
28+
// RegisterHandlers sets up handlers for the /certlists and /x509certpools endpoints.
29+
func (cm *CertificateManager) RegisterHandlers(register func(method string, path string, handler restapi.HandlerFunc)) {
30+
register(http.MethodGet, "/certlists", cm.newListCertListsHandler())
31+
register(http.MethodGet, "/certlists/{name}", newGetCertListHandler(cm.store))
32+
register(http.MethodPost, "/certlists/{name}/reload", newReloadCertListHandler(cm.store))
33+
34+
register(http.MethodGet, "/x509certpools", cm.newListX509CertPoolsHandler())
35+
}
36+
37+
func (cm *CertificateManager) newListCertListsHandler() restapi.HandlerFunc {
38+
certLists := &cm.store.Config().CertLists
39+
return func(w http.ResponseWriter, _ *http.Request) (int, error) {
40+
return restapi.EncodeResponse(w, http.StatusOK, certLists)
41+
}
42+
}
43+
44+
func (cm *CertificateManager) newListX509CertPoolsHandler() restapi.HandlerFunc {
45+
certPools := &cm.store.Config().X509CertPools
46+
return func(w http.ResponseWriter, _ *http.Request) (int, error) {
47+
return restapi.EncodeResponse(w, http.StatusOK, certPools)
48+
}
49+
}
50+
51+
var (
52+
certListNotFoundJSON = []byte(`{"error":"certificate list not found"}`)
53+
certListNotReloadableJSON = []byte(`{"error":"certificate list is not reloadable"}`)
54+
)
55+
56+
func newGetCertListHandler(store *tlscerts.Store) restapi.HandlerFunc {
57+
return func(w http.ResponseWriter, r *http.Request) (int, error) {
58+
name := r.PathValue("name")
59+
certList, ok := store.GetCertList(name)
60+
if !ok {
61+
return restapi.EncodeResponse(w, http.StatusNotFound, &certListNotFoundJSON)
62+
}
63+
return restapi.EncodeResponse(w, http.StatusOK, certList.Config())
64+
}
65+
}
66+
67+
func newReloadCertListHandler(store *tlscerts.Store) restapi.HandlerFunc {
68+
return func(w http.ResponseWriter, r *http.Request) (int, error) {
69+
name := r.PathValue("name")
70+
certList, ok := store.GetCertList(name)
71+
if !ok {
72+
return restapi.EncodeResponse(w, http.StatusNotFound, &certListNotFoundJSON)
73+
}
74+
if err := certList.Reload(); err != nil {
75+
if err == (tlscerts.ReloadDisabledError{}) {
76+
return restapi.EncodeResponse(w, http.StatusNotFound, &certListNotReloadableJSON)
77+
}
78+
return restapi.EncodeResponse(w, http.StatusInternalServerError, StandardError{Message: err.Error()})
79+
}
80+
return restapi.EncodeResponse(w, http.StatusNoContent, nil)
81+
}
82+
}

docs/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,8 @@
726726
"certPath": "/etc/letsencrypt/live/example.com/fullchain.pem",
727727
"keyPath": "/etc/letsencrypt/live/example.com/privkey.pem"
728728
}
729-
]
729+
],
730+
"reloadable": true
730731
},
731732
{
732733
"name": "my-client-cert",

service/client.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,11 @@ func (cc *ClientConfig) TCPClient() (netio.StreamClient, error) {
295295
}
296296

297297
if cc.HTTP.CertList != "" {
298-
certs, getClientCert, ok := cc.tlsCertStore.GetClientCertList(cc.HTTP.CertList)
298+
certList, ok := cc.tlsCertStore.GetCertList(cc.HTTP.CertList)
299299
if !ok {
300300
return nil, fmt.Errorf("certificate list not found: %q", cc.HTTP.CertList)
301301
}
302-
hpcc.Certificates = certs
303-
hpcc.GetClientCertificate = getClientCert
302+
hpcc.Certificates, hpcc.GetClientCertificate = certList.GetClientCertificateFunc()
304303
}
305304

306305
if cc.HTTP.RootCAs != "" {

service/server.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,12 +449,11 @@ func (sc *ServerConfig) TCPRelay() (*TCPRelay, error) {
449449
}
450450

451451
if sc.HTTP.CertList != "" {
452-
certs, getCert, ok := sc.tlsCertStore.GetCertList(sc.HTTP.CertList)
452+
certList, ok := sc.tlsCertStore.GetCertList(sc.HTTP.CertList)
453453
if !ok {
454454
return nil, fmt.Errorf("certificate list %q not found", sc.HTTP.CertList)
455455
}
456-
hpsc.Certificates = certs
457-
hpsc.GetCertificate = getCert
456+
hpsc.Certificates, hpsc.GetCertificate = certList.GetCertificateFunc()
458457
}
459458

460459
if sc.HTTP.ClientCAs != "" {

0 commit comments

Comments
 (0)