|
1 | 1 | package api |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
5 | | - "crypto/tls" |
6 | | - "crypto/x509" |
7 | 4 | "encoding/json" |
8 | | - "encoding/pem" |
9 | 5 | "errors" |
10 | 6 | "fmt" |
11 | 7 | "io" |
12 | 8 | "net/http" |
13 | 9 | "strings" |
14 | | - "time" |
15 | 10 |
|
16 | 11 | "github.com/micromdm/nanomdm/api" |
17 | | - "github.com/micromdm/nanomdm/cryptoutil" |
18 | | - mdmhttp "github.com/micromdm/nanomdm/http" |
19 | 12 | "github.com/micromdm/nanomdm/push" |
20 | 13 | "github.com/micromdm/nanomdm/storage" |
21 | 14 |
|
22 | 15 | "github.com/micromdm/nanolib/log" |
23 | 16 | "github.com/micromdm/nanolib/log/ctxlog" |
24 | 17 | ) |
25 | 18 |
|
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) { |
28 | 22 | if header < 1 { |
29 | 23 | header = http.StatusInternalServerError |
30 | 24 | } |
31 | 25 |
|
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 | | - |
40 | 26 | w.Header().Set("Content-type", "application/json") |
41 | 27 | w.WriteHeader(header) |
42 | 28 |
|
| 29 | + if v == nil { |
| 30 | + return |
| 31 | + } |
| 32 | + |
43 | 33 | enc := json.NewEncoder(w) |
44 | 34 | enc.SetIndent("", "\t") |
45 | | - |
46 | | - err := enc.Encode(r) |
| 35 | + err := enc.Encode(v) |
47 | 36 | if err != nil && logger != nil { |
48 | 37 | logger.Info("msg", "encoding json", "err", err) |
49 | 38 | } |
50 | 39 | } |
51 | 40 |
|
| 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 | + |
52 | 71 | // amendAPIError amends or inserts err into e. |
53 | 72 | func amendAPIError(err error, e **api.Error) { |
54 | 73 | if e == nil || err == nil { |
@@ -102,7 +121,7 @@ func PushToIDsHandler(pusher push.Pusher, logger log.Logger, idGetter func(*http |
102 | 121 | logger := ctxlog.Logger(r.Context(), logger) |
103 | 122 |
|
104 | 123 | defer func() { |
105 | | - writeAPIResult(logger, w, pr, header) |
| 124 | + writeAPIResult(pr, w, header, logger) |
106 | 125 | }() |
107 | 126 |
|
108 | 127 | ids, err := idGetter(r) |
@@ -169,7 +188,7 @@ func RawCommandEnqueueToIDsHandler(enqueuer storage.CommandEnqueuer, pusher push |
169 | 188 | logger := ctxlog.Logger(r.Context(), logger) |
170 | 189 |
|
171 | 190 | defer func() { |
172 | | - writeAPIResult(logger, w, er, header) |
| 191 | + writeAPIResult(er, w, header, logger) |
173 | 192 | }() |
174 | 193 |
|
175 | 194 | ids, err := idGetter(r) |
@@ -213,115 +232,3 @@ func RawCommandEnqueueToIDsHandler(enqueuer storage.CommandEnqueuer, pusher push |
213 | 232 | } |
214 | 233 | } |
215 | 234 | } |
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 | | -} |
0 commit comments