diff --git a/README.md b/README.md index 62d88721d5..c8a9360f24 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ conn.SetConnMaxLifetime(time.Hour) * read_timeout - a duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix such as "300ms", "1s". Valid time units are "ms", "s", "m" (default 5m). * max_compression_buffer - max size (bytes) of compression buffer during column by column compression (default 10MiB) * client_info_product - optional list (comma separated) of product name and version pair separated with `/`. This value will be pass a part of client info. e.g. `client_info_product=my_app/1.0,my_module/0.1` More details in [Client info](#client-info) section. +* http_proxy - HTTP proxy address SSL/TLS parameters: @@ -174,6 +175,8 @@ clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms ### HTTP Support (Experimental) +**Note**: using HTTP protocol is possible only with `database/sql` interface. + The native format can be used over the HTTP protocol. This is useful in scenarios where users need to proxy traffic e.g. using [ChProxy](https://www.chproxy.org/) or via load balancers. This can be achieved by modifying the DSN to specify the HTTP protocol. @@ -203,7 +206,19 @@ conn := clickhouse.OpenDB(&clickhouse.Options{ }) ``` -**Note**: using HTTP protocol is possible only with `database/sql` interface. +#### Proxy support + +HTTP proxy can be set in the DSN string by specifying the `http_proxy` parameter. +(make sure to URL encode the proxy address) + +```sh +http://host1:8123,host2:8123/database?dial_timeout=200ms&max_execution_time=60&http_proxy=http%3A%2F%2Fproxy%3A8080 +``` + +If you are using `clickhouse.OpenDB`, set the `HTTProxy` field in the `clickhouse.Options`. + +An alternative way is to enable proxy by setting the `HTTP_PROXY` (for HTTP) or `HTTPS_PROXY` (for HTTPS) environment variables. +See more details in the [Go documentation](https://pkg.go.dev/net/http#ProxyFromEnvironment). ## Compression diff --git a/clickhouse_options.go b/clickhouse_options.go index 57376029db..541bf1a49f 100644 --- a/clickhouse_options.go +++ b/clickhouse_options.go @@ -22,6 +22,7 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "net/url" "strconv" "strings" @@ -121,6 +122,8 @@ type DialResult struct { conn *connect } +type HTTPProxy func(*http.Request) (*url.URL, error) + type Options struct { Protocol Protocol ClientInfo ClientInfo @@ -143,7 +146,10 @@ type Options struct { HttpHeaders map[string]string // set additional headers on HTTP requests HttpUrlPath string // set additional URL path for HTTP requests BlockBufferSize uint8 // default 2 - can be overwritten on query - MaxCompressionBuffer int // default 10485760 - measured in bytes i.e. 10MiB + MaxCompressionBuffer int // default 10485760 - measured in bytes i.e. + + // HTTPProxy specifies an HTTP proxy URL to use for requests made by the client. + HTTPProxyURL *url.URL scheme string ReadTimeout time.Duration @@ -302,6 +308,12 @@ func (o *Options) fromDSN(in string) error { version, }) } + case "http_proxy": + proxyURL, err := url.Parse(params.Get(v)) + if err != nil { + return fmt.Errorf("clickhouse [dsn parse]: http_proxy: %s", err) + } + o.HTTPProxyURL = proxyURL default: switch p := strings.ToLower(params.Get(v)); p { case "true": diff --git a/clickhouse_options_test.go b/clickhouse_options_test.go index a39aeccc7f..63c4e84332 100644 --- a/clickhouse_options_test.go +++ b/clickhouse_options_test.go @@ -19,10 +19,12 @@ package clickhouse import ( "crypto/tls" + "net/url" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestParseDSN does not implement all use cases yet @@ -467,6 +469,19 @@ func TestParseDSN(t *testing.T) { }, "", }, + { + "http protocol with proxy", + "http://127.0.0.1/?http_proxy=http%3A%2F%2Fproxy.example.com%3A3128", + &Options{ + Protocol: HTTP, + TLS: nil, + Addr: []string{"127.0.0.1"}, + Settings: Settings{}, + scheme: "http", + HTTPProxyURL: parseURL(t, "http://proxy.example.com:3128"), + }, + "", + }, } for _, testCase := range testCases { @@ -484,3 +499,9 @@ func TestParseDSN(t *testing.T) { }) } } + +func parseURL(t *testing.T, v string) *url.URL { + u, err := url.Parse(v) + require.NoError(t, err) + return u +} diff --git a/conn_http.go b/conn_http.go index 2084fbd957..c9f2f929b4 100644 --- a/conn_http.go +++ b/conn_http.go @@ -201,8 +201,13 @@ func dialHttp(ctx context.Context, addr string, num int, opt *Options) (*httpCon query.Set("default_format", "Native") u.RawQuery = query.Encode() + httpProxy := http.ProxyFromEnvironment + if opt.HTTPProxyURL != nil { + httpProxy = http.ProxyURL(opt.HTTPProxyURL) + } + t := &http.Transport{ - Proxy: http.ProxyFromEnvironment, + Proxy: httpProxy, DialContext: (&net.Dialer{ Timeout: opt.DialTimeout, }).DialContext, diff --git a/examples/std/connect.go b/examples/std/connect.go index 139afdef1c..d710004262 100644 --- a/examples/std/connect.go +++ b/examples/std/connect.go @@ -20,6 +20,8 @@ package std import ( "database/sql" "fmt" + "net/url" + "github.com/ClickHouse/clickhouse-go/v2" ) @@ -50,3 +52,41 @@ func ConnectDSN() error { } return conn.Ping() } + +func ConnectUsingHTTPProxy() error { + env, err := GetStdTestEnvironment() + if err != nil { + return fmt.Errorf("failed to get test environment: %w", err) + } + + proxyURL, err := url.Parse("http://proxy.example.com:3128") + if err != nil { + return fmt.Errorf("failed to parse proxy URL: %w", err) + } + + conn := clickhouse.OpenDB(&clickhouse.Options{ + Addr: []string{fmt.Sprintf("%s:%d", env.Host, env.Port)}, + Auth: clickhouse.Auth{ + Database: env.Database, + Username: env.Username, + Password: env.Password, + }, + HTTPProxyURL: proxyURL, + }) + return conn.Ping() +} + +func ConnectUsingHTTPProxyDSN() error { + env, err := GetStdTestEnvironment() + if err != nil { + return fmt.Errorf("failed to get test environment: %w", err) + } + + urlEncodedProxyURL := url.QueryEscape("http://proxy.example.com:3128") + + conn, err := sql.Open("clickhouse", fmt.Sprintf("clickhouse://%s:%d?username=%s&password=%s&http_proxy=%s", env.Host, env.Port, env.Username, env.Password, urlEncodedProxyURL)) + if err != nil { + return err + } + return conn.Ping() +}