github.com/triarius/goreleaser@v1.12.5/internal/http/http.go (about) 1 // Package http implements functionality common to HTTP uploading pipelines. 2 package http 3 4 import ( 5 "crypto/tls" 6 "crypto/x509" 7 "fmt" 8 "io" 9 h "net/http" 10 "os" 11 "runtime" 12 "strings" 13 14 "github.com/caarlos0/log" 15 "github.com/triarius/goreleaser/internal/artifact" 16 "github.com/triarius/goreleaser/internal/pipe" 17 "github.com/triarius/goreleaser/internal/semerrgroup" 18 "github.com/triarius/goreleaser/internal/tmpl" 19 "github.com/triarius/goreleaser/pkg/config" 20 "github.com/triarius/goreleaser/pkg/context" 21 ) 22 23 const ( 24 // ModeBinary uploads only compiled binaries. 25 ModeBinary = "binary" 26 // ModeArchive uploads release archives. 27 ModeArchive = "archive" 28 ) 29 30 type asset struct { 31 ReadCloser io.ReadCloser 32 Size int64 33 } 34 35 type assetOpenFunc func(string, *artifact.Artifact) (*asset, error) 36 37 // nolint: gochecknoglobals 38 var assetOpen assetOpenFunc 39 40 // TODO: fix this. 41 // nolint: gochecknoinits 42 func init() { 43 assetOpenReset() 44 } 45 46 func assetOpenReset() { 47 assetOpen = assetOpenDefault 48 } 49 50 func assetOpenDefault(kind string, a *artifact.Artifact) (*asset, error) { 51 f, err := os.Open(a.Path) 52 if err != nil { 53 return nil, err 54 } 55 s, err := f.Stat() 56 if err != nil { 57 return nil, err 58 } 59 if s.IsDir() { 60 return nil, fmt.Errorf("%s: upload failed: the asset to upload can't be a directory", kind) 61 } 62 return &asset{ 63 ReadCloser: f, 64 Size: s.Size(), 65 }, nil 66 } 67 68 // Defaults sets default configuration options on upload structs. 69 func Defaults(uploads []config.Upload) error { 70 for i := range uploads { 71 defaults(&uploads[i]) 72 } 73 return nil 74 } 75 76 func defaults(upload *config.Upload) { 77 if upload.Mode == "" { 78 upload.Mode = ModeArchive 79 } 80 if upload.Method == "" { 81 upload.Method = h.MethodPut 82 } 83 } 84 85 // CheckConfig validates an upload configuration returning a descriptive error when appropriate. 86 func CheckConfig(ctx *context.Context, upload *config.Upload, kind string) error { 87 if upload.Target == "" { 88 return misconfigured(kind, upload, "missing target") 89 } 90 91 if upload.Name == "" { 92 return misconfigured(kind, upload, "missing name") 93 } 94 95 if upload.Mode != ModeArchive && upload.Mode != ModeBinary { 96 return misconfigured(kind, upload, "mode must be 'binary' or 'archive'") 97 } 98 99 username := getUsername(ctx, upload, kind) 100 password := getPassword(ctx, upload, kind) 101 passwordEnv := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(upload.Name)) 102 103 if password != "" && username == "" { 104 return misconfigured(kind, upload, fmt.Sprintf("'username' is required when '%s' environment variable is set", passwordEnv)) 105 } 106 107 if username != "" && password == "" { 108 return misconfigured(kind, upload, fmt.Sprintf("environment variable '%s' is required when 'username' is set", passwordEnv)) 109 } 110 111 if upload.TrustedCerts != "" && !x509.NewCertPool().AppendCertsFromPEM([]byte(upload.TrustedCerts)) { 112 return misconfigured(kind, upload, "no certificate could be added from the specified trusted_certificates configuration") 113 } 114 115 if upload.ClientX509Cert != "" && upload.ClientX509Key == "" { 116 return misconfigured(kind, upload, "'client_x509_key' must be set when 'client_x509_cert' is set") 117 } 118 if upload.ClientX509Key != "" && upload.ClientX509Cert == "" { 119 return misconfigured(kind, upload, "'client_x509_cert' must be set when 'client_x509_key' is set") 120 } 121 if upload.ClientX509Cert != "" && upload.ClientX509Key != "" { 122 if _, err := tls.LoadX509KeyPair(upload.ClientX509Cert, upload.ClientX509Key); err != nil { 123 return misconfigured(kind, upload, 124 "client x509 certificate could not be loaded from the specified 'client_x509_cert' and 'client_x509_key'") 125 } 126 } 127 128 return nil 129 } 130 131 // username is optional 132 func getUsername(ctx *context.Context, upload *config.Upload, kind string) string { 133 if upload.Username != "" { 134 return upload.Username 135 } 136 137 key := fmt.Sprintf("%s_%s_USERNAME", strings.ToUpper(kind), strings.ToUpper(upload.Name)) 138 return ctx.Env[key] 139 } 140 141 // password is optional 142 func getPassword(ctx *context.Context, upload *config.Upload, kind string) string { 143 key := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(upload.Name)) 144 return ctx.Env[key] 145 } 146 147 func misconfigured(kind string, upload *config.Upload, reason string) error { 148 return pipe.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason)) 149 } 150 151 // ResponseChecker is a function capable of validating an http server response. 152 // It must return and error when the response must be considered a failure. 153 type ResponseChecker func(*h.Response) error 154 155 // Upload does the actual uploading work. 156 func Upload(ctx *context.Context, uploads []config.Upload, kind string, check ResponseChecker) error { 157 // Handle every configured upload 158 for _, upload := range uploads { 159 upload := upload 160 filters := []artifact.Filter{} 161 if upload.Checksum { 162 filters = append(filters, artifact.ByType(artifact.Checksum)) 163 } 164 if upload.Signature { 165 filters = append(filters, artifact.ByType(artifact.Signature), artifact.ByType(artifact.Certificate)) 166 } 167 // We support two different modes 168 // - "archive": Upload all artifacts 169 // - "binary": Upload only the raw binaries 170 switch v := strings.ToLower(upload.Mode); v { 171 case ModeArchive: 172 // TODO: should we add source archives here too? 173 filters = append(filters, 174 artifact.ByType(artifact.UploadableArchive), 175 artifact.ByType(artifact.LinuxPackage), 176 ) 177 case ModeBinary: 178 filters = append(filters, artifact.ByType(artifact.UploadableBinary)) 179 default: 180 err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v) 181 log.WithFields(log.Fields{ 182 kind: upload.Name, 183 "mode": v, 184 }).Error(err.Error()) 185 return err 186 } 187 188 filter := artifact.Or(filters...) 189 if len(upload.IDs) > 0 { 190 filter = artifact.And(filter, artifact.ByIDs(upload.IDs...)) 191 } 192 if len(upload.Exts) > 0 { 193 filter = artifact.And(filter, artifact.ByExt(upload.Exts...)) 194 } 195 if err := uploadWithFilter(ctx, &upload, filter, kind, check); err != nil { 196 return err 197 } 198 } 199 200 return nil 201 } 202 203 func uploadWithFilter(ctx *context.Context, upload *config.Upload, filter artifact.Filter, kind string, check ResponseChecker) error { 204 artifacts := ctx.Artifacts.Filter(filter).List() 205 log.Debugf("will upload %d artifacts", len(artifacts)) 206 g := semerrgroup.New(ctx.Parallelism) 207 for _, artifact := range artifacts { 208 artifact := artifact 209 g.Go(func() error { 210 return uploadAsset(ctx, upload, artifact, kind, check) 211 }) 212 } 213 return g.Wait() 214 } 215 216 // uploadAsset uploads file to target and logs all actions. 217 func uploadAsset(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, kind string, check ResponseChecker) error { 218 // username and secret are optional since the server may not support/need 219 // basic authentication always 220 username := getUsername(ctx, upload, kind) 221 secret := getPassword(ctx, upload, kind) 222 223 // Generate the target url 224 targetURL, err := resolveTargetTemplate(ctx, upload, artifact) 225 if err != nil { 226 msg := fmt.Sprintf("%s: error while building the target url", kind) 227 log.WithField("instance", upload.Name).WithError(err).Error(msg) 228 return fmt.Errorf("%s: %w", msg, err) 229 } 230 231 // Handle the artifact 232 asset, err := assetOpen(kind, artifact) 233 if err != nil { 234 return err 235 } 236 defer asset.ReadCloser.Close() 237 238 // target url need to contain the artifact name unless the custom 239 // artifact name is used 240 if !upload.CustomArtifactName { 241 if !strings.HasSuffix(targetURL, "/") { 242 targetURL += "/" 243 } 244 targetURL += artifact.Name 245 } 246 log.Debugf("generated target url: %s", targetURL) 247 248 headers := map[string]string{} 249 if upload.CustomHeaders != nil { 250 for name, value := range upload.CustomHeaders { 251 resolvedValue, err := resolveHeaderTemplate(ctx, upload, artifact, value) 252 if err != nil { 253 msg := fmt.Sprintf("%s: failed to resolve custom_headers template", kind) 254 log.WithError(err).WithFields(log.Fields{ 255 "instance": upload.Name, 256 "header_name": name, 257 "header_value": value, 258 }).Error(msg) 259 return fmt.Errorf("%s: %w", msg, err) 260 } 261 headers[name] = resolvedValue 262 } 263 } 264 if upload.ChecksumHeader != "" { 265 sum, err := artifact.Checksum("sha256") 266 if err != nil { 267 return err 268 } 269 headers[upload.ChecksumHeader] = sum 270 } 271 272 res, err := uploadAssetToServer(ctx, upload, targetURL, username, secret, headers, asset, check) 273 if err != nil { 274 msg := fmt.Sprintf("%s: upload failed", kind) 275 log.WithError(err).WithFields(log.Fields{ 276 "instance": upload.Name, 277 }).Error(msg) 278 return fmt.Errorf("%s: %w", msg, err) 279 } 280 if err := res.Body.Close(); err != nil { 281 log.WithError(err).Warn("failed to close response body") 282 } 283 284 log.WithFields(log.Fields{ 285 "instance": upload.Name, 286 "mode": upload.Mode, 287 }).Info("uploaded successful") 288 289 return nil 290 } 291 292 // uploadAssetToServer uploads the asset file to target. 293 func uploadAssetToServer(ctx *context.Context, upload *config.Upload, target, username, secret string, headers map[string]string, a *asset, check ResponseChecker) (*h.Response, error) { 294 req, err := newUploadRequest(ctx, upload.Method, target, username, secret, headers, a) 295 if err != nil { 296 return nil, err 297 } 298 299 return executeHTTPRequest(ctx, upload, req, check) 300 } 301 302 // newUploadRequest creates a new h.Request for uploading. 303 func newUploadRequest(ctx *context.Context, method, target, username, secret string, headers map[string]string, a *asset) (*h.Request, error) { 304 req, err := h.NewRequestWithContext(ctx, method, target, a.ReadCloser) 305 if err != nil { 306 return nil, err 307 } 308 req.ContentLength = a.Size 309 310 if username != "" && secret != "" { 311 req.SetBasicAuth(username, secret) 312 } 313 314 for k, v := range headers { 315 req.Header.Add(k, v) 316 } 317 318 return req, err 319 } 320 321 func getHTTPClient(upload *config.Upload) (*h.Client, error) { 322 if upload.TrustedCerts == "" && upload.ClientX509Cert == "" && upload.ClientX509Key == "" { 323 return h.DefaultClient, nil 324 } 325 transport := &h.Transport{ 326 Proxy: h.ProxyFromEnvironment, 327 TLSClientConfig: &tls.Config{}, 328 } 329 if upload.TrustedCerts != "" { 330 pool, err := x509.SystemCertPool() 331 if err != nil { 332 if runtime.GOOS == "windows" { 333 // on windows ignore errors until golang issues #16736 & #18609 get fixed 334 pool = x509.NewCertPool() 335 } else { 336 return nil, err 337 } 338 } 339 pool.AppendCertsFromPEM([]byte(upload.TrustedCerts)) // already validated certs checked by CheckConfig 340 transport.TLSClientConfig.RootCAs = pool 341 } 342 if upload.ClientX509Cert != "" && upload.ClientX509Key != "" { 343 cert, err := tls.LoadX509KeyPair(upload.ClientX509Cert, upload.ClientX509Key) 344 if err != nil { 345 return nil, err 346 } 347 transport.TLSClientConfig.Certificates = []tls.Certificate{cert} 348 } 349 return &h.Client{Transport: transport}, nil 350 } 351 352 // executeHTTPRequest processes the http call with respect of context ctx. 353 func executeHTTPRequest(ctx *context.Context, upload *config.Upload, req *h.Request, check ResponseChecker) (*h.Response, error) { 354 client, err := getHTTPClient(upload) 355 if err != nil { 356 return nil, err 357 } 358 log.Debugf("executing request: %s %s (headers: %v)", req.Method, req.URL, req.Header) 359 resp, err := client.Do(req) 360 if err != nil { 361 // If we got an error, and the context has been canceled, 362 // the context's error is probably more useful. 363 select { 364 case <-ctx.Done(): 365 return nil, ctx.Err() 366 default: 367 } 368 return nil, err 369 } 370 371 defer resp.Body.Close() 372 373 err = check(resp) 374 if err != nil { 375 // even though there was an error, we still return the response 376 // in case the caller wants to inspect it further 377 return resp, err 378 } 379 380 return resp, err 381 } 382 383 // resolveTargetTemplate returns the resolved target template with replaced variables 384 // Those variables can be replaced by the given context, goos, goarch, goarm and more. 385 func resolveTargetTemplate(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact) (string, error) { 386 replacements := map[string]string{} 387 if upload.Mode == ModeBinary { 388 // TODO: multiple archives here 389 replacements = ctx.Config.Archives[0].Replacements 390 } 391 return tmpl.New(ctx). 392 WithArtifact(artifact, replacements). 393 Apply(upload.Target) 394 } 395 396 // resolveHeaderTemplate returns the resolved custom header template with replaced variables 397 // Those variables can be replaced by the given context, goos, goarch, goarm and more. 398 func resolveHeaderTemplate(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, headerValue string) (string, error) { 399 replacements := map[string]string{} 400 if upload.Mode == ModeBinary { 401 // TODO: multiple archives here 402 replacements = ctx.Config.Archives[0].Replacements 403 } 404 return tmpl.New(ctx). 405 WithArtifact(artifact, replacements). 406 Apply(headerValue) 407 }