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  }