github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/response.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package daemon 21 22 import ( 23 "bufio" 24 "encoding/json" 25 "fmt" 26 "io" 27 "mime" 28 "net/http" 29 "path/filepath" 30 "strconv" 31 "time" 32 33 "github.com/snapcore/snapd/asserts" 34 "github.com/snapcore/snapd/client" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/overlord/snapshotstate" 37 "github.com/snapcore/snapd/overlord/state" 38 "github.com/snapcore/snapd/snap" 39 "github.com/snapcore/snapd/systemd" 40 ) 41 42 // ResponseType is the response type 43 type ResponseType string 44 45 // "there are three standard return types: Standard return value, 46 // Background operation, Error", each returning a JSON object with the 47 // following "type" field: 48 const ( 49 ResponseTypeSync ResponseType = "sync" 50 ResponseTypeAsync ResponseType = "async" 51 ResponseTypeError ResponseType = "error" 52 ) 53 54 // Response knows how to serve itself. 55 type Response interface { 56 ServeHTTP(w http.ResponseWriter, r *http.Request) 57 } 58 59 // A StructuredResponse serializes itself to our standard JSON response format. 60 type StructuredResponse interface { 61 Response 62 63 JSON() *respJSON 64 } 65 66 // respJSON represents our standard JSON response format. 67 type respJSON struct { 68 Type ResponseType `json:"type"` 69 // Status is the HTTP status code. 70 Status int `json:"status-code"` 71 // StatusText is filled by the serving pipeline. 72 StatusText string `json:"status"` 73 // Result is a free-form optional result object. 74 Result interface{} `json:"result"` 75 // Change is the change ID for an async response. 76 Change string `json:"change,omitempty"` 77 // Sources is used in find responses. 78 Sources []string `json:"sources,omitempty"` 79 // XXX SuggestedCurrency is part of unsupported paid snap code. 80 SuggestedCurrency string `json:"suggested-currency,omitempty"` 81 // Maintenance... are filled as needed by the serving pipeline. 82 WarningTimestamp *time.Time `json:"warning-timestamp,omitempty"` 83 WarningCount int `json:"warning-count,omitempty"` 84 Maintenance *errorResult `json:"maintenance,omitempty"` 85 } 86 87 func (r *respJSON) JSON() *respJSON { 88 return r 89 } 90 91 func maintenanceForRestartType(rst state.RestartType) *errorResult { 92 e := &errorResult{} 93 switch rst { 94 case state.RestartSystem, state.RestartSystemNow: 95 e.Kind = client.ErrorKindSystemRestart 96 e.Message = systemRestartMsg 97 e.Value = map[string]interface{}{ 98 "op": "reboot", 99 } 100 case state.RestartSystemHaltNow: 101 e.Kind = client.ErrorKindSystemRestart 102 e.Message = systemHaltMsg 103 e.Value = map[string]interface{}{ 104 "op": "halt", 105 } 106 case state.RestartSystemPoweroffNow: 107 e.Kind = client.ErrorKindSystemRestart 108 e.Message = systemPoweroffMsg 109 e.Value = map[string]interface{}{ 110 "op": "poweroff", 111 } 112 case state.RestartDaemon: 113 e.Kind = client.ErrorKindDaemonRestart 114 e.Message = daemonRestartMsg 115 case state.RestartSocket: 116 e.Kind = client.ErrorKindDaemonRestart 117 e.Message = socketRestartMsg 118 case state.RestartUnset: 119 // shouldn't happen, maintenance for unset type should just be nil 120 panic("internal error: cannot marshal maintenance for RestartUnset") 121 } 122 return e 123 } 124 125 func (r *respJSON) addMaintenanceFromRestartType(rst state.RestartType) { 126 if rst == state.RestartUnset { 127 // nothing to do 128 return 129 } 130 r.Maintenance = maintenanceForRestartType(rst) 131 } 132 133 func (r *respJSON) addWarningCount(count int, stamp time.Time) { 134 if count == 0 { 135 return 136 } 137 r.WarningCount = count 138 r.WarningTimestamp = &stamp 139 } 140 141 func (r *respJSON) ServeHTTP(w http.ResponseWriter, _ *http.Request) { 142 status := r.Status 143 r.StatusText = http.StatusText(r.Status) 144 bs, err := json.Marshal(r) 145 if err != nil { 146 logger.Noticef("cannot marshal %#v to JSON: %v", *r, err) 147 bs = nil 148 status = 500 149 } 150 151 hdr := w.Header() 152 if r.Status == 202 || r.Status == 201 { 153 if m, ok := r.Result.(map[string]interface{}); ok { 154 if location, ok := m["resource"]; ok { 155 if location, ok := location.(string); ok && location != "" { 156 hdr.Set("Location", location) 157 } 158 } 159 } 160 } 161 162 hdr.Set("Content-Type", "application/json") 163 w.WriteHeader(status) 164 w.Write(bs) 165 } 166 167 // SyncResponse builds a "sync" response from the given result. 168 func SyncResponse(result interface{}) Response { 169 if rsp, ok := result.(Response); ok { 170 return rsp 171 } 172 173 if err, ok := result.(error); ok { 174 return InternalError("internal error: %v", err) 175 } 176 177 return &respJSON{ 178 Type: ResponseTypeSync, 179 Status: 200, 180 Result: result, 181 } 182 } 183 184 // AsyncResponse builds an "async" response for a created change 185 func AsyncResponse(result map[string]interface{}, change string) Response { 186 return &respJSON{ 187 Type: ResponseTypeAsync, 188 Status: 202, 189 Result: result, 190 Change: change, 191 } 192 } 193 194 // A snapStream ServeHTTP method streams a snap 195 type snapStream struct { 196 SnapName string 197 Filename string 198 Info *snap.DownloadInfo 199 Token string 200 stream io.ReadCloser 201 resume int64 202 } 203 204 // ServeHTTP from the Response interface 205 func (s *snapStream) ServeHTTP(w http.ResponseWriter, _ *http.Request) { 206 hdr := w.Header() 207 hdr.Set("Content-Type", "application/octet-stream") 208 snapname := fmt.Sprintf("attachment; filename=%s", s.Filename) 209 hdr.Set("Content-Disposition", snapname) 210 211 hdr.Set("Snap-Sha3-384", s.Info.Sha3_384) 212 // can't set Content-Length when stream is nil as it breaks http clients 213 // setting it also when there is a stream, for consistency 214 hdr.Set("Snap-Length", strconv.FormatInt(s.Info.Size, 10)) 215 if s.Token != "" { 216 hdr.Set("Snap-Download-Token", s.Token) 217 } 218 219 if s.stream == nil { 220 // nothing to actually stream 221 return 222 } 223 hdr.Set("Content-Length", strconv.FormatInt(s.Info.Size-s.resume, 10)) 224 225 if s.resume > 0 { 226 hdr.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", s.resume, s.Info.Size-1, s.Info.Size)) 227 w.WriteHeader(206) 228 } 229 230 defer s.stream.Close() 231 bytesCopied, err := io.Copy(w, s.stream) 232 if err != nil { 233 logger.Noticef("cannot copy snap %s (%#v) to the stream: %v", s.SnapName, s.Info, err) 234 http.Error(w, err.Error(), 500) 235 } 236 if bytesCopied != s.Info.Size-s.resume { 237 logger.Noticef("cannot copy snap %s (%#v) to the stream: bytes copied=%d, expected=%d", s.SnapName, s.Info, bytesCopied, s.Info.Size) 238 http.Error(w, io.EOF.Error(), 502) 239 } 240 } 241 242 // A snapshotExportResponse 's ServeHTTP method serves a specific snapshot ID 243 type snapshotExportResponse struct { 244 *snapshotstate.SnapshotExport 245 setID uint64 246 st *state.State 247 } 248 249 // ServeHTTP from the Response interface 250 func (s snapshotExportResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { 251 w.Header().Add("Content-Length", strconv.FormatInt(s.Size(), 10)) 252 w.Header().Add("Content-Type", client.SnapshotExportMediaType) 253 if err := s.StreamTo(w); err != nil { 254 logger.Debugf("cannot export snapshot: %v", err) 255 } 256 s.Close() 257 s.st.Lock() 258 defer s.st.Unlock() 259 snapshotstate.UnsetSnapshotOpInProgress(s.st, s.setID) 260 } 261 262 // A fileResponse 's ServeHTTP method serves the file 263 type fileResponse string 264 265 // ServeHTTP from the Response interface 266 func (f fileResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { 267 filename := fmt.Sprintf("attachment; filename=%s", filepath.Base(string(f))) 268 w.Header().Add("Content-Disposition", filename) 269 http.ServeFile(w, r, string(f)) 270 } 271 272 // A journalLineReaderSeqResponse's ServeHTTP method reads lines (presumed to 273 // be, each one on its own, a JSON dump of a systemd.Log, as output by 274 // journalctl -o json) from an io.ReadCloser, loads that into a client.Log, and 275 // outputs the json dump of that, padded with RS and LF to make it a valid 276 // json-seq response. 277 // 278 // The reader is always closed when done (this is important for 279 // osutil.WatingStdoutPipe). 280 // 281 // Tip: “jq” knows how to read this; “jq --seq” both reads and writes this. 282 type journalLineReaderSeqResponse struct { 283 io.ReadCloser 284 follow bool 285 } 286 287 func (rr *journalLineReaderSeqResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { 288 w.Header().Set("Content-Type", "application/json-seq") 289 290 flusher, hasFlusher := w.(http.Flusher) 291 292 var err error 293 dec := json.NewDecoder(rr) 294 writer := bufio.NewWriter(w) 295 enc := json.NewEncoder(writer) 296 for { 297 var log systemd.Log 298 if err = dec.Decode(&log); err != nil { 299 break 300 } 301 302 writer.WriteByte(0x1E) // RS -- see ascii(7), and RFC7464 303 304 // ignore the error... 305 t, _ := log.Time() 306 if err = enc.Encode(client.Log{ 307 Timestamp: t, 308 Message: log.Message(), 309 SID: log.SID(), 310 PID: log.PID(), 311 }); err != nil { 312 break 313 } 314 315 if rr.follow { 316 if e := writer.Flush(); e != nil { 317 break 318 } 319 if hasFlusher { 320 flusher.Flush() 321 } 322 } 323 } 324 if err != nil && err != io.EOF { 325 fmt.Fprintf(writer, `\x1E{"error": %q}\n`, err) 326 logger.Noticef("cannot stream response; problem reading: %v", err) 327 } 328 if err := writer.Flush(); err != nil { 329 logger.Noticef("cannot stream response; problem writing: %v", err) 330 } 331 rr.Close() 332 } 333 334 type assertResponse struct { 335 assertions []asserts.Assertion 336 bundle bool 337 } 338 339 // AssertResponse builds a response whose ServerHTTP method serves one or a bundle of assertions. 340 func AssertResponse(asserts []asserts.Assertion, bundle bool) Response { 341 if len(asserts) > 1 { 342 bundle = true 343 } 344 return &assertResponse{assertions: asserts, bundle: bundle} 345 } 346 347 func (ar assertResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { 348 t := asserts.MediaType 349 if ar.bundle { 350 t = mime.FormatMediaType(t, map[string]string{"bundle": "y"}) 351 } 352 w.Header().Set("Content-Type", t) 353 w.Header().Set("X-Ubuntu-Assertions-Count", strconv.Itoa(len(ar.assertions))) 354 w.WriteHeader(200) 355 enc := asserts.NewEncoder(w) 356 for _, a := range ar.assertions { 357 err := enc.Encode(a) 358 if err != nil { 359 logger.Noticef("cannot write encoded assertion into response: %v", err) 360 break 361 362 } 363 } 364 }