github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/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/apex/log" 15 "github.com/goreleaser/goreleaser/internal/artifact" 16 "github.com/goreleaser/goreleaser/internal/pipe" 17 "github.com/goreleaser/goreleaser/internal/semerrgroup" 18 "github.com/goreleaser/goreleaser/internal/tmpl" 19 "github.com/goreleaser/goreleaser/pkg/config" 20 "github.com/goreleaser/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 if _, err := getUsername(ctx, upload, kind); err != nil { 100 return err 101 } 102 103 if _, err := getPassword(ctx, upload, kind); err != nil { 104 return err 105 } 106 107 if upload.TrustedCerts != "" && !x509.NewCertPool().AppendCertsFromPEM([]byte(upload.TrustedCerts)) { 108 return misconfigured(kind, upload, "no certificate could be added from the specified trusted_certificates configuration") 109 } 110 111 return nil 112 } 113 114 func getUsername(ctx *context.Context, upload *config.Upload, kind string) (string, error) { 115 if upload.Username != "" { 116 return upload.Username, nil 117 } 118 var key = fmt.Sprintf("%s_%s_USERNAME", strings.ToUpper(kind), strings.ToUpper(upload.Name)) 119 user, ok := ctx.Env[key] 120 if !ok { 121 return "", misconfigured(kind, upload, fmt.Sprintf("missing username or %s environment variable", key)) 122 } 123 return user, nil 124 } 125 126 func getPassword(ctx *context.Context, upload *config.Upload, kind string) (string, error) { 127 var key = fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(upload.Name)) 128 pwd, ok := ctx.Env[key] 129 if !ok { 130 return "", misconfigured(kind, upload, fmt.Sprintf("missing %s environment variable", key)) 131 } 132 return pwd, nil 133 } 134 135 func misconfigured(kind string, upload *config.Upload, reason string) error { 136 return pipe.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason)) 137 } 138 139 // ResponseChecker is a function capable of validating an http server response. 140 // It must return and error when the response must be considered a failure. 141 type ResponseChecker func(*h.Response) error 142 143 // Upload does the actual uploading work. 144 func Upload(ctx *context.Context, uploads []config.Upload, kind string, check ResponseChecker) error { 145 if ctx.SkipPublish { 146 return pipe.ErrSkipPublishEnabled 147 } 148 149 // Handle every configured upload 150 for _, upload := range uploads { 151 upload := upload 152 filters := []artifact.Filter{} 153 if upload.Checksum { 154 filters = append(filters, artifact.ByType(artifact.Checksum)) 155 } 156 if upload.Signature { 157 filters = append(filters, artifact.ByType(artifact.Signature)) 158 } 159 // We support two different modes 160 // - "archive": Upload all artifacts 161 // - "binary": Upload only the raw binaries 162 switch v := strings.ToLower(upload.Mode); v { 163 case ModeArchive: 164 // TODO: should we add source archives here too? 165 filters = append(filters, 166 artifact.ByType(artifact.UploadableArchive), 167 artifact.ByType(artifact.LinuxPackage), 168 ) 169 case ModeBinary: 170 filters = append(filters, artifact.ByType(artifact.UploadableBinary)) 171 default: 172 err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v) 173 log.WithFields(log.Fields{ 174 kind: upload.Name, 175 "mode": v, 176 }).Error(err.Error()) 177 return err 178 } 179 180 var filter = artifact.Or(filters...) 181 if len(upload.IDs) > 0 { 182 filter = artifact.And(filter, artifact.ByIDs(upload.IDs...)) 183 } 184 if err := uploadWithFilter(ctx, &upload, filter, kind, check); err != nil { 185 return err 186 } 187 } 188 189 return nil 190 } 191 192 func uploadWithFilter(ctx *context.Context, upload *config.Upload, filter artifact.Filter, kind string, check ResponseChecker) error { 193 var artifacts = ctx.Artifacts.Filter(filter).List() 194 log.Debugf("will upload %d artifacts", len(artifacts)) 195 var g = semerrgroup.New(ctx.Parallelism) 196 for _, artifact := range artifacts { 197 artifact := artifact 198 g.Go(func() error { 199 return uploadAsset(ctx, upload, artifact, kind, check) 200 }) 201 } 202 return g.Wait() 203 } 204 205 // uploadAsset uploads file to target and logs all actions. 206 func uploadAsset(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, kind string, check ResponseChecker) error { 207 username, err := getUsername(ctx, upload, kind) 208 if err != nil { 209 return err 210 } 211 212 secret, err := getPassword(ctx, upload, kind) 213 if err != nil { 214 return err 215 } 216 217 // Generate the target url 218 targetURL, err := resolveTargetTemplate(ctx, upload, artifact) 219 if err != nil { 220 msg := fmt.Sprintf("%s: error while building the target url", kind) 221 log.WithField("instance", upload.Name).WithError(err).Error(msg) 222 return fmt.Errorf("%s: %w", msg, err) 223 } 224 225 // Handle the artifact 226 asset, err := assetOpen(kind, artifact) 227 if err != nil { 228 return err 229 } 230 defer asset.ReadCloser.Close() 231 232 // target url need to contain the artifact name unless the custom 233 // artifact name is used 234 if !upload.CustomArtifactName { 235 if !strings.HasSuffix(targetURL, "/") { 236 targetURL += "/" 237 } 238 targetURL += artifact.Name 239 } 240 log.Debugf("generated target url: %s", targetURL) 241 242 var headers = map[string]string{} 243 if upload.CustomHeaders != nil { 244 for name, value := range upload.CustomHeaders { 245 resolvedValue, err := resolveHeaderTemplate(ctx, upload, artifact, value) 246 if err != nil { 247 msg := fmt.Sprintf("%s: failed to resolve custom_headers template", kind) 248 log.WithError(err).WithFields(log.Fields{ 249 "instance": upload.Name, 250 "header_name": name, 251 "header_value": value, 252 }).Error(msg) 253 return fmt.Errorf("%s: %w", msg, err) 254 } 255 headers[name] = resolvedValue 256 } 257 } 258 if upload.ChecksumHeader != "" { 259 sum, err := artifact.Checksum("sha256") 260 if err != nil { 261 return err 262 } 263 headers[upload.ChecksumHeader] = sum 264 } 265 266 res, err := uploadAssetToServer(ctx, upload, targetURL, username, secret, headers, asset, check) 267 if err != nil { 268 msg := fmt.Sprintf("%s: upload failed", kind) 269 log.WithError(err).WithFields(log.Fields{ 270 "instance": upload.Name, 271 "username": username, 272 }).Error(msg) 273 return fmt.Errorf("%s: %w", msg, err) 274 } 275 if err := res.Body.Close(); err != nil { 276 log.WithError(err).Warn("failed to close response body") 277 } 278 279 log.WithFields(log.Fields{ 280 "instance": upload.Name, 281 "mode": upload.Mode, 282 }).Info("uploaded successful") 283 284 return nil 285 } 286 287 // uploadAssetToServer uploads the asset file to target. 288 func uploadAssetToServer(ctx *context.Context, upload *config.Upload, target, username, secret string, headers map[string]string, a *asset, check ResponseChecker) (*h.Response, error) { 289 req, err := newUploadRequest(ctx, upload.Method, target, username, secret, headers, a) 290 if err != nil { 291 return nil, err 292 } 293 294 return executeHTTPRequest(ctx, upload, req, check) 295 } 296 297 // newUploadRequest creates a new h.Request for uploading. 298 func newUploadRequest(ctx *context.Context, method, target, username, secret string, headers map[string]string, a *asset) (*h.Request, error) { 299 req, err := h.NewRequestWithContext(ctx, method, target, a.ReadCloser) 300 if err != nil { 301 return nil, err 302 } 303 req.ContentLength = a.Size 304 req.SetBasicAuth(username, secret) 305 306 for k, v := range headers { 307 req.Header.Add(k, v) 308 } 309 310 return req, err 311 } 312 313 func getHTTPClient(upload *config.Upload) (*h.Client, error) { 314 if upload.TrustedCerts == "" { 315 return h.DefaultClient, nil 316 } 317 pool, err := x509.SystemCertPool() 318 if err != nil { 319 if runtime.GOOS == "windows" { 320 // on windows ignore errors until golang issues #16736 & #18609 get fixed 321 pool = x509.NewCertPool() 322 } else { 323 return nil, err 324 } 325 } 326 pool.AppendCertsFromPEM([]byte(upload.TrustedCerts)) // already validated certs checked by CheckConfig 327 return &h.Client{ 328 Transport: &h.Transport{ 329 Proxy: h.ProxyFromEnvironment, 330 TLSClientConfig: &tls.Config{ // nolint: gosec 331 RootCAs: pool, 332 }, 333 }, 334 }, nil 335 } 336 337 // executeHTTPRequest processes the http call with respect of context ctx. 338 func executeHTTPRequest(ctx *context.Context, upload *config.Upload, req *h.Request, check ResponseChecker) (*h.Response, error) { 339 client, err := getHTTPClient(upload) 340 if err != nil { 341 return nil, err 342 } 343 log.Debugf("executing request: %s %s (headers: %v)", req.Method, req.URL, req.Header) 344 resp, err := client.Do(req) 345 if err != nil { 346 // If we got an error, and the context has been canceled, 347 // the context's error is probably more useful. 348 select { 349 case <-ctx.Done(): 350 return nil, ctx.Err() 351 default: 352 } 353 return nil, err 354 } 355 356 defer resp.Body.Close() 357 358 err = check(resp) 359 if err != nil { 360 // even though there was an error, we still return the response 361 // in case the caller wants to inspect it further 362 return resp, err 363 } 364 365 return resp, err 366 } 367 368 // resolveTargetTemplate returns the resolved target template with replaced variables 369 // Those variables can be replaced by the given context, goos, goarch, goarm and more. 370 func resolveTargetTemplate(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact) (string, error) { 371 var replacements = map[string]string{} 372 if upload.Mode == ModeBinary { 373 // TODO: multiple archives here 374 replacements = ctx.Config.Archives[0].Replacements 375 } 376 return tmpl.New(ctx). 377 WithArtifact(artifact, replacements). 378 Apply(upload.Target) 379 } 380 381 // resolveHeaderTemplate returns the resolved custom header template with replaced variables 382 // Those variables can be replaced by the given context, goos, goarch, goarm and more. 383 func resolveHeaderTemplate(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, headerValue string) (string, error) { 384 var replacements = map[string]string{} 385 if upload.Mode == ModeBinary { 386 // TODO: multiple archives here 387 replacements = ctx.Config.Archives[0].Replacements 388 } 389 return tmpl.New(ctx). 390 WithArtifact(artifact, replacements). 391 Apply(headerValue) 392 }