Skip to content

Commit 37b221a

Browse files
authored
Merge pull request #1198 from courageJ/harden-url-construction-clean
Hardening URL Construction and IPv6 Support
2 parents b87a7ed + 2cef1d1 commit 37b221a

6 files changed

Lines changed: 169 additions & 2 deletions

File tree

kubelet-to-gcm/monitor/controller/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919
import (
2020
"fmt"
2121
"io"
22+
"net"
2223
"net/http"
2324
"net/url"
2425
"strings"
@@ -87,7 +88,7 @@ type Client struct {
8788
// NewClient generates a client to hit the given controller.
8889
func NewClient(host string, port uint, client *http.Client) (*Client, error) {
8990
// Parse our URL upfront, so we can fail fast.
90-
urlStr := fmt.Sprintf("http://%s:%d/metrics", host, port)
91+
urlStr := fmt.Sprintf("http://%s/metrics", net.JoinHostPort(util.NormalizeHost(host), fmt.Sprint(port)))
9192
metricsURL, err := url.Parse(urlStr)
9293
if err != nil {
9394
return nil, err

kubelet-to-gcm/monitor/controller/client_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,52 @@ import (
2626
"github.com/GoogleCloudPlatform/k8s-stackdriver/kubelet-to-gcm/monitor/util"
2727
)
2828

29+
func TestNewClient(t *testing.T) {
30+
testCases := []struct {
31+
name string
32+
host string
33+
port uint
34+
expectedURL string
35+
}{
36+
{
37+
name: "IPv4",
38+
host: "127.0.0.1",
39+
port: 10250,
40+
expectedURL: "http://127.0.0.1:10250/metrics",
41+
},
42+
{
43+
name: "IPv6",
44+
host: "2001:db8::1",
45+
port: 10250,
46+
expectedURL: "http://[2001:db8::1]:10250/metrics",
47+
},
48+
{
49+
name: "Bracketed IPv6",
50+
host: "[2001:db8::1]",
51+
port: 10250,
52+
expectedURL: "http://[2001:db8::1]:10250/metrics",
53+
},
54+
{
55+
name: "Hostname",
56+
host: "localhost",
57+
port: 8080,
58+
expectedURL: "http://localhost:8080/metrics",
59+
},
60+
}
61+
62+
for _, tc := range testCases {
63+
t.Run(tc.name, func(t *testing.T) {
64+
c, err := NewClient(tc.host, tc.port, http.DefaultClient)
65+
if err != nil {
66+
t.Fatalf("NewClient failed: %v", err)
67+
}
68+
if c.metricsURL.String() != tc.expectedURL {
69+
t.Errorf("Expected URL %q, got %q", tc.expectedURL, c.metricsURL.String())
70+
}
71+
})
72+
}
73+
}
74+
2975
func TestDoRequestAndParse_SizeLimit(t *testing.T) {
3076
// Create a mock server that returns a response larger than maxResponseBodySize.
3177
largeDataSize := maxResponseBodySize + 1024

kubelet-to-gcm/monitor/kubelet/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package kubelet
1919
import (
2020
"encoding/json"
2121
"fmt"
22+
"net"
2223
"net/http"
2324
"net/url"
2425

@@ -46,7 +47,7 @@ func NewClient(host string, port uint, client *http.Client, useAuthPort bool) (*
4647
if useAuthPort {
4748
protocol = "https"
4849
}
49-
urlStr := fmt.Sprintf("%s://%s:%d/stats/summary", protocol, host, port)
50+
urlStr := fmt.Sprintf("%s://%s/stats/summary", protocol, net.JoinHostPort(util.NormalizeHost(host), fmt.Sprint(port)))
5051
summaryURL, err := url.Parse(urlStr)
5152
if err != nil {
5253
return nil, err

kubelet-to-gcm/monitor/kubelet/client_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,71 @@ import (
2828
stats "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
2929
)
3030

31+
func TestNewClient(t *testing.T) {
32+
testCases := []struct {
33+
name string
34+
host string
35+
port uint
36+
useAuthPort bool
37+
expectedURL string
38+
}{
39+
{
40+
name: "IPv4 HTTP",
41+
host: "127.0.0.1",
42+
port: 10255,
43+
useAuthPort: false,
44+
expectedURL: "http://127.0.0.1:10255/stats/summary",
45+
},
46+
{
47+
name: "IPv4 HTTPS",
48+
host: "127.0.0.1",
49+
port: 10250,
50+
useAuthPort: true,
51+
expectedURL: "https://127.0.0.1:10250/stats/summary",
52+
},
53+
{
54+
name: "IPv6 HTTP",
55+
host: "2001:db8::1",
56+
port: 10255,
57+
useAuthPort: false,
58+
expectedURL: "http://[2001:db8::1]:10255/stats/summary",
59+
},
60+
{
61+
name: "IPv6 HTTPS",
62+
host: "2001:db8::1",
63+
port: 10250,
64+
useAuthPort: true,
65+
expectedURL: "https://[2001:db8::1]:10250/stats/summary",
66+
},
67+
{
68+
name: "Bracketed IPv6 HTTP",
69+
host: "[2001:db8::1]",
70+
port: 10255,
71+
useAuthPort: false,
72+
expectedURL: "http://[2001:db8::1]:10255/stats/summary",
73+
},
74+
{
75+
name: "Bracketed IPv6 HTTPS",
76+
host: "[2001:db8::1]",
77+
port: 10250,
78+
useAuthPort: true,
79+
expectedURL: "https://[2001:db8::1]:10250/stats/summary",
80+
},
81+
}
82+
83+
for _, tc := range testCases {
84+
t.Run(tc.name, func(t *testing.T) {
85+
c, err := NewClient(tc.host, tc.port, http.DefaultClient, tc.useAuthPort)
86+
if err != nil {
87+
t.Fatalf("NewClient failed: %v", err)
88+
}
89+
if c.summaryURL.String() != tc.expectedURL {
90+
t.Errorf("Expected URL %q, got %q", tc.expectedURL, c.summaryURL.String())
91+
}
92+
})
93+
}
94+
}
95+
3196
func TestDoRequestAndUnmarshal_SizeLimit(t *testing.T) {
3297
largeDataSize := maxResponseBodySize + 1024
3398
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

kubelet-to-gcm/monitor/util/util.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package util
1919
import (
2020
"errors"
2121
"io"
22+
"strings"
2223
)
2324

2425
var (
@@ -39,3 +40,14 @@ func ReadWithLimit(r io.Reader, limit int64) ([]byte, error) {
3940
}
4041
return data, nil
4142
}
43+
44+
// NormalizeHost handles host strings that may already be bracketed (like IPv6).
45+
// net.JoinHostPort expects a raw IP or hostname; if it receives a bracketed IPv6,
46+
// it will double-bracket it.
47+
func NormalizeHost(host string) string {
48+
host = strings.TrimSpace(host)
49+
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
50+
return host[1 : len(host)-1]
51+
}
52+
return host
53+
}

kubelet-to-gcm/monitor/util/util_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,45 @@ func TestReadWithLimit(t *testing.T) {
6565
})
6666
}
6767
}
68+
69+
func TestNormalizeHost(t *testing.T) {
70+
testCases := []struct {
71+
desc string
72+
input string
73+
expected string
74+
}{
75+
{
76+
desc: "IPv4",
77+
input: "127.0.0.1",
78+
expected: "127.0.0.1",
79+
},
80+
{
81+
desc: "hostname",
82+
input: "localhost",
83+
expected: "localhost",
84+
},
85+
{
86+
desc: "raw IPv6",
87+
input: "2001:db8::1",
88+
expected: "2001:db8::1",
89+
},
90+
{
91+
desc: "bracketed IPv6",
92+
input: "[2001:db8::1]",
93+
expected: "2001:db8::1",
94+
},
95+
{
96+
desc: "bracketed IPv6 with spaces",
97+
input: " [2001:db8::1] ",
98+
expected: "2001:db8::1",
99+
},
100+
}
101+
102+
for _, tc := range testCases {
103+
t.Run(tc.desc, func(t *testing.T) {
104+
if got := NormalizeHost(tc.input); got != tc.expected {
105+
t.Errorf("NormalizeHost(%q) = %q, want %q", tc.input, got, tc.expected)
106+
}
107+
})
108+
}
109+
}

0 commit comments

Comments
 (0)