Skip to content

Commit 54b9eaf

Browse files
codegen api structs from oas, cleanup pushcert api endpoint (#225)
1 parent 4af988f commit 54b9eaf

File tree

10 files changed

+223
-146
lines changed

10 files changed

+223
-146
lines changed

docs/openapi.yaml

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,25 @@ paths:
3030
-----END RSA PRIVATE KEY-----
3131
responses:
3232
'200':
33-
description: The topic, expiry, and/or any errors are returned.
33+
description: The topic and expiry of the APNs certificate are returned.
3434
content:
3535
application/json:
3636
schema:
3737
$ref: '#/components/schemas/PushCertResponse'
38+
'400':
39+
description: Error(s) with the uploaded APNs certificate or private key.
40+
content:
41+
application/json:
42+
schema:
43+
$ref: '#/components/schemas/ErrorResponse'
3844
'401':
39-
$ref: '#/components/responses/UnauthorizedError'
45+
$ref: '#/components/responses/UnauthorizedError'
4046
'500':
41-
description: Error reading HTTP body from request
47+
description: Server errors reading HTTP request or storing APNs certificate or private key.
48+
content:
49+
application/json:
50+
schema:
51+
$ref: '#/components/schemas/ErrorResponse'
4252
/v1/push/{id*}:
4353
get:
4454
description: Send APNs push notifications to MDM enrollments
@@ -231,7 +241,6 @@ components:
231241
schemas:
232242
APIResult:
233243
type: object
234-
description: foo
235244
properties:
236245
no_push:
237246
type: boolean
@@ -263,10 +272,11 @@ components:
263272
type: string
264273
PushCertResponse:
265274
type: object
275+
description: APNs push certificate and key upload response.
276+
required:
277+
- topic
278+
- not_after
266279
properties:
267-
error:
268-
description: Error response string.
269-
type: string
270280
topic:
271281
type: string
272282
description: The "topic" (UID attribute) from the uploaded APNs certificate.
@@ -276,3 +286,13 @@ components:
276286
format: date-time
277287
description: Expiration date of the uploaded APNs certificate.
278288
example: '2026-01-07T04:04:46Z'
289+
ErrorResponse:
290+
type: object
291+
description: Error response.
292+
required:
293+
- error
294+
properties:
295+
error:
296+
type: string
297+
description: Error response string.
298+
example: The sun is shining.

http/api/api.go

Lines changed: 40 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,73 @@
11
package api
22

33
import (
4-
"bytes"
5-
"crypto/tls"
6-
"crypto/x509"
74
"encoding/json"
8-
"encoding/pem"
95
"errors"
106
"fmt"
117
"io"
128
"net/http"
139
"strings"
14-
"time"
1510

1611
"github.com/micromdm/nanomdm/api"
17-
"github.com/micromdm/nanomdm/cryptoutil"
18-
mdmhttp "github.com/micromdm/nanomdm/http"
1912
"github.com/micromdm/nanomdm/push"
2013
"github.com/micromdm/nanomdm/storage"
2114

2215
"github.com/micromdm/nanolib/log"
2316
"github.com/micromdm/nanolib/log/ctxlog"
2417
)
2518

26-
// writeAPIResult encodes r to JSON to w, logging errors to logger if necessary.
27-
func writeAPIResult(logger log.Logger, w http.ResponseWriter, r *api.APIResult, header int) {
19+
// writeJSON encodes v to JSON writing to w using the HTTP status of header.
20+
// An error during encoding is logged to logger if it is not nil.
21+
func writeJSON(w http.ResponseWriter, v interface{}, header int, logger log.Logger) {
2822
if header < 1 {
2923
header = http.StatusInternalServerError
3024
}
3125

32-
if r == nil {
33-
nilErr := api.NewError(errors.New("nil API result"))
34-
r = &api.APIResult{
35-
EnqueueError: nilErr,
36-
PushError: nilErr,
37-
}
38-
}
39-
4026
w.Header().Set("Content-type", "application/json")
4127
w.WriteHeader(header)
4228

29+
if v == nil {
30+
return
31+
}
32+
4333
enc := json.NewEncoder(w)
4434
enc.SetIndent("", "\t")
45-
46-
err := enc.Encode(r)
35+
err := enc.Encode(v)
4736
if err != nil && logger != nil {
4837
logger.Info("msg", "encoding json", "err", err)
4938
}
5039
}
5140

41+
// logAndWriteJSONError logs msg and err to logger as well as writes err to w as JSON.
42+
func logAndWriteJSONError(logger log.Logger, w http.ResponseWriter, msg string, err error, header int) {
43+
if logger != nil {
44+
logger.Info("msg", msg, "err", err)
45+
}
46+
47+
errStr := "<nil error>"
48+
if err != nil {
49+
errStr = err.Error()
50+
}
51+
52+
out := &ErrorResponseJson{Error: errStr}
53+
54+
writeJSON(w, out, header, logger)
55+
}
56+
57+
// writeAPIResult encodes r to JSON writing to w using the HTTP status of header.
58+
func writeAPIResult(r *api.APIResult, w http.ResponseWriter, header int, logger log.Logger) {
59+
if r == nil {
60+
nilErr := api.NewError(errors.New("nil API result"))
61+
r = &api.APIResult{
62+
EnqueueError: nilErr,
63+
PushError: nilErr,
64+
}
65+
header = 0 // override http status if a nil API result happens
66+
}
67+
68+
writeJSON(w, r, header, logger)
69+
}
70+
5271
// amendAPIError amends or inserts err into e.
5372
func amendAPIError(err error, e **api.Error) {
5473
if e == nil || err == nil {
@@ -102,7 +121,7 @@ func PushToIDsHandler(pusher push.Pusher, logger log.Logger, idGetter func(*http
102121
logger := ctxlog.Logger(r.Context(), logger)
103122

104123
defer func() {
105-
writeAPIResult(logger, w, pr, header)
124+
writeAPIResult(pr, w, header, logger)
106125
}()
107126

108127
ids, err := idGetter(r)
@@ -169,7 +188,7 @@ func RawCommandEnqueueToIDsHandler(enqueuer storage.CommandEnqueuer, pusher push
169188
logger := ctxlog.Logger(r.Context(), logger)
170189

171190
defer func() {
172-
writeAPIResult(logger, w, er, header)
191+
writeAPIResult(er, w, header, logger)
173192
}()
174193

175194
ids, err := idGetter(r)
@@ -213,115 +232,3 @@ func RawCommandEnqueueToIDsHandler(enqueuer storage.CommandEnqueuer, pusher push
213232
}
214233
}
215234
}
216-
217-
// readPEMCertAndKey reads a PEM-encoded certificate and non-encrypted
218-
// private key from input bytes and returns the separate PEM certificate
219-
// and private key in cert and key respectively.
220-
func readPEMCertAndKey(input []byte) (cert []byte, key []byte, err error) {
221-
// if the PEM blocks are mushed together with no newline then add one
222-
input = bytes.ReplaceAll(input, []byte("----------"), []byte("-----\n-----"))
223-
var block *pem.Block
224-
for {
225-
block, input = pem.Decode(input)
226-
if block == nil {
227-
break
228-
}
229-
if block.Type == "CERTIFICATE" {
230-
cert = pem.EncodeToMemory(block)
231-
} else if block.Type == "PRIVATE KEY" || strings.HasSuffix(block.Type, " PRIVATE KEY") {
232-
if x509.IsEncryptedPEMBlock(block) {
233-
err = errors.New("private key PEM appears to be encrypted")
234-
break
235-
}
236-
key = pem.EncodeToMemory(block)
237-
} else {
238-
err = fmt.Errorf("unrecognized PEM type: %q", block.Type)
239-
break
240-
}
241-
}
242-
return
243-
}
244-
245-
// StorePushCertHandler reads a PEM-encoded certificate and private
246-
// key from the HTTP body and saves it to storage. This effectively
247-
// enables us to do something like:
248-
// "% cat push.pem push.key | curl -T - http://api.example.com/" to
249-
// upload our push certs.
250-
func StorePushCertHandler(storage storage.PushCertStore, logger log.Logger) http.HandlerFunc {
251-
return func(w http.ResponseWriter, r *http.Request) {
252-
logger := ctxlog.Logger(r.Context(), logger)
253-
b, err := mdmhttp.ReadAllAndReplaceBody(r)
254-
if err != nil {
255-
logger.Info("msg", "reading body", "err", err)
256-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
257-
return
258-
}
259-
certPEM, keyPEM, err := readPEMCertAndKey(b)
260-
if err == nil {
261-
// sanity check the provided cert and key to make sure they're usable as a pair.
262-
_, err = tls.X509KeyPair(certPEM, keyPEM)
263-
}
264-
var cert *x509.Certificate
265-
if err == nil {
266-
cert, err = cryptoutil.DecodePEMCertificate(certPEM)
267-
}
268-
var topic string
269-
if err == nil {
270-
topic, err = cryptoutil.TopicFromCert(cert)
271-
}
272-
if err == nil {
273-
err = storage.StorePushCert(r.Context(), certPEM, keyPEM)
274-
}
275-
output := &struct {
276-
Error string `json:"error,omitempty"`
277-
Topic string `json:"topic,omitempty"`
278-
NotAfter time.Time `json:"not_after,omitempty"`
279-
}{
280-
Topic: topic,
281-
}
282-
if cert != nil {
283-
output.NotAfter = cert.NotAfter
284-
}
285-
if err != nil {
286-
logger.Info("msg", "store push cert", "err", err)
287-
output.Error = err.Error()
288-
w.WriteHeader(http.StatusInternalServerError)
289-
} else {
290-
logger.Debug("msg", "stored push cert", "topic", topic)
291-
}
292-
json, err := json.MarshalIndent(output, "", "\t")
293-
if err != nil {
294-
logger.Info("msg", "marshal json", "err", err)
295-
}
296-
w.Header().Set("Content-type", "application/json")
297-
_, err = w.Write(json)
298-
if err != nil {
299-
logger.Info("msg", "writing body", "err", err)
300-
}
301-
}
302-
}
303-
304-
// logAndJSONError is a helper for both logging and outputting errors in JSON.
305-
func logAndJSONError(logger log.Logger, w http.ResponseWriter, msg string, inErr error, header int) {
306-
logger.Info("msg", msg, "err", inErr)
307-
308-
if header < 1 {
309-
header = http.StatusInternalServerError
310-
}
311-
312-
w.Header().Set("Content-type", "application/json")
313-
w.WriteHeader(header)
314-
315-
type jsonError struct {
316-
Error string `json:"error"`
317-
}
318-
319-
out := &jsonError{Error: inErr.Error()}
320-
321-
enc := json.NewEncoder(w)
322-
enc.SetIndent("", "\t")
323-
err := enc.Encode(out)
324-
if err != nil && logger != nil {
325-
logger.Info("msg", "encoding json", "err", err)
326-
}
327-
}

http/api/escrowkeyunlock.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ func NewEscrowKeyUnlockHandler(store storage.PushCertStore, client *http.Client,
3939

4040
if !params.Valid() {
4141
err := errors.New("invalid or missing parameters")
42-
logAndJSONError(logger, w, "validating parameters", err, http.StatusBadRequest)
42+
logAndWriteJSONError(logger, w, "validating parameters", err, http.StatusBadRequest)
4343
return
4444
}
4545

4646
topic := values.Get("topic")
4747
if topic == "" {
4848
err := errors.New("empty topic")
49-
logAndJSONError(logger, w, "validating parameters", err, http.StatusBadRequest)
49+
logAndWriteJSONError(logger, w, "validating parameters", err, http.StatusBadRequest)
5050
return
5151
}
5252

@@ -59,7 +59,7 @@ func NewEscrowKeyUnlockHandler(store storage.PushCertStore, client *http.Client,
5959
params.FormParams(),
6060
)
6161
if err != nil {
62-
logAndJSONError(logger, w, "escrow key unlock", err, 0)
62+
logAndWriteJSONError(logger, w, "escrow key unlock", err, 0)
6363
return
6464
}
6565
defer resp.Body.Close()

http/api/generate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package api
2+
3+
//go:generate oa2js -o ErrorResponse.json ../../docs/openapi.yaml ErrorResponse
4+
//go:generate oa2js -o PushCertResponse.json ../../docs/openapi.yaml PushCertResponse
5+
//go:generate go-jsonschema -p $GOPACKAGE --tags json --only-models --output schema.go ErrorResponse.json PushCertResponse.json
6+
//go:generate rm -f ErrorResponse.json PushCertResponse.json

0 commit comments

Comments
 (0)