github.com/anchore/syft@v1.38.2/syft/source/snapsource/snapcraft_api.go (about) 1 package snapsource 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 9 "github.com/anchore/syft/internal/log" 10 ) 11 12 const ( 13 defaultChannel = "stable" 14 defaultArchitecture = "amd64" 15 defaultSeries = "16" 16 ) 17 18 // snapcraftClient handles interactions with the Snapcraft API 19 type snapcraftClient struct { 20 InfoAPIURL string 21 FindAPIURL string 22 HTTPClient *http.Client 23 } 24 25 // newSnapcraftClient creates a new Snapcraft API client with default settings 26 func newSnapcraftClient() *snapcraftClient { 27 return &snapcraftClient{ 28 InfoAPIURL: "https://api.snapcraft.io/v2/snaps/info/", 29 FindAPIURL: "https://api.snapcraft.io/v2/snaps/find", 30 HTTPClient: &http.Client{}, 31 } 32 } 33 34 // snapcraftInfo represents the response from the snapcraft info API 35 type snapcraftInfo struct { 36 ChannelMap []snapChannelMapEntry `json:"channel-map"` 37 } 38 39 type snapChannelMapEntry struct { 40 Channel snapChannel `json:"channel"` 41 Download snapDownload `json:"download"` 42 } 43 type snapChannel struct { 44 Architecture string `json:"architecture"` 45 Name string `json:"name"` 46 } 47 48 type snapDownload struct { 49 URL string `json:"url"` 50 } 51 52 // snapFindResponse represents the response from the snapcraft find API (search v2) 53 type snapFindResponse struct { 54 Results []struct { 55 Name string `json:"name"` 56 SnapID string `json:"snap-id"` 57 Snap struct{} `json:"snap"` 58 } `json:"results"` 59 } 60 61 // GetSnapDownloadURL retrieves the download URL for a snap package 62 func (c *snapcraftClient) GetSnapDownloadURL(id snapIdentity) (string, error) { 63 apiURL := c.InfoAPIURL + id.Name 64 65 log.WithFields("name", id.Name, "channel", id.Channel, "architecture", id.Architecture).Trace("requesting snap info") 66 67 req, err := http.NewRequest(http.MethodGet, apiURL, nil) 68 if err != nil { 69 return "", fmt.Errorf("failed to create HTTP request: %w", err) 70 } 71 72 req.Header.Set("Snap-Device-Series", defaultSeries) 73 74 resp, err := c.HTTPClient.Do(req) 75 if err != nil { 76 return "", fmt.Errorf("failed to send HTTP request: %w", err) 77 } 78 defer resp.Body.Close() 79 80 // handle 404 case - check if snap exists via find API 81 if resp.StatusCode == http.StatusNotFound { 82 log.WithFields("name", id.Name).Debug("snap info not found, checking if snap exists via find API") 83 84 exists, snapID, findErr := c.CheckSnapExists(id.Name) 85 if findErr != nil { 86 return "", fmt.Errorf("failed to check if snap exists: %w", findErr) 87 } 88 89 if exists { 90 return "", fmt.Errorf("found snap '%s' (id=%s) but it is unavailable for download", id.Name, snapID) 91 } 92 return "", fmt.Errorf("no snap found with name '%s'", id.Name) 93 } 94 95 if resp.StatusCode != http.StatusOK { 96 return "", fmt.Errorf("API request failed with status code %d", resp.StatusCode) 97 } 98 99 body, err := io.ReadAll(resp.Body) 100 if err != nil { 101 return "", fmt.Errorf("failed to read response body: %w", err) 102 } 103 104 var info snapcraftInfo 105 if err := json.Unmarshal(body, &info); err != nil { 106 return "", fmt.Errorf("failed to parse JSON response: %w", err) 107 } 108 109 for _, cm := range info.ChannelMap { 110 if cm.Channel.Architecture == id.Architecture && cm.Channel.Name == id.Channel { 111 return cm.Download.URL, nil 112 } 113 } 114 115 return "", fmt.Errorf("no matching snap found for %s", id.String()) 116 } 117 118 // CheckSnapExists uses the find API (search v2) to check if a snap exists 119 func (c *snapcraftClient) CheckSnapExists(snapName string) (bool, string, error) { 120 req, err := http.NewRequest(http.MethodGet, c.FindAPIURL, nil) 121 if err != nil { 122 return false, "", fmt.Errorf("failed to create find request: %w", err) 123 } 124 125 q := req.URL.Query() 126 q.Add("name-startswith", snapName) 127 req.URL.RawQuery = q.Encode() 128 129 req.Header.Set("Snap-Device-Series", defaultSeries) 130 131 resp, err := c.HTTPClient.Do(req) 132 if err != nil { 133 return false, "", fmt.Errorf("failed to send find request: %w", err) 134 } 135 defer resp.Body.Close() 136 137 if resp.StatusCode != http.StatusOK { 138 return false, "", fmt.Errorf("find API request failed with status code %d", resp.StatusCode) 139 } 140 141 body, err := io.ReadAll(resp.Body) 142 if err != nil { 143 return false, "", fmt.Errorf("failed to read find response body: %w", err) 144 } 145 146 var findResp snapFindResponse 147 if err := json.Unmarshal(body, &findResp); err != nil { 148 return false, "", fmt.Errorf("failed to parse find JSON response: %w", err) 149 } 150 151 // Look for exact name match 152 for _, result := range findResp.Results { 153 if result.Name == snapName { 154 return true, result.SnapID, nil 155 } 156 } 157 158 return false, "", nil 159 }