Skip to content

Commit 215083f

Browse files
committed
Certificates Renewal + SSE
- Certificate renewal is just a re-request as it's forced already - Rejig the routes for readability - Added Server Side Events so that the UI would invalidate the cache when changes happen on the backend, such as certs being provided or failing - Added a SSE Token, which has the same shelf life as normal token but can't be used interchangeably. The reason for this is, the SSE endpoint needs a token for auth as a Query param, so it would be stored in log files. If someone where to get a hold of that, it's pretty useless as it can't be used to change anything, only to listen for events until it expires - Added test endpoint for SSE testing only availabe in debug mode
1 parent 3555008 commit 215083f

File tree

29 files changed

+664
-196
lines changed

29 files changed

+664
-196
lines changed

backend/embed/api_docs/api.swagger.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@
163163
"$ref": "file://./paths/tokens/post.json"
164164
}
165165
},
166+
"/tokens/sse": {
167+
"post": {
168+
"$ref": "file://./paths/tokens/sse/post.json"
169+
}
170+
},
166171
"/upstreams": {
167172
"get": {
168173
"$ref": "file://./paths/upstreams/get.json"

backend/embed/api_docs/paths/tokens/get.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
{
22
"operationId": "refreshToken",
33
"summary": "Refresh your access token",
4-
"tags": [
5-
"Tokens"
6-
],
4+
"tags": ["Tokens"],
75
"responses": {
86
"200": {
97
"description": "200 response",
108
"content": {
119
"application/json": {
1210
"schema": {
13-
"required": [
14-
"result"
15-
],
11+
"required": ["result"],
1612
"properties": {
1713
"result": {
18-
"$ref": "#/components/schemas/StreamObject"
14+
"$ref": "#/components/schemas/TokenObject"
1915
}
2016
}
2117
},
@@ -34,4 +30,4 @@
3430
}
3531
}
3632
}
37-
}
33+
}

backend/embed/api_docs/paths/tokens/post.json

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
{
22
"operationId": "requestToken",
33
"summary": "Request a new access token from credentials",
4-
"tags": [
5-
"Tokens"
6-
],
4+
"tags": ["Tokens"],
75
"requestBody": {
86
"description": "Credentials Payload",
97
"required": true,
@@ -19,12 +17,10 @@
1917
"content": {
2018
"application/json": {
2119
"schema": {
22-
"required": [
23-
"result"
24-
],
20+
"required": ["result"],
2521
"properties": {
2622
"result": {
27-
"$ref": "#/components/schemas/StreamObject"
23+
"$ref": "#/components/schemas/TokenObject"
2824
}
2925
}
3026
},
@@ -49,9 +45,7 @@
4945
"schema": {
5046
"type": "object",
5147
"additionalProperties": false,
52-
"required": [
53-
"error"
54-
],
48+
"required": ["error"],
5549
"properties": {
5650
"result": {
5751
"nullable": true
@@ -76,4 +70,4 @@
7670
}
7771
}
7872
}
79-
}
73+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"operationId": "requestSSEToken",
3+
"summary": "Request a new SSE token",
4+
"tags": ["Tokens"],
5+
"responses": {
6+
"200": {
7+
"description": "200 response",
8+
"content": {
9+
"application/json": {
10+
"schema": {
11+
"required": ["result"],
12+
"properties": {
13+
"result": {
14+
"$ref": "#/components/schemas/TokenObject"
15+
}
16+
}
17+
},
18+
"examples": {
19+
"default": {
20+
"value": {
21+
"result": {
22+
"expires": 1566540510,
23+
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
24+
"scope": "user"
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}

backend/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/go-chi/chi v4.1.2+incompatible
1313
github.com/go-chi/cors v1.2.1
1414
github.com/go-chi/jwtauth v4.0.4+incompatible
15+
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58
1516
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760
1617
github.com/jmoiron/sqlx v1.3.5
1718
github.com/mattn/go-sqlite3 v1.14.16

backend/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
2727
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
2828
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
2929
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
30+
github.com/jc21/go-sse v0.0.0-20230307004720-e0a2266806a8 h1:3zrxixsRpzrMXd/c6vUHaoIi9OqirEPxPe4Ydxn3jNU=
31+
github.com/jc21/go-sse v0.0.0-20230307004720-e0a2266806a8/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
32+
github.com/jc21/go-sse v0.0.0-20230307015818-b2783ddda573 h1:aaRu9mFSjxNfbXWVe7MlarmuB0vcdTShXFbxjzHAseA=
33+
github.com/jc21/go-sse v0.0.0-20230307015818-b2783ddda573/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
34+
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58 h1:WSD0YdEuFPZHIe8hkAjxoAEWZnzieAiLg3zw28EVf80=
35+
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
3036
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
3137
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
3238
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=

backend/internal/api/handler/certificates.go

Lines changed: 95 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,11 @@ func GetCertificates() func(http.ResponseWriter, *http.Request) {
3939
// Route: GET /certificates/{certificateID}
4040
func GetCertificate() func(http.ResponseWriter, *http.Request) {
4141
return func(w http.ResponseWriter, r *http.Request) {
42-
var err error
43-
var certificateID int
44-
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
45-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
46-
return
47-
}
48-
49-
item, err := certificate.GetByID(certificateID)
50-
switch err {
51-
case sql.ErrNoRows:
52-
h.NotFound(w, r)
53-
case nil:
42+
logger.Debug("here")
43+
if item := getCertificateFromRequest(w, r); item != nil {
5444
// nolint: errcheck,gosec
5545
item.Expand(getExpandFromContext(r))
5646
h.ResultResponseJSON(w, r, http.StatusOK, item)
57-
default:
58-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
5947
}
6048
}
6149
}
@@ -64,77 +52,40 @@ func GetCertificate() func(http.ResponseWriter, *http.Request) {
6452
// Route: POST /certificates
6553
func CreateCertificate() func(http.ResponseWriter, *http.Request) {
6654
return func(w http.ResponseWriter, r *http.Request) {
67-
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
68-
69-
var newCertificate certificate.Model
70-
err := json.Unmarshal(bodyBytes, &newCertificate)
71-
if err != nil {
72-
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
73-
return
74-
}
75-
76-
// Get userID from token
77-
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
78-
newCertificate.UserID = userID
55+
var item certificate.Model
56+
if fillObjectFromBody(w, r, "", &item) {
57+
// Get userID from token
58+
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
59+
item.UserID = userID
60+
61+
if err := item.Save(); err != nil {
62+
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
63+
return
64+
}
7965

80-
if err = newCertificate.Save(); err != nil {
81-
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
82-
return
66+
configureCertificate(item)
67+
h.ResultResponseJSON(w, r, http.StatusOK, item)
8368
}
84-
85-
configureCertificate(newCertificate)
86-
87-
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
8869
}
8970
}
9071

9172
// UpdateCertificate updates a cert
9273
// Route: PUT /certificates/{certificateID}
9374
func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
9475
return func(w http.ResponseWriter, r *http.Request) {
95-
var err error
96-
var certificateID int
97-
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
98-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
99-
return
100-
}
101-
102-
certificateObject, err := certificate.GetByID(certificateID)
103-
switch err {
104-
case sql.ErrNoRows:
105-
h.NotFound(w, r)
106-
case nil:
76+
if item := getCertificateFromRequest(w, r); item != nil {
10777
// This is a special endpoint, as it needs to verify the schema payload
10878
// based on the certificate type, without being given a type in the payload.
10979
// The middleware would normally handle this.
110-
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
111-
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes)
112-
if jsonErr != nil {
113-
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
114-
return
115-
}
116-
117-
if len(schemaErrors) > 0 {
118-
h.ResultSchemaErrorJSON(w, r, schemaErrors)
119-
return
80+
if fillObjectFromBody(w, r, schema.UpdateCertificate(item.Type), item) {
81+
if err := item.Save(); err != nil {
82+
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
83+
return
84+
}
85+
86+
// configureCertificate(*item, item.Request)
87+
h.ResultResponseJSON(w, r, http.StatusOK, item)
12088
}
121-
122-
err := json.Unmarshal(bodyBytes, &certificateObject)
123-
if err != nil {
124-
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
125-
return
126-
}
127-
128-
if err = certificateObject.Save(); err != nil {
129-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
130-
return
131-
}
132-
133-
configureCertificate(certificateObject)
134-
135-
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
136-
default:
137-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
13889
}
13990
}
14091
}
@@ -143,31 +94,87 @@ func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
14394
// Route: DELETE /certificates/{certificateID}
14495
func DeleteCertificate() func(http.ResponseWriter, *http.Request) {
14596
return func(w http.ResponseWriter, r *http.Request) {
146-
var err error
147-
var certificateID int
148-
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
149-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
150-
return
151-
}
152-
153-
item, err := certificate.GetByID(certificateID)
154-
switch err {
155-
case sql.ErrNoRows:
156-
h.NotFound(w, r)
157-
case nil:
158-
// Ensure that this upstream isn't in use by a host
159-
cnt := host.GetCertificateUseCount(certificateID)
97+
if item := getCertificateFromRequest(w, r); item != nil {
98+
cnt := host.GetCertificateUseCount(item.ID)
16099
if cnt > 0 {
161100
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot delete certificate that is in use by at least 1 host", nil)
162101
return
163102
}
164103
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
165-
default:
166-
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
167104
}
168105
}
169106
}
170107

108+
// RenewCertificate is self explanatory
109+
// Route: PUT /certificates/{certificateID}/renew
110+
func RenewCertificate() func(http.ResponseWriter, *http.Request) {
111+
return func(w http.ResponseWriter, r *http.Request) {
112+
if item := getCertificateFromRequest(w, r); item != nil {
113+
configureCertificate(*item)
114+
h.ResultResponseJSON(w, r, http.StatusOK, true)
115+
}
116+
}
117+
}
118+
119+
// DownloadCertificate is self explanatory
120+
// Route: PUT /certificates/{certificateID}/download
121+
func DownloadCertificate() func(http.ResponseWriter, *http.Request) {
122+
return func(w http.ResponseWriter, r *http.Request) {
123+
if item := getCertificateFromRequest(w, r); item != nil {
124+
// todo
125+
h.ResultResponseJSON(w, r, http.StatusOK, "ok")
126+
}
127+
}
128+
}
129+
130+
// getCertificateFromRequest has some reusable code for all endpoints that
131+
// have a certificate id in the url. it will write errors to the output.
132+
func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certificate.Model {
133+
var err error
134+
var certificateID int
135+
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
136+
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
137+
return nil
138+
}
139+
140+
certificateObject, err := certificate.GetByID(certificateID)
141+
switch err {
142+
case sql.ErrNoRows:
143+
h.NotFound(w, r)
144+
case nil:
145+
return &certificateObject
146+
default:
147+
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
148+
}
149+
return nil
150+
}
151+
152+
// getCertificateFromRequest has some reusable code for all endpoints that
153+
// have a certificate id in the url. it will write errors to the output.
154+
func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o interface{}) bool {
155+
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
156+
157+
if validationSchema != "" {
158+
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), validationSchema, bodyBytes)
159+
if jsonErr != nil {
160+
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
161+
return false
162+
}
163+
if len(schemaErrors) > 0 {
164+
h.ResultSchemaErrorJSON(w, r, schemaErrors)
165+
return false
166+
}
167+
}
168+
169+
err := json.Unmarshal(bodyBytes, o)
170+
if err != nil {
171+
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
172+
return false
173+
}
174+
175+
return true
176+
}
177+
171178
func configureCertificate(c certificate.Model) {
172179
err := jobqueue.AddJob(jobqueue.Job{
173180
Name: "RequestCertificate",

0 commit comments

Comments
 (0)