github.phpd.cn/goreleaser/goreleaser@v0.92.0/internal/http/http.go (about) 1 // Package http implements functionality common to HTTP uploading pipelines. 2 package http 3 4 import ( 5 "bytes" 6 "crypto/tls" 7 "crypto/x509" 8 "fmt" 9 "html/template" 10 "io" 11 h "net/http" 12 "os" 13 "runtime" 14 "strings" 15 16 "github.com/apex/log" 17 "github.com/pkg/errors" 18 19 "github.com/goreleaser/goreleaser/internal/artifact" 20 "github.com/goreleaser/goreleaser/internal/pipe" 21 "github.com/goreleaser/goreleaser/internal/semerrgroup" 22 "github.com/goreleaser/goreleaser/pkg/config" 23 "github.com/goreleaser/goreleaser/pkg/context" 24 ) 25 26 const ( 27 // ModeBinary uploads only compiled binaries 28 ModeBinary = "binary" 29 // ModeArchive uploads release archives 30 ModeArchive = "archive" 31 ) 32 33 type asset struct { 34 ReadCloser io.ReadCloser 35 Size int64 36 } 37 38 type assetOpenFunc func(string, *artifact.Artifact) (*asset, error) 39 40 var assetOpen assetOpenFunc 41 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, errors.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 Put structs 69 func Defaults(puts []config.Put) error { 70 for i := range puts { 71 defaults(&puts[i]) 72 } 73 return nil 74 } 75 76 func defaults(put *config.Put) { 77 if put.Mode == "" { 78 put.Mode = ModeArchive 79 } 80 } 81 82 // CheckConfig validates a Put configuration returning a descriptive error when appropriate 83 func CheckConfig(ctx *context.Context, put *config.Put, kind string) error { 84 85 if put.Target == "" { 86 return misconfigured(kind, put, "missing target") 87 } 88 89 if put.Name == "" { 90 return misconfigured(kind, put, "missing name") 91 } 92 93 if put.Mode != ModeArchive && put.Mode != ModeBinary { 94 return misconfigured(kind, put, "mode must be 'binary' or 'archive'") 95 } 96 97 envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name)) 98 if _, ok := ctx.Env[envName]; !ok { 99 return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName)) 100 } 101 102 if put.TrustedCerts != "" && !x509.NewCertPool().AppendCertsFromPEM([]byte(put.TrustedCerts)) { 103 return misconfigured(kind, put, "no certificate could be added from the specified trusted_certificates configuration") 104 } 105 106 return nil 107 108 } 109 110 func misconfigured(kind string, upload *config.Put, reason string) error { 111 return pipe.Skip(fmt.Sprintf("%s section '%s' is not configured properly (%s)", kind, upload.Name, reason)) 112 } 113 114 // ResponseChecker is a function capable of validating an http server response. 115 // It must return and error when the response must be considered a failure. 116 type ResponseChecker func(*h.Response) error 117 118 // Upload does the actual uploading work 119 func Upload(ctx *context.Context, puts []config.Put, kind string, check ResponseChecker) error { 120 if ctx.SkipPublish { 121 return pipe.ErrSkipPublishEnabled 122 } 123 124 // Handle every configured put 125 for _, put := range puts { 126 filters := []artifact.Filter{} 127 if put.Checksum { 128 filters = append(filters, artifact.ByType(artifact.Checksum)) 129 } 130 if put.Signature { 131 filters = append(filters, artifact.ByType(artifact.Signature)) 132 } 133 // We support two different modes 134 // - "archive": Upload all artifacts 135 // - "binary": Upload only the raw binaries 136 switch v := strings.ToLower(put.Mode); v { 137 case ModeArchive: 138 filters = append(filters, 139 artifact.ByType(artifact.UploadableArchive), 140 artifact.ByType(artifact.LinuxPackage), 141 ) 142 case ModeBinary: 143 filters = append(filters, artifact.ByType(artifact.UploadableBinary)) 144 default: 145 err := fmt.Errorf("%s: mode \"%s\" not supported", kind, v) 146 log.WithFields(log.Fields{ 147 kind: put.Name, 148 "mode": v, 149 }).Error(err.Error()) 150 return err 151 } 152 if err := uploadWithFilter(ctx, &put, artifact.Or(filters...), kind, check); err != nil { 153 return err 154 } 155 } 156 157 return nil 158 } 159 160 func uploadWithFilter(ctx *context.Context, put *config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { 161 var artifacts = ctx.Artifacts.Filter(filter).List() 162 log.Debugf("will upload %d artifacts", len(artifacts)) 163 var g = semerrgroup.New(ctx.Parallelism) 164 for _, artifact := range artifacts { 165 artifact := artifact 166 g.Go(func() error { 167 return uploadAsset(ctx, put, artifact, kind, check) 168 }) 169 } 170 return g.Wait() 171 } 172 173 // uploadAsset uploads file to target and logs all actions 174 func uploadAsset(ctx *context.Context, put *config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { 175 envBase := fmt.Sprintf("%s_%s_", strings.ToUpper(kind), strings.ToUpper(put.Name)) 176 username := put.Username 177 if username == "" { 178 // username not configured: using env 179 username = ctx.Env[envBase+"USERNAME"] 180 } 181 secret := ctx.Env[envBase+"SECRET"] 182 183 // Generate the target url 184 targetURL, err := resolveTargetTemplate(ctx, put, artifact) 185 if err != nil { 186 msg := fmt.Sprintf("%s: error while building the target url", kind) 187 log.WithField("instance", put.Name).WithError(err).Error(msg) 188 return errors.Wrap(err, msg) 189 } 190 191 // Handle the artifact 192 asset, err := assetOpen(kind, &artifact) 193 if err != nil { 194 return err 195 } 196 defer asset.ReadCloser.Close() // nolint: errcheck 197 198 // The target url needs to contain the artifact name 199 if !strings.HasSuffix(targetURL, "/") { 200 targetURL += "/" 201 } 202 targetURL += artifact.Name 203 204 var headers = map[string]string{} 205 if put.ChecksumHeader != "" { 206 sum, err := artifact.Checksum() 207 if err != nil { 208 return err 209 } 210 headers[put.ChecksumHeader] = sum 211 } 212 213 _, err = uploadAssetToServer(ctx, put, targetURL, username, secret, headers, asset, check) 214 if err != nil { 215 msg := fmt.Sprintf("%s: upload failed", kind) 216 log.WithError(err).WithFields(log.Fields{ 217 "instance": put.Name, 218 "username": username, 219 }).Error(msg) 220 return errors.Wrap(err, msg) 221 } 222 223 log.WithFields(log.Fields{ 224 "instance": put.Name, 225 "mode": put.Mode, 226 }).Info("uploaded successful") 227 228 return nil 229 } 230 231 // uploadAssetToServer uploads the asset file to target 232 func uploadAssetToServer(ctx *context.Context, put *config.Put, target, username, secret string, headers map[string]string, a *asset, check ResponseChecker) (*h.Response, error) { 233 req, err := newUploadRequest(target, username, secret, headers, a) 234 if err != nil { 235 return nil, err 236 } 237 238 return executeHTTPRequest(ctx, put, req, check) 239 } 240 241 // newUploadRequest creates a new h.Request for uploading 242 func newUploadRequest(target, username, secret string, headers map[string]string, a *asset) (*h.Request, error) { 243 req, err := h.NewRequest("PUT", target, a.ReadCloser) 244 if err != nil { 245 return nil, err 246 } 247 req.ContentLength = a.Size 248 req.SetBasicAuth(username, secret) 249 250 for k, v := range headers { 251 req.Header.Add(k, v) 252 } 253 254 return req, err 255 } 256 257 func getHTTPClient(put *config.Put) (*h.Client, error) { 258 if put.TrustedCerts == "" { 259 return h.DefaultClient, nil 260 } 261 pool, err := x509.SystemCertPool() 262 if err != nil { 263 if runtime.GOOS == "windows" { 264 // on windows ignore errors until golang issues #16736 & #18609 get fixed 265 pool = x509.NewCertPool() 266 } else { 267 return nil, err 268 } 269 } 270 pool.AppendCertsFromPEM([]byte(put.TrustedCerts)) // already validated certs checked by CheckConfig 271 return &h.Client{ 272 Transport: &h.Transport{ 273 TLSClientConfig: &tls.Config{ 274 RootCAs: pool, 275 }, 276 }, 277 }, nil 278 } 279 280 // executeHTTPRequest processes the http call with respect of context ctx 281 func executeHTTPRequest(ctx *context.Context, put *config.Put, req *h.Request, check ResponseChecker) (*h.Response, error) { 282 client, err := getHTTPClient(put) 283 if err != nil { 284 return nil, err 285 } 286 log.Debugf("executing request: %s %s (headers: %v)", req.Method, req.URL, req.Header) 287 resp, err := client.Do(req) 288 if err != nil { 289 // If we got an error, and the context has been canceled, 290 // the context's error is probably more useful. 291 select { 292 case <-ctx.Done(): 293 return nil, ctx.Err() 294 default: 295 } 296 return nil, err 297 } 298 299 defer resp.Body.Close() // nolint: errcheck 300 301 err = check(resp) 302 if err != nil { 303 // even though there was an error, we still return the response 304 // in case the caller wants to inspect it further 305 return resp, err 306 } 307 308 return resp, err 309 } 310 311 // targetData is used as a template struct for 312 // Artifactory.Target 313 type targetData struct { 314 Version string 315 Tag string 316 ProjectName string 317 318 // Only supported in mode binary 319 Os string 320 Arch string 321 Arm string 322 } 323 324 // resolveTargetTemplate returns the resolved target template with replaced variables 325 // Those variables can be replaced by the given context, goos, goarch, goarm and more 326 func resolveTargetTemplate(ctx *context.Context, put *config.Put, artifact artifact.Artifact) (string, error) { 327 data := targetData{ 328 Version: ctx.Version, 329 Tag: ctx.Git.CurrentTag, 330 ProjectName: ctx.Config.ProjectName, 331 } 332 333 if put.Mode == ModeBinary { 334 data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos) 335 data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch) 336 data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm) 337 } 338 339 var out bytes.Buffer 340 t, err := template.New(ctx.Config.ProjectName).Parse(put.Target) 341 if err != nil { 342 return "", err 343 } 344 err = t.Execute(&out, data) 345 return out.String(), err 346 } 347 348 func replace(replacements map[string]string, original string) string { 349 result := replacements[original] 350 if result == "" { 351 return original 352 } 353 return result 354 }