github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/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 }