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  }