diff --git a/internal/http/http.go b/internal/http/http.go index 83fd4da31..8b9ee018e 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -9,7 +9,6 @@ import ( "html/template" "io" h "net/http" - "net/url" "os" "strings" @@ -99,7 +98,7 @@ func CheckConfig(ctx *context.Context, put *config.Put, kind string) error { return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName)) } - if ctx.Config.TrustedCerts != "" && !x509.NewCertPool().AppendCertsFromPEM([]byte(ctx.Config.TrustedCerts)) { + if put.TrustedCerts != "" && !x509.NewCertPool().AppendCertsFromPEM([]byte(put.TrustedCerts)) { return misconfigured(kind, put, "no certificate could be added from the specified trusted_certificates configuration") } @@ -149,7 +148,7 @@ func Upload(ctx *context.Context, puts []config.Put, kind string, check Response }).Error(err.Error()) return err } - if err := uploadWithFilter(ctx, put, artifact.Or(filters...), kind, check); err != nil { + if err := uploadWithFilter(ctx, &put, artifact.Or(filters...), kind, check); err != nil { return err } } @@ -157,7 +156,7 @@ func Upload(ctx *context.Context, puts []config.Put, kind string, check Response return nil } -func uploadWithFilter(ctx *context.Context, put config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { +func uploadWithFilter(ctx *context.Context, put *config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { var g = semerrgroup.New(ctx.Parallelism) for _, artifact := range ctx.Artifacts.Filter(filter).List() { artifact := artifact @@ -169,7 +168,7 @@ func uploadWithFilter(ctx *context.Context, put config.Put, filter artifact.Filt } // uploadAsset uploads file to target and logs all actions -func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { +func uploadAsset(ctx *context.Context, put *config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { envBase := fmt.Sprintf("%s_%s_", strings.ToUpper(kind), strings.ToUpper(put.Name)) username := put.Username if username == "" { @@ -199,7 +198,7 @@ func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifac } targetURL += artifact.Name - _, err = uploadAssetToServer(ctx, targetURL, username, secret, asset, check) + _, err = uploadAssetToServer(ctx, put, targetURL, username, secret, asset, check) if err != nil { msg := fmt.Sprintf("%s: upload failed", kind) log.WithError(err).WithFields(log.Fields{ @@ -218,22 +217,18 @@ func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifac } // uploadAssetToServer uploads the asset file to target -func uploadAssetToServer(ctx *context.Context, target, username, secret string, a *asset, check ResponseChecker) (*h.Response, error) { +func uploadAssetToServer(ctx *context.Context, put *config.Put, target, username, secret string, a *asset, check ResponseChecker) (*h.Response, error) { req, err := newUploadRequest(target, username, secret, a) if err != nil { return nil, err } - return executeHTTPRequest(ctx, req, check) + return executeHTTPRequest(ctx, put, req, check) } // newUploadRequest creates a new h.Request for uploading func newUploadRequest(target, username, secret string, a *asset) (*h.Request, error) { - u, err := url.Parse(target) - if err != nil { - return nil, err - } - req, err := h.NewRequest("PUT", u.String(), a.ReadCloser) + req, err := h.NewRequest("PUT", target, a.ReadCloser) if err != nil { return nil, err } @@ -244,22 +239,29 @@ func newUploadRequest(target, username, secret string, a *asset) (*h.Request, er return req, err } -// executeHTTPRequest processes the http call with respect of context ctx -func executeHTTPRequest(ctx *context.Context, req *h.Request, check ResponseChecker) (*h.Response, error) { - client := h.DefaultClient - if ctx.Config.TrustedCerts != "" { - pool, err := loadSystemRoots() - if err != nil { - return "", nil, err - } - pool.AppendCertsFromPEM([]byte(ctx.Config.TrustedCerts)) // already validated certs checked by CheckConfig - client = &h.Client{ - Transport: &h.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: pool, - }, +func getHTTPClient(put *config.Put) (*h.Client, error) { + if put.TrustedCerts == "" { + return h.DefaultClient, nil + } + pool, err := loadSystemRoots() + if err != nil { + return nil, err + } + pool.AppendCertsFromPEM([]byte(put.TrustedCerts)) // already validated certs checked by CheckConfig + return &h.Client{ + Transport: &h.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, }, - } + }, + }, nil +} + +// executeHTTPRequest processes the http call with respect of context ctx +func executeHTTPRequest(ctx *context.Context, put *config.Put, req *h.Request, check ResponseChecker) (*h.Response, error) { + client, err := getHTTPClient(put) + if err != nil { + return nil, err } resp, err := client.Do(req) if err != nil { @@ -300,21 +302,21 @@ type targetData struct { // resolveTargetTemplate returns the resolved target template with replaced variables // Those variables can be replaced by the given context, goos, goarch, goarm and more -func resolveTargetTemplate(ctx *context.Context, artifactory config.Put, artifact artifact.Artifact) (string, error) { +func resolveTargetTemplate(ctx *context.Context, put *config.Put, artifact artifact.Artifact) (string, error) { data := targetData{ Version: ctx.Version, Tag: ctx.Git.CurrentTag, ProjectName: ctx.Config.ProjectName, } - if artifactory.Mode == ModeBinary { + if put.Mode == ModeBinary { data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos) data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch) data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm) } var out bytes.Buffer - t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target) + t, err := template.New(ctx.Config.ProjectName).Parse(put.Target) if err != nil { return "", err } diff --git a/internal/http/http_test.go b/internal/http/http_test.go index bc26d14fe..ccad186d4 100644 --- a/internal/http/http_test.go +++ b/internal/http/http_test.go @@ -2,12 +2,14 @@ package http import ( "bytes" + "encoding/pem" "fmt" "io" "io/ioutil" h "net/http" "net/http/httptest" "os" + "strings" "sync" "testing" @@ -18,20 +20,6 @@ import ( "github.com/goreleaser/goreleaser/pkg/context" ) -var ( - mux *h.ServeMux - srv *httptest.Server -) - -func setup() { - mux = h.NewServeMux() - srv = httptest.NewServer(mux) -} - -func teardown() { - srv.Close() -} - func TestAssetOpenDefault(t *testing.T) { tf, err := ioutil.TempFile("", "") if err != nil { @@ -112,6 +100,7 @@ func TestCheckConfig(t *testing.T) { {"name missing", args{ctx, &config.Put{Target: "http://blabla", Username: "pepe", Mode: ModeArchive}, "test"}, true}, {"mode missing", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe"}, "test"}, true}, {"mode invalid", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe", Mode: "blabla"}, "test"}, true}, + {"cert invalid", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe", Mode: ModeBinary, TrustedCerts: "bad cert!"}, "test"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -196,11 +185,10 @@ func doCheck(c check, r *h.Request) error { } func TestUpload(t *testing.T) { - setup() - defer teardown() content := []byte("blah!") requests := []*h.Request{} var m sync.Mutex + mux := h.NewServeMux() mux.Handle("/", h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) { bs, err := ioutil.ReadAll(r.Body) if err != nil { @@ -247,37 +235,93 @@ func TestUpload(t *testing.T) { } { ctx.Artifacts.Add(artifact.Artifact{Name: "a." + a.ext, Path: "/a/a." + a.ext, Type: a.typ}) } + tests := []struct { - name string - ctx *context.Context - wantErr bool - put config.Put - check func(r []*h.Request) error + name string + tryPlain bool + tryTLS bool + wantErrPlain bool + wantErrTLS bool + setup func(*httptest.Server) (*context.Context, config.Put) + check func(r []*h.Request) error }{ - {"wrong-mode", ctx, true, - config.Put{Mode: "wrong-mode", Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u1"}, + {"wrong-mode", true, true, true, true, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: "wrong-mode", + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + Username: "u1", + TrustedCerts: cert(s), + } + }, checks(), }, - {"username-from-env", ctx, false, - config.Put{Mode: ModeArchive, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/"}, + {"username-from-env", true, true, false, false, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeArchive, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + TrustedCerts: cert(s), + } + }, checks( check{"/blah/2.1.0/a.deb", "u2", "x", content}, check{"/blah/2.1.0/a.tar", "u2", "x", content}, ), }, - {"archive", ctx, false, - config.Put{Mode: ModeArchive, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u1"}, + {"archive", true, true, false, false, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeArchive, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + Username: "u1", + TrustedCerts: cert(s), + } + }, checks( check{"/blah/2.1.0/a.deb", "u1", "x", content}, check{"/blah/2.1.0/a.tar", "u1", "x", content}, ), }, - {"binary", ctx, false, - config.Put{Mode: ModeBinary, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u2"}, + {"binary", true, true, false, false, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeBinary, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + Username: "u2", + TrustedCerts: cert(s), + } + }, checks(check{"/blah/2.1.0/a.ubi", "u2", "x", content}), }, - {"archive-with-checksum-and-signature", ctx, false, - config.Put{Mode: ModeArchive, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u3", Checksum: true, Signature: true}, + {"binary-add-ending-bar", true, true, false, false, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeBinary, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}", + Username: "u2", + TrustedCerts: cert(s), + } + }, + checks(check{"/blah/2.1.0/a.ubi", "u2", "x", content}), + }, + {"archive-with-checksum-and-signature", true, true, false, false, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeArchive, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + Username: "u3", + Checksum: true, + Signature: true, + TrustedCerts: cert(s), + } + }, checks( check{"/blah/2.1.0/a.deb", "u3", "x", content}, check{"/blah/2.1.0/a.tar", "u3", "x", content}, @@ -285,16 +329,100 @@ func TestUpload(t *testing.T) { check{"/blah/2.1.0/a.sig", "u3", "x", content}, ), }, + {"bad-template", true, true, true, true, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeBinary, + Name: "a", + Target: s.URL + "/{{.ProjectNameXXX}}/{{.VersionXXX}}/", + Username: "u3", + Checksum: true, + Signature: true, + TrustedCerts: cert(s), + } + }, + checks(), + }, + {"failed-request", true, true, true, true, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeBinary, + Name: "a", + Target: s.URL[0:strings.LastIndex(s.URL, ":")] + "/{{.ProjectName}}/{{.Version}}/", + Username: "u3", + Checksum: true, + Signature: true, + TrustedCerts: cert(s), + } + }, + checks(), + }, + {"broken-cert", false, true, false, true, + func(s *httptest.Server) (*context.Context, config.Put) { + return ctx, config.Put{ + Mode: ModeBinary, + Name: "a", + Target: s.URL + "/{{.ProjectName}}/{{.Version}}/", + Username: "u3", + Checksum: false, + Signature: false, + TrustedCerts: "bad certs!", + } + }, + checks(), + }, + {"skip-publishing", true, true, true, true, + func(s *httptest.Server) (*context.Context, config.Put) { + c := *ctx + c.SkipPublish = true + return &c, config.Put{} + }, + checks(), + }, } + + uploadAndCheck := func(setup func(*httptest.Server) (*context.Context, config.Put), wantErrPlain, wantErrTLS bool, check func(r []*h.Request) error, srv *httptest.Server) { + requests = nil + ctx, put := setup(srv) + wantErr := wantErrPlain + if srv.Certificate() != nil { + wantErr = wantErrTLS + } + if err := Upload(ctx, []config.Put{put}, "test", is2xx); (err != nil) != wantErr { + t.Errorf("Upload() error = %v, wantErr %v", err, wantErr) + } + if err := check(requests); err != nil { + t.Errorf("Upload() request invalid. Error: %v", err) + } + } + for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - requests = nil - if err := Upload(tt.ctx, []config.Put{tt.put}, "test", is2xx); (err != nil) != tt.wantErr { - t.Errorf("Upload() error = %v, wantErr %v", err, tt.wantErr) - } - if err := tt.check(requests); err != nil { - t.Errorf("Upload() request invalid. Error: %v", err) - } - }) + if tt.tryPlain { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(mux) + defer srv.Close() + uploadAndCheck(tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv) + }) + } + if tt.tryTLS { + t.Run(tt.name+"-tls", func(t *testing.T) { + srv := httptest.NewUnstartedServer(mux) + srv.StartTLS() + defer srv.Close() + uploadAndCheck(tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv) + }) + } } + +} + +func cert(srv *httptest.Server) string { + if srv == nil || srv.Certificate() == nil { + return "" + } + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: srv.Certificate().Raw, + } + return string(pem.EncodeToMemory(block)) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 3b0bcf3b7..5d2365218 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -287,12 +287,13 @@ type S3 struct { // Put HTTP upload configuration type Put struct { - Name string `yaml:",omitempty"` - Target string `yaml:",omitempty"` - Username string `yaml:",omitempty"` - Mode string `yaml:",omitempty"` - Checksum bool `yaml:",omitempty"` - Signature bool `yaml:",omitempty"` + Name string `yaml:",omitempty"` + Target string `yaml:",omitempty"` + Username string `yaml:",omitempty"` + Mode string `yaml:",omitempty"` + Checksum bool `yaml:",omitempty"` + Signature bool `yaml:",omitempty"` + TrustedCerts string `yaml:"trusted_certificates,omitempty"` } // Project includes all project configuration @@ -317,7 +318,6 @@ type Project struct { EnvFiles EnvFiles `yaml:"env_files,omitempty"` Git Git `yaml:",omitempty"` Before Before `yaml:",omitempty"` - TrustedCerts string `yaml:"trusted_certificates,omitempty"` // this is a hack ¯\_(ツ)_/¯ SingleBuild Build `yaml:"build,omitempty"` diff --git a/www/content/artifactory.md b/www/content/artifactory.md index 32b6ebdaf..00baac49c 100644 --- a/www/content/artifactory.md +++ b/www/content/artifactory.md @@ -95,21 +95,25 @@ certificate chain in your configuration. The trusted certificate chain will be used to validate the server certificates. -You can set the trusted certificate chain using the global `trusted_certificates` -setting and PEM encoded certificates on a YAML literal block like this: +You can set the trusted certificate chain using the `trusted_certificates` +setting the artifactory section with PEM encoded certificates on a YAML literal +block like this: ```yaml -trusted_certificates: | - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- +puts: + - name: "some artifactory server with a private TLS certificate" + #...(other settings)... + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- ``` ## Customization @@ -136,13 +140,13 @@ artifactories: checksum: true # Upload signatures (defaults to false) signature: true -# Certificate chain used to validate server certificates -trusted_certificates: | - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- + # Certificate chain used to validate server certificates + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- ``` These settings should allow you to push your artifacts into multiple Artifactories. diff --git a/www/content/put.md b/www/content/put.md index 9d10cef2b..5a1f33aec 100644 --- a/www/content/put.md +++ b/www/content/put.md @@ -5,7 +5,6 @@ hideFromIndex: true weight: 120 --- -Since [vX.Y.Z](https://github.com/goreleaser/goreleaser/releases/tag/vX.Y.Z), GoReleaser supports building and pushing artifacts to HTTP servers using simple HTTP PUT requests. ## How it works @@ -88,25 +87,29 @@ The name will be transformed to uppercase. ### Server authentication You can authenticate your TLS server adding a trusted X.509 certificate chain -in your configuration. +in your put configuration. The trusted certificate chain will be used to validate the server certificates. -You can set the trusted certificate chain using the global `trusted_certificates` -setting and PEM encoded certificates on a YAML literal block like this: +You can set the trusted certificate chain using the `trusted_certificates` +setting the put section with PEM encoded certificates on a YAML literal block +like this: ```yaml -trusted_certificates: | - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- +puts: + - name: "some HTTP/TLS server" + #...(other settings)... + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- ``` ## Customization @@ -133,13 +136,13 @@ puts: checksum: true # Upload signatures (defaults to false) signature: true -# Certificate chain used to validate server certificates -trusted_certificates: | - -----BEGIN CERTIFICATE----- - MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE - ...(edited content)... - TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== - -----END CERTIFICATE----- + # Certificate chain used to validate server certificates + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDrjCCApagAwIBAgIIShr2zchZo+8wDQYJKoZIhvcNAQENBQAwNTEXMBUGA1UE + ...(edited content)... + TyzMJasj5BPZrmKjJb6O/tOtEIJ66xPSBTxPShkEYHnB7A== + -----END CERTIFICATE----- ``` These settings should allow you to push your artifacts into multiple HTTP servers.