github.com/windmeup/goreleaser@v1.21.95/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/windmeup/goreleaser/internal/artifact" 16 "github.com/windmeup/goreleaser/internal/pipe" 17 "github.com/windmeup/goreleaser/internal/semerrgroup" 18 "github.com/windmeup/goreleaser/internal/tmpl" 19 "github.com/windmeup/goreleaser/pkg/config" 20 "github.com/windmeup/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.Skipf("%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 return fmt.Errorf("%s: %s: mode \"%s\" not supported", upload.Name, kind, v) 181 } 182 183 filter := artifact.Or(filters...) 184 if len(upload.IDs) > 0 { 185 filter = artifact.And(filter, artifact.ByIDs(upload.IDs...)) 186 } 187 if len(upload.Exts) > 0 { 188 filter = artifact.And(filter, artifact.ByExt(upload.Exts...)) 189 } 190 if err := uploadWithFilter(ctx, &upload, filter, kind, check); err != nil { 191 return err 192 } 193 } 194 195 return nil 196 } 197 198 func uploadWithFilter(ctx *context.Context, upload *config.Upload, filter artifact.Filter, kind string, check ResponseChecker) error { 199 artifacts := ctx.Artifacts.Filter(filter).List() 200 log.Debugf("will upload %d artifacts", len(artifacts)) 201 g := semerrgroup.New(ctx.Parallelism) 202 for _, artifact := range artifacts { 203 artifact := artifact 204 g.Go(func() error { 205 return uploadAsset(ctx, upload, artifact, kind, check) 206 }) 207 } 208 return g.Wait() 209 } 210 211 // uploadAsset uploads file to target and logs all actions. 212 func uploadAsset(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, kind string, check ResponseChecker) error { 213 // username and secret are optional since the server may not support/need 214 // basic authentication always 215 username := getUsername(ctx, upload, kind) 216 secret := getPassword(ctx, upload, kind) 217 218 // Generate the target url 219 targetURL, err := tmpl.New(ctx).WithArtifact(artifact).Apply(upload.Target) 220 if err != nil { 221 return fmt.Errorf("%s: %s: error while building target URL: %w", upload.Name, kind, err) 222 } 223 224 // Handle the artifact 225 asset, err := assetOpen(kind, artifact) 226 if err != nil { 227 return err 228 } 229 defer asset.ReadCloser.Close() 230 231 // target url need to contain the artifact name unless the custom 232 // artifact name is used 233 if !upload.CustomArtifactName { 234 if !strings.HasSuffix(targetURL, "/") { 235 targetURL += "/" 236 } 237 targetURL += artifact.Name 238 } 239 log.Debugf("generated target url: %s", targetURL) 240 241 headers := map[string]string{} 242 if upload.CustomHeaders != nil { 243 for name, value := range upload.CustomHeaders { 244 resolvedValue, err := tmpl.New(ctx).WithArtifact(artifact).Apply(value) 245 if err != nil { 246 return fmt.Errorf("%s: %s: failed to resolve custom_headers template: %w", upload.Name, kind, err) 247 } 248 headers[name] = resolvedValue 249 } 250 } 251 if upload.ChecksumHeader != "" { 252 sum, err := artifact.Checksum("sha256") 253 if err != nil { 254 return err 255 } 256 headers[upload.ChecksumHeader] = sum 257 } 258 259 res, err := uploadAssetToServer(ctx, upload, targetURL, username, secret, headers, asset, check) 260 if err != nil { 261 return fmt.Errorf("%s: %s: upload failed: %w", upload.Name, kind, err) 262 } 263 if err := res.Body.Close(); err != nil { 264 log.WithError(err).Warn("failed to close response body") 265 } 266 267 log.WithField("instance", upload.Name). 268 WithField("mode", upload.Mode). 269 Info("uploaded successful") 270 271 return nil 272 } 273 274 // uploadAssetToServer uploads the asset file to target. 275 func uploadAssetToServer(ctx *context.Context, upload *config.Upload, target, username, secret string, headers map[string]string, a *asset, check ResponseChecker) (*h.Response, error) { 276 req, err := newUploadRequest(ctx, upload.Method, target, username, secret, headers, a) 277 if err != nil { 278 return nil, err 279 } 280 281 return executeHTTPRequest(ctx, upload, req, check) 282 } 283 284 // newUploadRequest creates a new h.Request for uploading. 285 func newUploadRequest(ctx *context.Context, method, target, username, secret string, headers map[string]string, a *asset) (*h.Request, error) { 286 req, err := h.NewRequestWithContext(ctx, method, target, a.ReadCloser) 287 if err != nil { 288 return nil, err 289 } 290 req.ContentLength = a.Size 291 292 if username != "" && secret != "" { 293 req.SetBasicAuth(username, secret) 294 } 295 296 for k, v := range headers { 297 req.Header.Add(k, v) 298 } 299 300 return req, err 301 } 302 303 func getHTTPClient(upload *config.Upload) (*h.Client, error) { 304 if upload.TrustedCerts == "" && upload.ClientX509Cert == "" && upload.ClientX509Key == "" { 305 return h.DefaultClient, nil 306 } 307 transport := &h.Transport{ 308 Proxy: h.ProxyFromEnvironment, 309 TLSClientConfig: &tls.Config{}, 310 } 311 if upload.TrustedCerts != "" { 312 pool, err := x509.SystemCertPool() 313 if err != nil { 314 if runtime.GOOS == "windows" { 315 // on windows ignore errors until golang issues #16736 & #18609 get fixed 316 pool = x509.NewCertPool() 317 } else { 318 return nil, err 319 } 320 } 321 pool.AppendCertsFromPEM([]byte(upload.TrustedCerts)) // already validated certs checked by CheckConfig 322 transport.TLSClientConfig.RootCAs = pool 323 } 324 if upload.ClientX509Cert != "" && upload.ClientX509Key != "" { 325 cert, err := tls.LoadX509KeyPair(upload.ClientX509Cert, upload.ClientX509Key) 326 if err != nil { 327 return nil, err 328 } 329 transport.TLSClientConfig.Certificates = []tls.Certificate{cert} 330 } 331 return &h.Client{Transport: transport}, nil 332 } 333 334 // executeHTTPRequest processes the http call with respect of context ctx. 335 func executeHTTPRequest(ctx *context.Context, upload *config.Upload, req *h.Request, check ResponseChecker) (*h.Response, error) { 336 client, err := getHTTPClient(upload) 337 if err != nil { 338 return nil, err 339 } 340 log.Debugf("executing request: %s %s (headers: %v)", req.Method, req.URL, req.Header) 341 resp, err := client.Do(req) 342 if err != nil { 343 // If we got an error, and the context has been canceled, 344 // the context's error is probably more useful. 345 select { 346 case <-ctx.Done(): 347 return nil, ctx.Err() 348 default: 349 } 350 return nil, err 351 } 352 353 defer resp.Body.Close() 354 355 err = check(resp) 356 if err != nil { 357 // even though there was an error, we still return the response 358 // in case the caller wants to inspect it further 359 return resp, err 360 } 361 362 return resp, err 363 }