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