github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api_download.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"context"
    24  	"crypto/hmac"
    25  	"crypto/sha256"
    26  	"encoding/base64"
    27  	"encoding/json"
    28  	"errors"
    29  	"fmt"
    30  	"net/http"
    31  	"path/filepath"
    32  	"regexp"
    33  	"strconv"
    34  	"time"
    35  
    36  	"github.com/snapcore/snapd/logger"
    37  	"github.com/snapcore/snapd/overlord/auth"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/randutil"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/store"
    42  )
    43  
    44  var snapDownloadCmd = &Command{
    45  	Path:     "/v2/download",
    46  	PolkitOK: "io.snapcraft.snapd.manage",
    47  	POST:     postSnapDownload,
    48  }
    49  
    50  var validRangeRegexp = regexp.MustCompile(`^\s*bytes=(\d+)-\s*$`)
    51  
    52  // SnapDownloadAction is used to request a snap download
    53  type snapDownloadAction struct {
    54  	SnapName string `json:"snap-name"`
    55  	snapRevisionOptions
    56  
    57  	// HeaderPeek if set requests a peek at the header without the
    58  	// body being returned.
    59  	HeaderPeek bool `json:"header-peek"`
    60  
    61  	ResumeToken    string `json:"resume-token"`
    62  	resumePosition int64
    63  }
    64  
    65  var (
    66  	errDownloadNameRequired     = errors.New("download operation requires one snap name")
    67  	errDownloadHeaderPeekResume = errors.New("cannot request header-only peek when resuming")
    68  	errDownloadResumeNoToken    = errors.New("cannot resume without a token")
    69  )
    70  
    71  func (action *snapDownloadAction) validate() error {
    72  	if action.SnapName == "" {
    73  		return errDownloadNameRequired
    74  	}
    75  	if action.HeaderPeek && (action.resumePosition > 0 || action.ResumeToken != "") {
    76  		return errDownloadHeaderPeekResume
    77  	}
    78  	if action.resumePosition > 0 && action.ResumeToken == "" {
    79  		return errDownloadResumeNoToken
    80  	}
    81  	return action.snapRevisionOptions.validate()
    82  }
    83  
    84  func postSnapDownload(c *Command, r *http.Request, user *auth.UserState) Response {
    85  	var action snapDownloadAction
    86  	decoder := json.NewDecoder(r.Body)
    87  	if err := decoder.Decode(&action); err != nil {
    88  		return BadRequest("cannot decode request body into download operation: %v", err)
    89  	}
    90  	if decoder.More() {
    91  		return BadRequest("extra content found after download operation")
    92  	}
    93  	if rangestr := r.Header.Get("Range"); rangestr != "" {
    94  		// "An origin server MUST ignore a Range header field
    95  		//  that contains a range unit it does not understand."
    96  		subs := validRangeRegexp.FindStringSubmatch(rangestr)
    97  		if len(subs) == 2 {
    98  			n, err := strconv.ParseInt(subs[1], 10, 64)
    99  			if err == nil {
   100  				action.resumePosition = n
   101  			}
   102  		}
   103  	}
   104  	if err := action.validate(); err != nil {
   105  		return BadRequest(err.Error())
   106  	}
   107  
   108  	return streamOneSnap(c, action, user)
   109  }
   110  
   111  func streamOneSnap(c *Command, action snapDownloadAction, user *auth.UserState) Response {
   112  	secret, err := downloadTokensSecret(c)
   113  	if err != nil {
   114  		return InternalError(err.Error())
   115  	}
   116  	theStore := getStore(c)
   117  
   118  	var ss *snapStream
   119  	if action.ResumeToken == "" {
   120  		var info *snap.Info
   121  		actions := []*store.SnapAction{{
   122  			Action:       "download",
   123  			InstanceName: action.SnapName,
   124  			Revision:     action.Revision,
   125  			CohortKey:    action.CohortKey,
   126  			Channel:      action.Channel,
   127  		}}
   128  		results, _, err := theStore.SnapAction(context.TODO(), nil, actions, nil, user, nil)
   129  		if err != nil {
   130  			return errToResponse(err, []string{action.SnapName}, InternalError, "cannot download snap: %v")
   131  		}
   132  		if len(results) != 1 {
   133  			return InternalError("internal error: unexpected number %v of results for a single download", len(results))
   134  		}
   135  		info = results[0].Info
   136  
   137  		ss, err = newSnapStream(action.SnapName, info, secret)
   138  		if err != nil {
   139  			return InternalError(err.Error())
   140  		}
   141  	} else {
   142  		var err error
   143  		ss, err = newResumingSnapStream(action.SnapName, action.ResumeToken, secret)
   144  		if err != nil {
   145  			return BadRequest(err.Error())
   146  		}
   147  		ss.resume = action.resumePosition
   148  	}
   149  
   150  	if !action.HeaderPeek {
   151  		stream, status, err := theStore.DownloadStream(context.TODO(), action.SnapName, ss.Info, action.resumePosition, user)
   152  		if err != nil {
   153  			return InternalError(err.Error())
   154  		}
   155  		ss.stream = stream
   156  		if status != 206 {
   157  			// store/cdn has no partial content (valid
   158  			// reply per RFC)
   159  			logger.Debugf("store refused our range request")
   160  			ss.resume = 0
   161  		}
   162  	}
   163  
   164  	return ss
   165  }
   166  
   167  func newSnapStream(snapName string, info *snap.Info, secret []byte) (*snapStream, error) {
   168  	dlInfo := &info.DownloadInfo
   169  	fname := filepath.Base(info.MountFile())
   170  	tokenJSON := downloadTokenJSON{
   171  		SnapName: snapName,
   172  		Filename: fname,
   173  		Info:     dlInfo,
   174  	}
   175  	tokStr, err := sealDownloadToken(&tokenJSON, secret)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	return &snapStream{
   180  		SnapName: snapName,
   181  		Filename: fname,
   182  		Info:     dlInfo,
   183  		Token:    tokStr,
   184  	}, nil
   185  }
   186  
   187  func newResumingSnapStream(snapName string, tokStr string, secret []byte) (*snapStream, error) {
   188  	d, err := unsealDownloadToken(tokStr, secret)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	if d.SnapName != snapName {
   193  		return nil, fmt.Errorf("resume snap name does not match original snap name")
   194  	}
   195  	return &snapStream{
   196  		SnapName: snapName,
   197  		Filename: d.Filename,
   198  		Info:     d.Info,
   199  		Token:    tokStr,
   200  	}, nil
   201  }
   202  
   203  type downloadTokenJSON struct {
   204  	SnapName string             `json:"snap-name"`
   205  	Filename string             `json:"filename"`
   206  	Info     *snap.DownloadInfo `json:"dl-info"`
   207  }
   208  
   209  func sealDownloadToken(d *downloadTokenJSON, secret []byte) (string, error) {
   210  	b, err := json.Marshal(d)
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  	mac := hmac.New(sha256.New, secret)
   215  	mac.Write(b)
   216  	// append the HMAC hash to b to build the full raw token tok
   217  	tok := mac.Sum(b)
   218  	return base64.RawURLEncoding.EncodeToString(tok), nil
   219  }
   220  
   221  var errInvalidDownloadToken = errors.New("download token is invalid")
   222  
   223  func unsealDownloadToken(tokStr string, secret []byte) (*downloadTokenJSON, error) {
   224  	tok, err := base64.RawURLEncoding.DecodeString(tokStr)
   225  	if err != nil {
   226  		return nil, errInvalidDownloadToken
   227  	}
   228  	sz := len(tok)
   229  	if sz < sha256.Size {
   230  		return nil, errInvalidDownloadToken
   231  	}
   232  	h := tok[sz-sha256.Size:]
   233  	b := tok[:sz-sha256.Size]
   234  	mac := hmac.New(sha256.New, secret)
   235  	mac.Write(b)
   236  	if !hmac.Equal(h, mac.Sum(nil)) {
   237  		return nil, errInvalidDownloadToken
   238  	}
   239  	var d downloadTokenJSON
   240  	if err := json.Unmarshal(b, &d); err != nil {
   241  		return nil, err
   242  	}
   243  	return &d, nil
   244  }
   245  
   246  func downloadTokensSecret(c *Command) (secret []byte, err error) {
   247  	st := c.d.overlord.State()
   248  	st.Lock()
   249  	defer st.Unlock()
   250  	const k = "api-download-tokens-secret"
   251  	err = st.Get(k, &secret)
   252  	if err == nil {
   253  		return secret, nil
   254  	}
   255  	if err != nil && err != state.ErrNoState {
   256  		return nil, err
   257  	}
   258  	secret, err = randutil.CryptoTokenBytes(32)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	st.Set(k, secret)
   263  	st.Set(k+"-time", time.Now().UTC())
   264  	return secret, nil
   265  }