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  }