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