github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/daemon/api_find.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  	"encoding/json"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  
    28  	"github.com/gorilla/mux"
    29  
    30  	"github.com/snapcore/snapd/client"
    31  	"github.com/snapcore/snapd/client/clientutil"
    32  	"github.com/snapcore/snapd/httputil"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/snap"
    36  	"github.com/snapcore/snapd/store"
    37  )
    38  
    39  var (
    40  	findCmd = &Command{
    41  		Path:       "/v2/find",
    42  		GET:        searchStore,
    43  		ReadAccess: openAccess{},
    44  	}
    45  )
    46  
    47  func searchStore(c *Command, r *http.Request, user *auth.UserState) Response {
    48  	route := c.d.router.Get(snapCmd.Path)
    49  	if route == nil {
    50  		return InternalError("cannot find route for snaps")
    51  	}
    52  	query := r.URL.Query()
    53  	q := query.Get("q")
    54  	commonID := query.Get("common-id")
    55  	// TODO: support both "category" (search v2) and "section"
    56  	section := query.Get("section")
    57  	name := query.Get("name")
    58  	scope := query.Get("scope")
    59  	private := false
    60  	prefix := false
    61  
    62  	if sel := query.Get("select"); sel != "" {
    63  		switch sel {
    64  		case "refresh":
    65  			if commonID != "" {
    66  				return BadRequest("cannot use 'common-id' with 'select=refresh'")
    67  			}
    68  			if name != "" {
    69  				return BadRequest("cannot use 'name' with 'select=refresh'")
    70  			}
    71  			if q != "" {
    72  				return BadRequest("cannot use 'q' with 'select=refresh'")
    73  			}
    74  			return storeUpdates(c, r, user)
    75  		case "private":
    76  			private = true
    77  		}
    78  	}
    79  
    80  	if name != "" {
    81  		if q != "" {
    82  			return BadRequest("cannot use 'q' and 'name' together")
    83  		}
    84  		if commonID != "" {
    85  			return BadRequest("cannot use 'common-id' and 'name' together")
    86  		}
    87  
    88  		if name[len(name)-1] != '*' {
    89  			return findOne(c, r, user, name)
    90  		}
    91  
    92  		prefix = true
    93  		q = name[:len(name)-1]
    94  	}
    95  
    96  	if commonID != "" && q != "" {
    97  		return BadRequest("cannot use 'common-id' and 'q' together")
    98  	}
    99  
   100  	theStore := storeFrom(c.d)
   101  	ctx := store.WithClientUserAgent(r.Context(), r)
   102  	found, err := theStore.Find(ctx, &store.Search{
   103  		Query:    q,
   104  		Prefix:   prefix,
   105  		CommonID: commonID,
   106  		Category: section,
   107  		Private:  private,
   108  		Scope:    scope,
   109  	}, user)
   110  	switch err {
   111  	case nil:
   112  		// pass
   113  	case store.ErrBadQuery:
   114  		return BadQuery()
   115  	case store.ErrUnauthenticated, store.ErrInvalidCredentials:
   116  		return Unauthorized(err.Error())
   117  	default:
   118  		// XXX should these return 503 actually?
   119  		if e, ok := err.(*url.Error); ok {
   120  			if neterr, ok := e.Err.(*net.OpError); ok {
   121  				if dnserr, ok := neterr.Err.(*net.DNSError); ok {
   122  					return &apiError{
   123  						Status:  400,
   124  						Message: dnserr.Error(),
   125  						Kind:    client.ErrorKindDNSFailure,
   126  					}
   127  				}
   128  			}
   129  		}
   130  		if e, ok := err.(net.Error); ok && e.Timeout() {
   131  			return &apiError{
   132  				Status:  400,
   133  				Message: err.Error(),
   134  				Kind:    client.ErrorKindNetworkTimeout,
   135  			}
   136  		}
   137  		if e, ok := err.(*httputil.PersistentNetworkError); ok {
   138  			return &apiError{
   139  				Status:  400,
   140  				Message: e.Error(),
   141  				Kind:    client.ErrorKindDNSFailure,
   142  			}
   143  		}
   144  
   145  		return InternalError("%v", err)
   146  	}
   147  
   148  	fresp := &findResponse{
   149  		Sources:           []string{"store"},
   150  		SuggestedCurrency: theStore.SuggestedCurrency(),
   151  	}
   152  
   153  	return sendStorePackages(route, found, fresp)
   154  }
   155  
   156  func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response {
   157  	if err := snap.ValidateName(name); err != nil {
   158  		return BadRequest(err.Error())
   159  	}
   160  
   161  	theStore := storeFrom(c.d)
   162  	spec := store.SnapSpec{
   163  		Name: name,
   164  	}
   165  	ctx := store.WithClientUserAgent(r.Context(), r)
   166  	snapInfo, err := theStore.SnapInfo(ctx, spec, user)
   167  	switch err {
   168  	case nil:
   169  		// pass
   170  	case store.ErrInvalidCredentials:
   171  		return Unauthorized("%v", err)
   172  	case store.ErrSnapNotFound:
   173  		return SnapNotFound(name, err)
   174  	default:
   175  		return InternalError("%v", err)
   176  	}
   177  
   178  	results := make([]*json.RawMessage, 1)
   179  	data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String()))
   180  	if err != nil {
   181  		return InternalError(err.Error())
   182  	}
   183  	results[0] = (*json.RawMessage)(&data)
   184  	return &findResponse{
   185  		Results:           results,
   186  		Sources:           []string{"store"},
   187  		SuggestedCurrency: theStore.SuggestedCurrency(),
   188  	}
   189  }
   190  
   191  func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response {
   192  	route := c.d.router.Get(snapCmd.Path)
   193  	if route == nil {
   194  		return InternalError("cannot find route for snaps")
   195  	}
   196  
   197  	state := c.d.overlord.State()
   198  	state.Lock()
   199  	updates, err := snapstateRefreshCandidates(state, user)
   200  	state.Unlock()
   201  	if err != nil {
   202  		return InternalError("cannot list updates: %v", err)
   203  	}
   204  
   205  	return sendStorePackages(route, updates, nil)
   206  }
   207  
   208  func sendStorePackages(route *mux.Route, found []*snap.Info, resp *findResponse) StructuredResponse {
   209  	results := make([]*json.RawMessage, 0, len(found))
   210  	for _, x := range found {
   211  		url, err := route.URL("name", x.InstanceName())
   212  		if err != nil {
   213  			logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.InstanceName(), x.Revision, err)
   214  			continue
   215  		}
   216  
   217  		data, err := json.Marshal(webify(mapRemote(x), url.String()))
   218  		if err != nil {
   219  			return InternalError("%v", err)
   220  		}
   221  		raw := json.RawMessage(data)
   222  		results = append(results, &raw)
   223  	}
   224  
   225  	if resp == nil {
   226  		resp = &findResponse{}
   227  	}
   228  
   229  	resp.Results = results
   230  
   231  	return resp
   232  }
   233  
   234  func mapRemote(remoteSnap *snap.Info) *client.Snap {
   235  	result, err := clientutil.ClientSnapFromSnapInfo(remoteSnap, nil)
   236  	if err != nil {
   237  		logger.Noticef("cannot get full app info for snap %q: %v", remoteSnap.SnapName(), err)
   238  	}
   239  	result.DownloadSize = remoteSnap.Size
   240  	if remoteSnap.MustBuy {
   241  		result.Status = "priced"
   242  	} else {
   243  		result.Status = "available"
   244  	}
   245  
   246  	return result
   247  }
   248  
   249  type findResponse struct {
   250  	Results           interface{}
   251  	Sources           []string
   252  	SuggestedCurrency string
   253  }
   254  
   255  func (r *findResponse) JSON() *respJSON {
   256  	return &respJSON{
   257  		Status:            200,
   258  		Type:              ResponseTypeSync,
   259  		Result:            r.Results,
   260  		Sources:           r.Sources,
   261  		SuggestedCurrency: r.SuggestedCurrency,
   262  	}
   263  }
   264  
   265  func (r *findResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   266  	r.JSON().ServeHTTP(w, req)
   267  }