github.com/safing/portbase@v0.19.5/updater/fetch.go (about) 1 package updater 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "hash" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "path" 14 "path/filepath" 15 "time" 16 17 "github.com/safing/jess/filesig" 18 "github.com/safing/jess/lhash" 19 "github.com/safing/portbase/log" 20 "github.com/safing/portbase/utils/renameio" 21 ) 22 23 func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { 24 // backoff when retrying 25 if tries > 0 { 26 select { 27 case <-ctx.Done(): 28 return nil // module is shutting down 29 case <-time.After(time.Duration(tries*tries) * time.Second): 30 } 31 } 32 33 // check destination dir 34 dirPath := filepath.Dir(rv.storagePath()) 35 err := reg.storageDir.EnsureAbsPath(dirPath) 36 if err != nil { 37 return fmt.Errorf("could not create updates folder: %s", dirPath) 38 } 39 40 // If verification is enabled, download signature first. 41 var ( 42 verifiedHash *lhash.LabeledHash 43 sigFileData []byte 44 ) 45 if rv.resource.VerificationOptions != nil { 46 verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( 47 ctx, client, 48 rv.resource.VerificationOptions, 49 rv.versionedSigPath(), rv.SigningMetadata(), 50 tries, 51 ) 52 53 if err != nil { 54 switch rv.resource.VerificationOptions.DownloadPolicy { 55 case SignaturePolicyRequire: 56 return fmt.Errorf("signature verification failed: %w", err) 57 case SignaturePolicyWarn: 58 log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) 59 case SignaturePolicyDisable: 60 log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) 61 } 62 } 63 } 64 65 // open file for writing 66 atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) 67 if err != nil { 68 return fmt.Errorf("could not create temp file for download: %w", err) 69 } 70 defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway 71 72 // start file download 73 resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) 74 if err != nil { 75 return err 76 } 77 defer func() { 78 _ = resp.Body.Close() 79 }() 80 81 // Write to the hasher at the same time, if needed. 82 var hasher hash.Hash 83 var writeDst io.Writer = atomicFile 84 if verifiedHash != nil { 85 hasher = verifiedHash.Algorithm().RawHasher() 86 writeDst = io.MultiWriter(hasher, atomicFile) 87 } 88 89 // Download and write file. 90 n, err := io.Copy(writeDst, resp.Body) 91 if err != nil { 92 return fmt.Errorf("failed to download %q: %w", downloadURL, err) 93 } 94 if resp.ContentLength != n { 95 return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) 96 } 97 98 // Before file is finalized, check if hash, if available. 99 if hasher != nil { 100 downloadDigest := hasher.Sum(nil) 101 if verifiedHash.EqualRaw(downloadDigest) { 102 log.Infof("%s: verified signature of %s", reg.Name, downloadURL) 103 } else { 104 switch rv.resource.VerificationOptions.DownloadPolicy { 105 case SignaturePolicyRequire: 106 return errors.New("file does not match signed checksum") 107 case SignaturePolicyWarn: 108 log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) 109 case SignaturePolicyDisable: 110 log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) 111 } 112 113 // Reset hasher to signal that the sig should not be written. 114 hasher = nil 115 } 116 } 117 118 // Write signature file, if we have one and if verification succeeded. 119 if len(sigFileData) > 0 && hasher != nil { 120 sigFilePath := rv.storagePath() + filesig.Extension 121 err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec 122 if err != nil { 123 switch rv.resource.VerificationOptions.DownloadPolicy { 124 case SignaturePolicyRequire: 125 return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) 126 case SignaturePolicyWarn: 127 log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) 128 case SignaturePolicyDisable: 129 log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) 130 } 131 } 132 } 133 134 // finalize file 135 err = atomicFile.CloseAtomicallyReplace() 136 if err != nil { 137 return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) 138 } 139 // set permissions 140 if !onWindows { 141 // TODO: only set executable files to 0755, set other to 0644 142 err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. 143 if err != nil { 144 log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) 145 } 146 } 147 148 log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) 149 return nil 150 } 151 152 func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { 153 // backoff when retrying 154 if tries > 0 { 155 select { 156 case <-ctx.Done(): 157 return nil // module is shutting down 158 case <-time.After(time.Duration(tries*tries) * time.Second): 159 } 160 } 161 162 // Check destination dir. 163 dirPath := filepath.Dir(rv.storagePath()) 164 err := reg.storageDir.EnsureAbsPath(dirPath) 165 if err != nil { 166 return fmt.Errorf("could not create updates folder: %s", dirPath) 167 } 168 169 // Download and verify the missing signature. 170 verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( 171 ctx, client, 172 rv.resource.VerificationOptions, 173 rv.versionedSigPath(), rv.SigningMetadata(), 174 tries, 175 ) 176 if err != nil { 177 switch rv.resource.VerificationOptions.DownloadPolicy { 178 case SignaturePolicyRequire: 179 return fmt.Errorf("signature verification failed: %w", err) 180 case SignaturePolicyWarn: 181 log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) 182 case SignaturePolicyDisable: 183 log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) 184 } 185 return nil 186 } 187 188 // Check if the signature matches the resource file. 189 ok, err := verifiedHash.MatchesFile(rv.storagePath()) 190 if err != nil { 191 switch rv.resource.VerificationOptions.DownloadPolicy { 192 case SignaturePolicyRequire: 193 return fmt.Errorf("error while verifying resource file: %w", err) 194 case SignaturePolicyWarn: 195 log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) 196 case SignaturePolicyDisable: 197 log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) 198 } 199 return nil 200 } 201 if !ok { 202 switch rv.resource.VerificationOptions.DownloadPolicy { 203 case SignaturePolicyRequire: 204 return errors.New("resource file does not match signed checksum") 205 case SignaturePolicyWarn: 206 log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) 207 case SignaturePolicyDisable: 208 log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) 209 } 210 return nil 211 } 212 213 // Write signature file. 214 err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec 215 if err != nil { 216 switch rv.resource.VerificationOptions.DownloadPolicy { 217 case SignaturePolicyRequire: 218 return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) 219 case SignaturePolicyWarn: 220 log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) 221 case SignaturePolicyDisable: 222 log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) 223 } 224 } 225 226 log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) 227 return nil 228 } 229 230 func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { 231 // Download signature file. 232 resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) 233 if err != nil { 234 return nil, nil, err 235 } 236 defer func() { 237 _ = resp.Body.Close() 238 }() 239 sigFileData, err := io.ReadAll(resp.Body) 240 if err != nil { 241 return nil, nil, err 242 } 243 244 // Extract all signatures. 245 sigs, err := filesig.ParseSigFile(sigFileData) 246 switch { 247 case len(sigs) == 0 && err != nil: 248 return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) 249 case len(sigs) == 0: 250 return nil, nil, errors.New("no signatures found in signature file") 251 case err != nil: 252 return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) 253 } 254 255 // Verify all signatures. 256 var verifiedHash *lhash.LabeledHash 257 for _, sig := range sigs { 258 fd, err := filesig.VerifyFileData( 259 sig, 260 requiredMetadata, 261 verifOpts.TrustStore, 262 ) 263 if err != nil { 264 return nil, sigFileData, err 265 } 266 267 // Save or check verified hash. 268 if verifiedHash == nil { 269 verifiedHash = fd.FileHash() 270 } else if !fd.FileHash().Equal(verifiedHash) { 271 // Return an error if two valid hashes mismatch. 272 // For simplicity, all hash algorithms must be the same for now. 273 return nil, sigFileData, errors.New("file hashes from different signatures do not match") 274 } 275 } 276 277 return verifiedHash, sigFileData, nil 278 } 279 280 func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { 281 // backoff when retrying 282 if tries > 0 { 283 select { 284 case <-ctx.Done(): 285 return nil, "", nil // module is shutting down 286 case <-time.After(time.Duration(tries*tries) * time.Second): 287 } 288 } 289 290 // start file download 291 resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) 292 if err != nil { 293 return nil, downloadURL, err 294 } 295 defer func() { 296 _ = resp.Body.Close() 297 }() 298 299 // download and write file 300 buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) 301 n, err := io.Copy(buf, resp.Body) 302 if err != nil { 303 return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) 304 } 305 if resp.ContentLength != n { 306 return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) 307 } 308 309 return buf.Bytes(), downloadURL, nil 310 } 311 312 func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { 313 // parse update URL 314 updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] 315 u, err := url.Parse(updateBaseURL) 316 if err != nil { 317 return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) 318 } 319 // add download path 320 u.Path = path.Join(u.Path, downloadPath) 321 // compile URL 322 downloadURL = u.String() 323 324 // create request 325 req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) 326 if err != nil { 327 return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) 328 } 329 330 // set user agent 331 if reg.UserAgent != "" { 332 req.Header.Set("User-Agent", reg.UserAgent) 333 } 334 335 // start request 336 resp, err = client.Do(req) 337 if err != nil { 338 return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) 339 } 340 341 // check return code 342 if resp.StatusCode != http.StatusOK { 343 _ = resp.Body.Close() 344 return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) 345 } 346 347 return resp, downloadURL, err 348 }