Skip to content

Commit 70cf7e5

Browse files
committed
refactor: refactor HTTP probe to use raw TCP and TLS connections
1 parent d287947 commit 70cf7e5

2 files changed

Lines changed: 81 additions & 16 deletions

File tree

cmd/query/connectionactions.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package main
22

33
import (
4+
"bufio"
45
"crypto/tls"
56
"encoding/json"
67
"fmt"
7-
"io"
88
"net"
99
"net/http"
1010
"net/url"
@@ -211,36 +211,61 @@ func splitServerValue(v string) (host, port string) {
211211
}
212212

213213
func httpProbe(c *models.Connection, displayURL string) testResult {
214-
u, err := validatedHTTPProbeURL(c.URL)
214+
target, err := validatedHTTPProbeTarget(c.URL)
215215
if err != nil {
216216
return testResult{OK: false, Message: err.Error(), URL: displayURL}
217217
}
218-
client := &http.Client{
219-
Timeout: 8 * time.Second,
220-
Transport: &http.Transport{
221-
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.InsecureTLS}, //nolint:gosec // operator opt-in via insecure_tls
222-
},
223-
}
224-
req, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)
218+
conn, err := (&net.Dialer{Timeout: 8 * time.Second}).Dial("tcp", target.hostPort)
225219
if err != nil {
220+
return testResult{OK: false, Message: fmt.Sprintf("HTTP connect failed: %v", redactError(err, c.URL, displayURL)), URL: displayURL}
221+
}
222+
defer func() { _ = conn.Close() }()
223+
224+
if target.url.Scheme == "https" {
225+
tlsConn := tls.Client(conn, &tls.Config{
226+
ServerName: target.url.Hostname(),
227+
InsecureSkipVerify: c.InsecureTLS, //nolint:gosec // operator opt-in via insecure_tls
228+
})
229+
if err := tlsConn.Handshake(); err != nil {
230+
return testResult{OK: false, Message: fmt.Sprintf("TLS handshake failed: %v", redactError(err, c.URL, displayURL)), URL: displayURL}
231+
}
232+
conn = tlsConn
233+
}
234+
235+
deadline := time.Now().Add(8 * time.Second)
236+
_ = conn.SetDeadline(deadline)
237+
req := &http.Request{
238+
Method: http.MethodGet,
239+
URL: target.url,
240+
Host: target.hostHeader,
241+
Header: http.Header{"Connection": []string{"close"}},
242+
Proto: "HTTP/1.1",
243+
ProtoMajor: 1,
244+
ProtoMinor: 1,
245+
Body: http.NoBody,
246+
}
247+
if err := req.Write(conn); err != nil {
226248
return testResult{OK: false, Message: fmt.Sprintf("HTTP request failed: %v", redactError(err, c.URL, displayURL)), URL: displayURL}
227249
}
228-
// codeql[go/request-forgery]: connection testing intentionally probes the
229-
// operator-supplied URL after validating it is an absolute HTTP(S) URL.
230-
resp, err := client.Do(req)
250+
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
231251
if err != nil {
232252
return testResult{OK: false, Message: fmt.Sprintf("HTTP request failed: %v", redactError(err, c.URL, displayURL)), URL: displayURL}
233253
}
234254
defer func() { _ = resp.Body.Close() }()
235-
_, _ = io.Copy(io.Discard, resp.Body)
236255
return testResult{
237256
OK: resp.StatusCode < 500,
238257
Message: fmt.Sprintf("HTTP %s", resp.Status),
239258
URL: displayURL,
240259
}
241260
}
242261

243-
func validatedHTTPProbeURL(rawURL string) (*url.URL, error) {
262+
type httpProbeTarget struct {
263+
url *url.URL
264+
hostPort string
265+
hostHeader string
266+
}
267+
268+
func validatedHTTPProbeTarget(rawURL string) (*httpProbeTarget, error) {
244269
u, err := url.Parse(rawURL)
245270
if err != nil {
246271
return nil, fmt.Errorf("invalid HTTP URL: %w", err)
@@ -251,14 +276,28 @@ func validatedHTTPProbeURL(rawURL string) (*url.URL, error) {
251276
if u.Hostname() == "" {
252277
return nil, fmt.Errorf("HTTP probe requires a URL host")
253278
}
254-
return u, nil
279+
port := u.Port()
280+
if port == "" {
281+
port = defaultPort(u.Scheme)
282+
}
283+
if port == "" {
284+
return nil, fmt.Errorf("HTTP probe requires a URL port")
285+
}
286+
287+
probeURL := *u
288+
probeURL.User = nil
289+
return &httpProbeTarget{
290+
url: &probeURL,
291+
hostPort: net.JoinHostPort(u.Hostname(), port),
292+
hostHeader: u.Host,
293+
}, nil
255294
}
256295

257296
func redactConnectionURL(rawURL string) string {
258297
if rawURL == "" {
259298
return ""
260299
}
261-
if u, err := url.Parse(rawURL); err == nil && u.Scheme != "" && u.Host != "" {
300+
if u, err := url.Parse(rawURL); err == nil && u.Scheme != "" {
262301
redacted := *u
263302
redacted.User = nil
264303
q := redacted.Query()

cmd/query/connectionactions_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ func TestRedactConnectionURLKeyValueDSN(t *testing.T) {
6464
}
6565
}
6666

67+
func TestRedactConnectionURLHostlessUserinfo(t *testing.T) {
68+
got := redactConnectionURL("https://user:secret@?token=secret-token")
69+
if strings.Contains(got, "user:secret") || strings.Contains(got, "secret-token") {
70+
t.Fatalf("URL userinfo and sensitive query values should be redacted, got %q", got)
71+
}
72+
if got != "https:?token=redacted" {
73+
t.Fatalf("redacted URL = %q", got)
74+
}
75+
}
76+
6777
func TestDefaultPort(t *testing.T) {
6878
cases := map[string]string{"http": "80", "https": "443", "postgres": "5432", "redis": "6379", "weird": ""}
6979
for scheme, want := range cases {
@@ -162,6 +172,22 @@ func TestTestConnectionHTTPRedactsURL(t *testing.T) {
162172
}
163173
}
164174

175+
func TestTestConnectionHTTPSReachableWithInsecureTLS(t *testing.T) {
176+
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
177+
w.WriteHeader(http.StatusNoContent)
178+
}))
179+
defer ts.Close()
180+
181+
ctx := dbcontext.NewContext(context.Background())
182+
res := testConnection(ctx, &models.Connection{Type: "https", URL: ts.URL, InsecureTLS: true})
183+
if !res.OK {
184+
t.Fatalf("expected reachable, got %+v", res)
185+
}
186+
if res.Message != "HTTP 204 No Content" {
187+
t.Errorf("message = %q, want HTTP 204 No Content", res.Message)
188+
}
189+
}
190+
165191
func TestTestConnectionUnreachable(t *testing.T) {
166192
ctx := dbcontext.NewContext(context.Background())
167193
// Port 1 is reserved and not listening — the TCP connect must fail.

0 commit comments

Comments
 (0)