You've already forked woodpecker
mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-11-23 21:44:44 +02:00
Add Header User-Agent for request client (#5664)
add Header User-Agent for request client for more precise in recognized the http request from. close #3778
This commit is contained in:
@@ -208,6 +208,7 @@
|
|||||||
"unsanitize",
|
"unsanitize",
|
||||||
"Upsert",
|
"Upsert",
|
||||||
"urfave",
|
"urfave",
|
||||||
|
"useragent",
|
||||||
"usecase",
|
"usecase",
|
||||||
"varchar",
|
"varchar",
|
||||||
"varz",
|
"varz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,23 +73,27 @@ func NewClient(ctx context.Context, c *cli.Command) (woodpecker.Client, error) {
|
|||||||
|
|
||||||
trans, _ := client.Transport.(*oauth2.Transport)
|
trans, _ := client.Transport.(*oauth2.Transport)
|
||||||
|
|
||||||
|
var baseTransport http.RoundTripper
|
||||||
if len(socks) != 0 && !socksOff {
|
if len(socks) != 0 && !socksOff {
|
||||||
dialer, err := proxy.SOCKS5("tcp", socks, nil, proxy.Direct)
|
dialer, err := proxy.SOCKS5("tcp", socks, nil, proxy.Direct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
trans.Base = &http.Transport{
|
baseTransport = &http.Transport{
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
Dial: dialer.Dial,
|
Dial: dialer.Dial,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trans.Base = &http.Transport{
|
baseTransport = &http.Transport{
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap the base transport with User-Agent support
|
||||||
|
trans.Base = httputil.NewUserAgentRoundTripper(baseTransport, "cli")
|
||||||
|
|
||||||
return woodpecker.NewClient(server, client), nil
|
return woodpecker.NewClient(server, client), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
|
||||||
backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
|
backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,7 +93,9 @@ func httpClientOfOpts(dockerCertPath string, verifyTLS bool) *http.Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: &http.Transport{TLSClientConfig: tlsConf},
|
Transport: httputil.NewUserAgentRoundTripper(
|
||||||
|
&http.Transport{TLSClientConfig: tlsConf},
|
||||||
|
"backend-docker"),
|
||||||
CheckRedirect: client.CheckRedirect,
|
CheckRedirect: client.CheckRedirect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"go.woodpecker-ci.org/woodpecker/v3/server/forge/common"
|
"go.woodpecker-ci.org/woodpecker/v3/server/forge/common"
|
||||||
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -449,7 +450,7 @@ func (c *config) newClient(ctx context.Context, u *model.User) *internal.Client
|
|||||||
|
|
||||||
// helper function to return the bitbucket oauth2 client.
|
// helper function to return the bitbucket oauth2 client.
|
||||||
func (c *config) newClientToken(ctx context.Context, accessToken, refreshToken string) *internal.Client {
|
func (c *config) newClientToken(ctx context.Context, accessToken, refreshToken string) *internal.Client {
|
||||||
return internal.NewClientToken(
|
client := internal.NewClientToken(
|
||||||
ctx,
|
ctx,
|
||||||
c.api,
|
c.api,
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -459,6 +460,8 @@ func (c *config) newClientToken(ctx context.Context, accessToken, refreshToken s
|
|||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
client.Client = httputil.WrapClient(client.Client, "forge-bitbucket")
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to return the bitbucket oauth2 config.
|
// helper function to return the bitbucket oauth2 config.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -768,5 +769,6 @@ func (c *client) newClient(ctx context.Context, u *model.User) (*bb.Client, erro
|
|||||||
AccessToken: u.AccessToken,
|
AccessToken: u.AccessToken,
|
||||||
}
|
}
|
||||||
client := config.Client(ctx, t)
|
client := config.Client(ctx, t)
|
||||||
|
client = httputil.WrapClient(client, "forge-bitbucketdatacenter")
|
||||||
return bb.NewClient(c.urlAPI, client)
|
return bb.NewClient(c.urlAPI, client)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -586,12 +587,13 @@ func (c *Forgejo) newClientToken(ctx context.Context, token string) (*forgejo.Cl
|
|||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client, err := forgejo.NewClient(c.url, forgejo.SetToken(token), forgejo.SetHTTPClient(httpClient), forgejo.SetContext(ctx))
|
wrappedClient := httputil.WrapClient(httpClient, "forge-forgejo")
|
||||||
|
client, err := forgejo.NewClient(c.url, forgejo.SetToken(token), forgejo.SetHTTPClient(wrappedClient), forgejo.SetContext(ctx))
|
||||||
if err != nil &&
|
if err != nil &&
|
||||||
(errors.Is(err, &forgejo.ErrUnknownVersion{}) || strings.Contains(err.Error(), "Malformed version")) {
|
(errors.Is(err, &forgejo.ErrUnknownVersion{}) || strings.Contains(err.Error(), "Malformed version")) {
|
||||||
// we guess it's a dev forgejo version
|
// we guess it's a dev forgejo version
|
||||||
log.Error().Err(err).Msgf("could not detect forgejo version, assume dev version %s", forgejoDevVersion)
|
log.Error().Err(err).Msgf("could not detect forgejo version, assume dev version %s", forgejoDevVersion)
|
||||||
client, err = forgejo.NewClient(c.url, forgejo.SetForgejoVersion(forgejoDevVersion), forgejo.SetToken(token), forgejo.SetHTTPClient(httpClient), forgejo.SetContext(ctx))
|
client, err = forgejo.NewClient(c.url, forgejo.SetForgejoVersion(forgejoDevVersion), forgejo.SetToken(token), forgejo.SetHTTPClient(wrappedClient), forgejo.SetContext(ctx))
|
||||||
}
|
}
|
||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import (
|
|||||||
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
shared_utils "go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -593,12 +594,13 @@ func (c *Gitea) newClientToken(ctx context.Context, token string) (*gitea.Client
|
|||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
client, err := gitea.NewClient(c.url, gitea.SetToken(token), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
|
wrappedClient := httputil.WrapClient(httpClient, "forge-gitea")
|
||||||
|
client, err := gitea.NewClient(c.url, gitea.SetToken(token), gitea.SetHTTPClient(wrappedClient), gitea.SetContext(ctx))
|
||||||
if err != nil &&
|
if err != nil &&
|
||||||
(errors.Is(err, &gitea.ErrUnknownVersion{}) || strings.Contains(err.Error(), "Malformed version")) {
|
(errors.Is(err, &gitea.ErrUnknownVersion{}) || strings.Contains(err.Error(), "Malformed version")) {
|
||||||
// we guess it's a dev gitea version
|
// we guess it's a dev gitea version
|
||||||
log.Error().Err(err).Msgf("could not detect gitea version, assume dev version %s", giteaDevVersion)
|
log.Error().Err(err).Msgf("could not detect gitea version, assume dev version %s", giteaDevVersion)
|
||||||
client, err = gitea.NewClient(c.url, gitea.SetGiteaVersion(giteaDevVersion), gitea.SetToken(token), gitea.SetHTTPClient(httpClient), gitea.SetContext(ctx))
|
client, err = gitea.NewClient(c.url, gitea.SetGiteaVersion(giteaDevVersion), gitea.SetToken(token), gitea.SetHTTPClient(wrappedClient), gitea.SetContext(ctx))
|
||||||
}
|
}
|
||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
"go.woodpecker-ci.org/woodpecker/v3/shared/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -469,15 +470,22 @@ func (c *client) newClientToken(ctx context.Context, token string) *github.Clien
|
|||||||
&oauth2.Token{AccessToken: token},
|
&oauth2.Token{AccessToken: token},
|
||||||
)
|
)
|
||||||
tc := oauth2.NewClient(ctx, ts)
|
tc := oauth2.NewClient(ctx, ts)
|
||||||
|
|
||||||
|
// Get the oauth2 transport to set custom base
|
||||||
|
tp, _ := tc.Transport.(*oauth2.Transport)
|
||||||
|
|
||||||
|
baseTransport := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
if c.SkipVerify {
|
if c.SkipVerify {
|
||||||
tp, _ := tc.Transport.(*oauth2.Transport)
|
baseTransport.TLSClientConfig = &tls.Config{
|
||||||
tp.Base = &http.Transport{
|
InsecureSkipVerify: true,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap the base transport with User-Agent support
|
||||||
|
tp.Base = httputil.NewUserAgentRoundTripper(baseTransport, "forge-github")
|
||||||
|
|
||||||
client := github.NewClient(tc)
|
client := github.NewClient(tc)
|
||||||
client.BaseURL, _ = url.Parse(c.API)
|
client.BaseURL, _ = url.Parse(c.API)
|
||||||
return client
|
return client
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import (
|
|||||||
|
|
||||||
gitlab "gitlab.com/gitlab-org/api/client-go"
|
gitlab "gitlab.com/gitlab-org/api/client-go"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,10 +35,12 @@ func newClient(url, accessToken string, skipVerify bool) (*gitlab.Client, error)
|
|||||||
return gitlab.NewAuthSourceClient(gitlab.OAuthTokenSource{
|
return gitlab.NewAuthSourceClient(gitlab.OAuthTokenSource{
|
||||||
TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
|
TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
|
||||||
}, gitlab.WithBaseURL(url), gitlab.WithHTTPClient(&http.Client{
|
}, gitlab.WithBaseURL(url), gitlab.WithHTTPClient(&http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: httputil.NewUserAgentRoundTripper(
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipVerify},
|
&http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipVerify},
|
||||||
},
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
"forge-gitlab"),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/yaronf/httpsign"
|
"github.com/yaronf/httpsign"
|
||||||
|
|
||||||
host_matcher "go.woodpecker-ci.org/woodpecker/v3/server/services/utils/hostmatcher"
|
host_matcher "go.woodpecker-ci.org/woodpecker/v3/server/services/utils/hostmatcher"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/shared/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -58,12 +59,18 @@ func getHTTPClient(privateKey crypto.PrivateKey, allowedHostListValue string) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{
|
// Create base transport with custom User-Agent
|
||||||
Timeout: timeout,
|
baseTransport := httputil.NewUserAgentRoundTripper(
|
||||||
Transport: &http.Transport{
|
&http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
||||||
DialContext: host_matcher.NewDialContext("extensions", allowedHostMatcher),
|
DialContext: host_matcher.NewDialContext("extensions", allowedHostMatcher),
|
||||||
},
|
},
|
||||||
|
"server-extensions",
|
||||||
|
)
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: baseTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
config := httpsign.NewClientConfig().SetSignatureName(pubKeyID).SetSigner(signer)
|
config := httpsign.NewClientConfig().SetSignatureName(pubKeyID).SetSigner(signer)
|
||||||
|
|||||||
72
shared/httputil/useragent.go
Normal file
72
shared/httputil/useragent.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2024 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAgentRoundTripper is an http.RoundTripper that sets a custom User-Agent header
|
||||||
|
// on all outgoing requests.
|
||||||
|
type UserAgentRoundTripper struct {
|
||||||
|
base http.RoundTripper
|
||||||
|
userAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserAgentRoundTripper creates a new RoundTripper that adds the Woodpecker User-Agent
|
||||||
|
// to all requests. If base is nil, http.DefaultTransport is used.
|
||||||
|
func NewUserAgentRoundTripper(base http.RoundTripper, component string) *UserAgentRoundTripper {
|
||||||
|
if base == nil {
|
||||||
|
base = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := fmt.Sprintf("Woodpecker/%s", version.String())
|
||||||
|
if component != "" {
|
||||||
|
userAgent = fmt.Sprintf("%s (%s)", userAgent, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserAgentRoundTripper{
|
||||||
|
base: base,
|
||||||
|
userAgent: userAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip implements the http.RoundTripper interface.
|
||||||
|
func (rt *UserAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
// Clone the request to avoid modifying the original
|
||||||
|
reqClone := req.Clone(req.Context())
|
||||||
|
|
||||||
|
// Set the User-Agent header if not already set
|
||||||
|
if reqClone.Header.Get("User-Agent") == "" {
|
||||||
|
reqClone.Header.Set("User-Agent", rt.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the request using the base transport
|
||||||
|
return rt.base.RoundTrip(reqClone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapClient wraps an existing http.Client with the UserAgentRoundTripper.
|
||||||
|
// If client is nil, a new client with default settings is created.
|
||||||
|
func WrapClient(client *http.Client, component string) *http.Client {
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Transport = NewUserAgentRoundTripper(client.Transport, component)
|
||||||
|
return client
|
||||||
|
}
|
||||||
169
shared/httputil/useragent_test.go
Normal file
169
shared/httputil/useragent_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2024 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUserAgentRoundTripper(t *testing.T) {
|
||||||
|
t.Run("with custom component", func(t *testing.T) {
|
||||||
|
rt := NewUserAgentRoundTripper(nil, "test-component")
|
||||||
|
assert.NotNil(t, rt)
|
||||||
|
assert.NotNil(t, rt.base)
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s (test-component)", version.String())
|
||||||
|
assert.Equal(t, expectedUA, rt.userAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without component", func(t *testing.T) {
|
||||||
|
rt := NewUserAgentRoundTripper(nil, "")
|
||||||
|
assert.NotNil(t, rt)
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s", version.String())
|
||||||
|
assert.Equal(t, expectedUA, rt.userAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with custom base transport", func(t *testing.T) {
|
||||||
|
customTransport := &http.Transport{}
|
||||||
|
rt := NewUserAgentRoundTripper(customTransport, "custom")
|
||||||
|
assert.Equal(t, customTransport, rt.base)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAgentRoundTripper_RoundTrip(t *testing.T) {
|
||||||
|
// Create a test server to capture requests
|
||||||
|
var capturedUserAgent string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedUserAgent = r.Header.Get("User-Agent")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
t.Run("sets user-agent when not present", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "agent"),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s (agent)", version.String())
|
||||||
|
assert.Equal(t, expectedUA, capturedUserAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves existing user-agent", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "agent"),
|
||||||
|
}
|
||||||
|
|
||||||
|
customUA := "CustomUserAgent/1.0"
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("User-Agent", customUA)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, customUA, capturedUserAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not modify original request", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
originalUserAgent := req.Header.Get("User-Agent")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Original request should remain unchanged
|
||||||
|
assert.Equal(t, originalUserAgent, req.Header.Get("User-Agent"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapClient(t *testing.T) {
|
||||||
|
t.Run("wraps existing client", func(t *testing.T) {
|
||||||
|
originalClient := &http.Client{}
|
||||||
|
wrappedClient := WrapClient(originalClient, "cli")
|
||||||
|
|
||||||
|
assert.Equal(t, originalClient, wrappedClient)
|
||||||
|
assert.IsType(t, &UserAgentRoundTripper{}, wrappedClient.Transport)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creates new client when nil", func(t *testing.T) {
|
||||||
|
wrappedClient := WrapClient(nil, "server")
|
||||||
|
|
||||||
|
assert.NotNil(t, wrappedClient)
|
||||||
|
assert.IsType(t, &UserAgentRoundTripper{}, wrappedClient.Transport)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves existing transport", func(t *testing.T) {
|
||||||
|
customTransport := &http.Transport{}
|
||||||
|
originalClient := &http.Client{
|
||||||
|
Transport: customTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedClient := WrapClient(originalClient, "test")
|
||||||
|
|
||||||
|
rt, ok := wrappedClient.Transport.(*UserAgentRoundTripper)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, customTransport, rt.base)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_UserAgentInRealRequest(t *testing.T) {
|
||||||
|
// Test with a real HTTP server
|
||||||
|
var receivedHeaders http.Header
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedHeaders = r.Header.Clone()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := WrapClient(nil, "integration-test")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
userAgent := receivedHeaders.Get("User-Agent")
|
||||||
|
assert.NotEmpty(t, userAgent)
|
||||||
|
assert.Contains(t, userAgent, "Woodpecker/")
|
||||||
|
assert.Contains(t, userAgent, "(integration-test)")
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -50,12 +52,14 @@ type client struct {
|
|||||||
|
|
||||||
// New returns a client at the specified url.
|
// New returns a client at the specified url.
|
||||||
func New(uri string) Client {
|
func New(uri string) Client {
|
||||||
return &client{http.DefaultClient, strings.TrimSuffix(uri, "/")}
|
wrappedClient := httputil.WrapClient(http.DefaultClient, "go-client")
|
||||||
|
return &client{wrappedClient, strings.TrimSuffix(uri, "/")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a client at the specified url.
|
// NewClient returns a client at the specified url.
|
||||||
func NewClient(uri string, cli *http.Client) Client {
|
func NewClient(uri string, cli *http.Client) Client {
|
||||||
return &client{cli, strings.TrimSuffix(uri, "/")}
|
wrappedClient := httputil.WrapClient(cli, "go-client")
|
||||||
|
return &client{wrappedClient, strings.TrimSuffix(uri, "/")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetClient sets the http.Client.
|
// SetClient sets the http.Client.
|
||||||
|
|||||||
72
woodpecker-go/woodpecker/httputil/useragent.go
Normal file
72
woodpecker-go/woodpecker/httputil/useragent.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2024 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAgentRoundTripper is an http.RoundTripper that sets a custom User-Agent header
|
||||||
|
// on all outgoing requests.
|
||||||
|
type UserAgentRoundTripper struct {
|
||||||
|
base http.RoundTripper
|
||||||
|
userAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserAgentRoundTripper creates a new RoundTripper that adds the Woodpecker User-Agent
|
||||||
|
// to all requests. If base is nil, http.DefaultTransport is used.
|
||||||
|
func NewUserAgentRoundTripper(base http.RoundTripper, component string) *UserAgentRoundTripper {
|
||||||
|
if base == nil {
|
||||||
|
base = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := fmt.Sprintf("Woodpecker/%s", version.String())
|
||||||
|
if component != "" {
|
||||||
|
userAgent = fmt.Sprintf("%s (%s)", userAgent, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserAgentRoundTripper{
|
||||||
|
base: base,
|
||||||
|
userAgent: userAgent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip implements the http.RoundTripper interface.
|
||||||
|
func (rt *UserAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
// Clone the request to avoid modifying the original
|
||||||
|
reqClone := req.Clone(req.Context())
|
||||||
|
|
||||||
|
// Set the User-Agent header if not already set
|
||||||
|
if reqClone.Header.Get("User-Agent") == "" {
|
||||||
|
reqClone.Header.Set("User-Agent", rt.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the request using the base transport
|
||||||
|
return rt.base.RoundTrip(reqClone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapClient wraps an existing http.Client with the UserAgentRoundTripper.
|
||||||
|
// If client is nil, a new client with default settings is created.
|
||||||
|
func WrapClient(client *http.Client, component string) *http.Client {
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Transport = NewUserAgentRoundTripper(client.Transport, component)
|
||||||
|
return client
|
||||||
|
}
|
||||||
169
woodpecker-go/woodpecker/httputil/useragent_test.go
Normal file
169
woodpecker-go/woodpecker/httputil/useragent_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2024 Woodpecker Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUserAgentRoundTripper(t *testing.T) {
|
||||||
|
t.Run("with custom component", func(t *testing.T) {
|
||||||
|
rt := NewUserAgentRoundTripper(nil, "test-component")
|
||||||
|
assert.NotNil(t, rt)
|
||||||
|
assert.NotNil(t, rt.base)
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s (test-component)", version.String())
|
||||||
|
assert.Equal(t, expectedUA, rt.userAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without component", func(t *testing.T) {
|
||||||
|
rt := NewUserAgentRoundTripper(nil, "")
|
||||||
|
assert.NotNil(t, rt)
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s", version.String())
|
||||||
|
assert.Equal(t, expectedUA, rt.userAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with custom base transport", func(t *testing.T) {
|
||||||
|
customTransport := &http.Transport{}
|
||||||
|
rt := NewUserAgentRoundTripper(customTransport, "custom")
|
||||||
|
assert.Equal(t, customTransport, rt.base)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAgentRoundTripper_RoundTrip(t *testing.T) {
|
||||||
|
// Create a test server to capture requests
|
||||||
|
var capturedUserAgent string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedUserAgent = r.Header.Get("User-Agent")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
t.Run("sets user-agent when not present", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "agent"),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
expectedUA := fmt.Sprintf("Woodpecker/%s (agent)", version.String())
|
||||||
|
assert.Equal(t, expectedUA, capturedUserAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves existing user-agent", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "agent"),
|
||||||
|
}
|
||||||
|
|
||||||
|
customUA := "CustomUserAgent/1.0"
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("User-Agent", customUA)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, customUA, capturedUserAgent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("does not modify original request", func(t *testing.T) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewUserAgentRoundTripper(nil, "test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
originalUserAgent := req.Header.Get("User-Agent")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Original request should remain unchanged
|
||||||
|
assert.Equal(t, originalUserAgent, req.Header.Get("User-Agent"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapClient(t *testing.T) {
|
||||||
|
t.Run("wraps existing client", func(t *testing.T) {
|
||||||
|
originalClient := &http.Client{}
|
||||||
|
wrappedClient := WrapClient(originalClient, "cli")
|
||||||
|
|
||||||
|
assert.Equal(t, originalClient, wrappedClient)
|
||||||
|
assert.IsType(t, &UserAgentRoundTripper{}, wrappedClient.Transport)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creates new client when nil", func(t *testing.T) {
|
||||||
|
wrappedClient := WrapClient(nil, "server")
|
||||||
|
|
||||||
|
assert.NotNil(t, wrappedClient)
|
||||||
|
assert.IsType(t, &UserAgentRoundTripper{}, wrappedClient.Transport)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves existing transport", func(t *testing.T) {
|
||||||
|
customTransport := &http.Transport{}
|
||||||
|
originalClient := &http.Client{
|
||||||
|
Transport: customTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedClient := WrapClient(originalClient, "test")
|
||||||
|
|
||||||
|
rt, ok := wrappedClient.Transport.(*UserAgentRoundTripper)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, customTransport, rt.base)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_UserAgentInRealRequest(t *testing.T) {
|
||||||
|
// Test with a real HTTP server
|
||||||
|
var receivedHeaders http.Header
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedHeaders = r.Header.Clone()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := WrapClient(nil, "integration-test")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
userAgent := receivedHeaders.Get("User-Agent")
|
||||||
|
assert.NotEmpty(t, userAgent)
|
||||||
|
assert.Contains(t, userAgent, "Woodpecker/")
|
||||||
|
assert.Contains(t, userAgent, "(integration-test)")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user