Repository: hetznercloud/hcloud-go
Pull request #194 (2022-03-14 16:28:21)

Catch invalid token values and error out without value exposure
This PR handles tokens that have been previously for example encoded in base64 for Kubernetes in an erroneous way containing newline characters.

Currently the error returned return the token value. This may be an unexpected value exposure not intended by an operator.

Example:
`net/http: invalid header field value "Bearer broken\n" for key Authorization`

Applying this change changes the error to not contain any sensitive data anymore:
`Authorization token contains invalid characters`


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1ca9223..bb57606 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go-version: [1.13, 1.14, 1.15, 1.16, 1.17]
+        go-version: [1.13, 1.14, 1.15, 1.16, 1.17, 1.18]
     steps:
     - name: Set up Go ${{ matrix.go-version }}
       uses: actions/setup-go@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e8cfea5..c4bf88c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.17
+          go-version: 1.18
       - name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v2
         with:
diff --git a/go.mod b/go.mod
index 8bb6c55..33a3399 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,12 @@
 module github.com/hetznercloud/hcloud-go
 
-go 1.17
+go 1.18
 
 require (
 	github.com/google/go-cmp v0.5.5
 	github.com/prometheus/client_golang v1.11.0
 	github.com/stretchr/testify v1.7.0
+	golang.org/x/net v0.0.0-20200625001655-4c5254603344
 )
 
 require (
@@ -19,6 +20,7 @@ require (
 	github.com/prometheus/common v0.26.0 // indirect
 	github.com/prometheus/procfs v0.6.0 // indirect
 	golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
+	golang.org/x/text v0.3.2 // indirect
 	google.golang.org/protobuf v1.26.0-rc.1 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
diff --git a/go.sum b/go.sum
index cfc3bc2..7b8c252 100644
--- a/go.sum
+++ b/go.sum
@@ -102,6 +102,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -121,6 +122,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
diff --git a/hcloud/certificate.go b/hcloud/certificate.go
index 771a67e..04334de 100644
--- a/hcloud/certificate.go
+++ b/hcloud/certificate.go
@@ -138,6 +138,7 @@ func (c *CertificateClient) Get(ctx context.Context, idOrName string) (*Certific
 type CertificateListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l CertificateListOpts) values() url.Values {
@@ -145,6 +146,9 @@ func (l CertificateListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/client.go b/hcloud/client.go
index 07db0b7..ae2a7ea 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -16,9 +17,9 @@ import (
 	"time"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/internal/instrumentation"
-
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
 	"github.com/prometheus/client_golang/prometheus"
+	"golang.org/x/net/http/httpguts"
 )
 
 // Endpoint is the base URL of the API.
@@ -53,6 +54,7 @@ func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {
 type Client struct {
 	endpoint                string
 	token                   string
+	tokenValid              bool
 	pollInterval            time.Duration
 	backoffFunc             BackoffFunc
 	httpClient              *http.Client
@@ -96,6 +98,7 @@ func WithEndpoint(endpoint string) ClientOption {
 func WithToken(token string) ClientOption {
 	return func(client *Client) {
 		client.token = token
+		client.tokenValid = httpguts.ValidHeaderFieldValue(token)
 	}
 }
 
@@ -150,6 +153,7 @@ func WithInstrumentation(registry *prometheus.Registry) ClientOption {
 func NewClient(options ...ClientOption) *Client {
 	client := &Client{
 		endpoint:     Endpoint,
+		tokenValid:   true,
 		httpClient:   &http.Client{},
 		backoffFunc:  ExponentialBackoff(2, 500*time.Millisecond),
 		pollInterval: 500 * time.Millisecond,
@@ -196,9 +200,13 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re
 		return nil, err
 	}
 	req.Header.Set("User-Agent", c.userAgent)
-	if c.token != "" {
+
+	if !c.tokenValid {
+		return nil, errors.New("Authorization token contains invalid characters")
+	} else if c.token != "" {
 		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
 	}
+
 	if body != nil {
 		req.Header.Set("Content-Type", "application/json")
 	}
diff --git a/hcloud/datacenter.go b/hcloud/datacenter.go
index ae8b6c6..5a841f9 100644
--- a/hcloud/datacenter.go
+++ b/hcloud/datacenter.go
@@ -72,6 +72,7 @@ func (c *DatacenterClient) Get(ctx context.Context, idOrName string) (*Datacente
 type DatacenterListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l DatacenterListOpts) values() url.Values {
@@ -79,6 +80,9 @@ func (l DatacenterListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/firewall.go b/hcloud/firewall.go
index d254ca6..514f23a 100644
--- a/hcloud/firewall.go
+++ b/hcloud/firewall.go
@@ -136,6 +136,7 @@ func (c *FirewallClient) Get(ctx context.Context, idOrName string) (*Firewall, *
 type FirewallListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l FirewallListOpts) values() url.Values {
@@ -143,6 +144,9 @@ func (l FirewallListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index c6bced5..b180409 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -137,6 +137,7 @@ func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingI
 type FloatingIPListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l FloatingIPListOpts) values() url.Values {
@@ -144,6 +145,9 @@ func (l FloatingIPListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/hcloud.go b/hcloud/hcloud.go
index d2f0edf..f9665d2 100644
--- a/hcloud/hcloud.go
+++ b/hcloud/hcloud.go
@@ -2,4 +2,4 @@
 package hcloud
 
 // Version is the library's version following Semantic Versioning.
-const Version = "1.32.0"
+const Version = "1.34.0"
diff --git a/hcloud/iso.go b/hcloud/iso.go
index b3d97ba..3ec4a3d 100644
--- a/hcloud/iso.go
+++ b/hcloud/iso.go
@@ -82,6 +82,7 @@ func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response,
 type ISOListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l ISOListOpts) values() url.Values {
@@ -89,6 +90,9 @@ func (l ISOListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/labels.go b/hcloud/labels.go
new file mode 100644
index 0000000..a8c3b4f
--- /dev/null
+++ b/hcloud/labels.go
@@ -0,0 +1,23 @@
+package hcloud
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var keyRegexp = regexp.MustCompile(
+	`^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,62}[a-z0-9A-Z])?$`)
+var valueRegexp = regexp.MustCompile(`^([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,62})?[a-z0-9A-Z]$`)
+
+func ValidateResourceLabels(labels map[string]interface{}) (bool, error) {
+	for k, v := range labels {
+		if match := keyRegexp.MatchString(k); !match {
+			return false, fmt.Errorf("label key '%s' is not correctly formatted", k)
+		}
+
+		if match := valueRegexp.MatchString(v.(string)); !match {
+			return false, fmt.Errorf("label value '%s' (key: %s) is not correctly formatted", v, k)
+		}
+	}
+	return true, nil
+}
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index 2858c81..96aeaf4 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -283,6 +283,7 @@ func (c *LoadBalancerClient) Get(ctx context.Context, idOrName string) (*LoadBal
 type LoadBalancerListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l LoadBalancerListOpts) values() url.Values {
@@ -290,6 +291,9 @@ func (l LoadBalancerListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/load_balancer_type.go b/hcloud/load_balancer_type.go
index 7380004..5ac7481 100644
--- a/hcloud/load_balancer_type.go
+++ b/hcloud/load_balancer_type.go
@@ -69,6 +69,7 @@ func (c *LoadBalancerTypeClient) Get(ctx context.Context, idOrName string) (*Loa
 type LoadBalancerTypeListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l LoadBalancerTypeListOpts) values() url.Values {
@@ -76,6 +77,9 @@ func (l LoadBalancerTypeListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/location.go b/hcloud/location.go
index 05f4232..c749bdf 100644
--- a/hcloud/location.go
+++ b/hcloud/location.go
@@ -69,6 +69,7 @@ func (c *LocationClient) Get(ctx context.Context, idOrName string) (*Location, *
 type LocationListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l LocationListOpts) values() url.Values {
@@ -76,6 +77,9 @@ func (l LocationListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/metadata/client.go b/hcloud/metadata/client.go
index c56f1b9..a95fb10 100644
--- a/hcloud/metadata/client.go
+++ b/hcloud/metadata/client.go
@@ -1,6 +1,7 @@
 package metadata
 
 import (
+	"fmt"
 	"io/ioutil"
 	"net"
 	"net/http"
@@ -71,12 +72,16 @@ func (c *Client) get(path string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	body, err := ioutil.ReadAll(resp.Body)
+	defer resp.Body.Close()
+	bodyBytes, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		return "", err
 	}
-	resp.Body.Close()
-	return string(body), nil
+	body := string(bodyBytes)
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return body, fmt.Errorf("response status was %d", resp.StatusCode)
+	}
+	return body, nil
 }
 
 // IsHcloudServer checks if the currently called server is a hcloud server by calling a metadata endpoint
diff --git a/hcloud/network.go b/hcloud/network.go
index 2763e3e..e3bf6de 100644
--- a/hcloud/network.go
+++ b/hcloud/network.go
@@ -114,6 +114,7 @@ func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Re
 type NetworkListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l NetworkListOpts) values() url.Values {
@@ -121,6 +122,9 @@ func (l NetworkListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/placement_group.go b/hcloud/placement_group.go
index d8df195..5056a11 100644
--- a/hcloud/placement_group.go
+++ b/hcloud/placement_group.go
@@ -80,6 +80,7 @@ type PlacementGroupListOpts struct {
 	ListOpts
 	Name string
 	Type PlacementGroupType
+	Sort []string
 }
 
 func (l PlacementGroupListOpts) values() url.Values {
@@ -90,6 +91,9 @@ func (l PlacementGroupListOpts) values() url.Values {
 	if l.Type != "" {
 		vals.Add("type", string(l.Type))
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/schema.go b/hcloud/schema.go
index e7918cf..769f582 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -842,7 +842,9 @@ func loadBalancerCreateOptsToSchema(opts LoadBalancerCreateOpts) schema.LoadBala
 		req.Network = Int(opts.Network.ID)
 	}
 	for _, target := range opts.Targets {
-		schemaTarget := schema.LoadBalancerCreateRequestTarget{}
+		schemaTarget := schema.LoadBalancerCreateRequestTarget{
+			UsePrivateIP: target.UsePrivateIP,
+		}
 		switch target.Type {
 		case LoadBalancerTargetTypeServer:
 			schemaTarget.Type = string(LoadBalancerTargetTypeServer)
diff --git a/hcloud/server.go b/hcloud/server.go
index 4a0f2b1..281dd1f 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -228,6 +228,7 @@ type ServerListOpts struct {
 	ListOpts
 	Name   string
 	Status []ServerStatus
+	Sort   []string
 }
 
 func (l ServerListOpts) values() url.Values {
@@ -238,6 +239,9 @@ func (l ServerListOpts) values() url.Values {
 	for _, status := range l.Status {
 		vals.Add("status", string(status))
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/server_type.go b/hcloud/server_type.go
index a681d09..cf712eb 100644
--- a/hcloud/server_type.go
+++ b/hcloud/server_type.go
@@ -92,6 +92,7 @@ func (c *ServerTypeClient) Get(ctx context.Context, idOrName string) (*ServerTyp
 type ServerTypeListOpts struct {
 	ListOpts
 	Name string
+	Sort []string
 }
 
 func (l ServerTypeListOpts) values() url.Values {
@@ -99,6 +100,9 @@ func (l ServerTypeListOpts) values() url.Values {
 	if l.Name != "" {
 		vals.Add("name", l.Name)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index 52195e7..f450c8b 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -81,6 +81,7 @@ type SSHKeyListOpts struct {
 	ListOpts
 	Name        string
 	Fingerprint string
+	Sort        []string
 }
 
 func (l SSHKeyListOpts) values() url.Values {
@@ -91,6 +92,9 @@ func (l SSHKeyListOpts) values() url.Values {
 	if l.Fingerprint != "" {
 		vals.Add("fingerprint", l.Fingerprint)
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 
diff --git a/hcloud/volume.go b/hcloud/volume.go
index 5355936..93e221e 100644
--- a/hcloud/volume.go
+++ b/hcloud/volume.go
@@ -92,6 +92,7 @@ type VolumeListOpts struct {
 	ListOpts
 	Name   string
 	Status []VolumeStatus
+	Sort   []string
 }
 
 func (l VolumeListOpts) values() url.Values {
@@ -102,6 +103,9 @@ func (l VolumeListOpts) values() url.Values {
 	for _, status := range l.Status {
 		vals.Add("status", string(status))
 	}
+	for _, sort := range l.Sort {
+		vals.Add("sort", sort)
+	}
 	return vals
 }
 


Test patch:
diff --git a/hcloud/client_test.go b/hcloud/client_test.go
index f27f403..5c025fc 100644
--- a/hcloud/client_test.go
+++ b/hcloud/client_test.go
@@ -86,6 +86,25 @@ func TestClientError(t *testing.T) {
 	}
 }
 
+func TestClientInvalidToken(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Client = NewClient(
+		WithEndpoint(env.Server.URL),
+		WithToken("invalid token\n"),
+	)
+
+	ctx := context.Background()
+	_, err := env.Client.NewRequest(ctx, "GET", "/", nil)
+
+	if nil == err {
+		t.Error("Failed to trigger expected error")
+	} else if err.Error() != "Authorization token contains invalid characters" {
+		t.Fatalf("Invalid encoded authorization token triggered unexpected error message: %s", err)
+	}
+}
+
 func TestClientMeta(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
diff --git a/hcloud/labels_test.go b/hcloud/labels_test.go
new file mode 100644
index 0000000..394520e
--- /dev/null
+++ b/hcloud/labels_test.go
@@ -0,0 +1,69 @@
+package hcloud
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCheckLabels(t *testing.T) {
+	t.Run("correct labels", func(t *testing.T) {
+		labelMap := map[string]interface{}{
+			"label1":                    "correct.de",
+			"label2.de/hallo":           "1correct2.de",
+			"label3-test.de/hallo.welt": "233344444443",
+			"d/d":                       "d",
+		}
+
+		ok, err := ValidateResourceLabels(labelMap)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.True(t, ok)
+	})
+	t.Run("incorrect string label values", func(t *testing.T) {
+		incorrectLabels := []string{
+			"incorrect .com",
+			"-incorrect.com",
+			"incorrect.com-",
+			"incorr,ect.com-",
+			"incorrect-111111111111111111111111111111111111111111111111111111111111.com",
+		}
+
+		for _, label := range incorrectLabels {
+			labelMap := map[string]interface{}{
+				"test1": "valid",
+				"test2": label,
+			}
+			ok, err := ValidateResourceLabels(labelMap)
+			assert.Error(t, err)
+			assert.False(t, ok)
+		}
+	})
+	t.Run("incorrect string label keys", func(t *testing.T) {
+		incorrectLabels := []string{
+			"incorrect.de/",
+			"incor rect.de/",
+			"incorrect.de/+",
+			"-incorrect.de",
+			"incorrect.de-",
+			"incorrect.de/tes t",
+			"incorrect.de/test-",
+			"incorrect.de/test-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
+			"incorrect-11111111111111111111111111111111111111111111111111111111111111111111111111111111" +
+				"11111111111111111111111111111111111111111111111111111111111111111111111111111111" +
+				"11111111111111111111111111111111111111111111111111111111111111111111111111111111" +
+				"11111111111111111111111111111111111111111111111111111111111111111111111111111111" +
+				".de/test",
+		}
+
+		for _, label := range incorrectLabels {
+			labelMap := map[string]interface{}{
+				label: "cor-rect.de",
+			}
+			ok, err := ValidateResourceLabels(labelMap)
+			assert.Error(t, err)
+			assert.False(t, ok)
+		}
+	})
+}
diff --git a/hcloud/metadata/client_test.go b/hcloud/metadata/client_test.go
index 001e76e..ba1e59e 100644
--- a/hcloud/metadata/client_test.go
+++ b/hcloud/metadata/client_test.go
@@ -34,6 +34,24 @@ func newTestEnv() testEnv {
 	}
 }
 
+func TestClient_Base(t *testing.T) {
+	env := newTestEnv()
+	env.Mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("ok"))
+	})
+	env.Mux.HandleFunc("/not-found", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(404)
+		w.Write([]byte("not found"))
+	})
+
+	if body, err := env.Client.get("/ok"); assert.NoError(t, err) {
+		assert.Equal(t, "ok", body)
+	}
+	if body, err := env.Client.get("/not-found"); assert.EqualError(t, err, "response status was 404") {
+		assert.Equal(t, "not found", body)
+	}
+}
+
 func TestClient_IsHcloudServer(t *testing.T) {
 	env := newTestEnv()
 	env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) {