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