Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions third_party/event-subscriber/events/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"

"github.com/gorilla/websocket"
Expand Down Expand Up @@ -68,8 +68,10 @@ func NewEventRouter(name string, priority int, apiURL string, accessKey string,
}
}

// TODO Get subscribe collection URL from API instead of hard coding
subscribeURL := strings.Replace(apiURL+"/subscribe", "http", "ws", -1)
subscribeURL, err := buildEventSubscriptionURL(apiURL)
if err != nil {
return nil, err
}

return &EventRouter{
name: name,
Expand Down Expand Up @@ -183,23 +185,28 @@ func (router *EventRouter) subscribeToEvents(subscribeURL string, accessKey stri
dialer := &websocket.Dialer{}
headers := http.Header{}
headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(accessKey+":"+secretKey)))
subscribeURL = subscribeURL + "?" + data.Encode()
ws, resp, err := dialer.Dial(subscribeURL, headers)
target, err := validateEventSubscriptionURL(router.apiURL, subscribeURL, data)
if err != nil {
return nil, err
}
if !eventSubscriptionURLPattern.MatchString(target) {
return nil, fmt.Errorf("event subscription URL failed validation")
}
ws, resp, err := dialer.Dial(target, headers)

if err != nil {
endpoint, _ := url.Parse(target)
log.WithFields(log.Fields{
"subscribeUrl": subscribeURL,
"subscribeEndpoint": endpoint.Scheme + "://" + endpoint.Host + endpoint.Path,
}).Errorf("Error subscribing to events: %s", err)
if resp != nil {
log.WithFields(log.Fields{
"status": resp.Status,
"statusCode": resp.StatusCode,
"responseHeaders": resp.Header,
"status": resp.Status,
"statusCode": resp.StatusCode,
}).Error("Got error response")
if resp.Body != nil {
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
log.Errorf("Error response: %s", body)
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64<<10))
}
}
return nil, err
Expand Down
64 changes: 64 additions & 0 deletions third_party/event-subscriber/events/security_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package events

import (
"fmt"
"net/url"
"regexp"
"strings"
)

var eventSubscriptionURLPattern = regexp.MustCompile(`^wss?://(?:\[[0-9A-Fa-f:.%]+\]|[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?(?:\?[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*)?$`)

func buildEventSubscriptionURL(apiURL string) (string, error) {
base, err := url.Parse(apiURL)
if err != nil || base.Opaque != "" || base.User != nil || base.Hostname() == "" {
return "", fmt.Errorf("event API URL must contain a valid origin")
}
switch strings.ToLower(base.Scheme) {
case "http":
base.Scheme = "ws"
case "https":
base.Scheme = "wss"
default:
return "", fmt.Errorf("unsupported event API URL scheme %q", base.Scheme)
}
base.Path = strings.TrimRight(base.Path, "/") + "/subscribe"
base.RawPath = ""
base.RawQuery = ""
base.Fragment = ""
return validateEventSubscriptionURL(apiURL, base.String(), nil)
}

func validateEventSubscriptionURL(apiURL, candidateURL string, data url.Values) (string, error) {
if !eventSubscriptionURLPattern.MatchString(candidateURL) {
return "", fmt.Errorf("event subscription URL has an unsupported format")
}
base, err := url.Parse(apiURL)
if err != nil || base.Opaque != "" || base.User != nil || base.Hostname() == "" {
return "", fmt.Errorf("event API URL must contain a valid origin")
}
target, err := url.Parse(candidateURL)
if err != nil || target.Opaque != "" || target.User != nil || target.Hostname() == "" {
return "", fmt.Errorf("event subscription URL must contain a valid origin")
}
expectedScheme := "ws"
if strings.EqualFold(base.Scheme, "https") {
expectedScheme = "wss"
} else if !strings.EqualFold(base.Scheme, "http") {
return "", fmt.Errorf("unsupported event API URL scheme %q", base.Scheme)
}
if !strings.EqualFold(target.Scheme, expectedScheme) || !strings.EqualFold(target.Host, base.Host) {
return "", fmt.Errorf("event subscription URL crosses the configured origin")
}
query := target.Query()
for key, values := range data {
for _, value := range values {
query.Add(key, value)
}
}
target.RawQuery = query.Encode()
if !eventSubscriptionURLPattern.MatchString(target.String()) {
return "", fmt.Errorf("event subscription URL failed validation")
}
return target.String(), nil
}
28 changes: 28 additions & 0 deletions third_party/event-subscriber/events/security_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package events

import (
"net/url"
"testing"
)

func TestEventSubscriptionURLBindsConfiguredOrigin(t *testing.T) {
base, err := buildEventSubscriptionURL("https://api.example.test/v2-beta")
if err != nil {
t.Fatal(err)
}
query := url.Values{"eventNames": {"ping", "host.change"}}
target, err := validateEventSubscriptionURL("https://api.example.test/v2-beta", base, query)
if err != nil {
t.Fatal(err)
}
if !eventSubscriptionURLPattern.MatchString(target) {
t.Fatalf("validated target did not match the request policy: %s", target)
}
if _, err := validateEventSubscriptionURL(
"https://api.example.test/v2-beta",
"wss://metadata.example.test/v2-beta/subscribe",
nil,
); err == nil {
t.Fatal("cross-origin event subscription URL was accepted")
}
}
34 changes: 20 additions & 14 deletions third_party/go-rancher/v2/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func setupRancherBaseClient(rancherClient *RancherBaseClientImpl, opts *ClientOp
opts.Timeout = time.Second * 10
}
client := &http.Client{Timeout: opts.Timeout}
req, err := http.NewRequest("GET", opts.Url, nil)
req, err := newRancherRequest(opts.Url, "GET", opts.Url, nil)
if err != nil {
return err
}
Expand All @@ -172,11 +172,11 @@ func setupRancherBaseClient(rancherClient *RancherBaseClientImpl, opts *ClientOp
}

if schemasUrls != opts.Url {
req, err = http.NewRequest("GET", schemasUrls, nil)
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
req, err = newRancherRequest(opts.Url, "GET", schemasUrls, nil)
if err != nil {
return err
}
req.SetBasicAuth(opts.AccessKey, opts.SecretKey)

resp, err = client.Do(req)
if err != nil {
Expand Down Expand Up @@ -230,7 +230,7 @@ func (rancherClient *RancherBaseClientImpl) newHttpClient() *http.Client {

func (rancherClient *RancherBaseClientImpl) doDelete(url string) error {
client := rancherClient.newHttpClient()
req, err := http.NewRequest("DELETE", url, nil)
req, err := newRancherRequest(rancherClient.Opts.Url, "DELETE", url, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -263,7 +263,14 @@ func (rancherClient *RancherBaseClientImpl) Websocket(url string, headers map[st
httpHeaders.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
}

return dialer.Dial(url, http.Header(httpHeaders))
target, err := validateRancherRequestURL(rancherClient.Opts.Url, url, true)
if err != nil {
return nil, nil, err
}
if !rancherWebSocketURLPattern.MatchString(target) {
return nil, nil, fmt.Errorf("Rancher WebSocket URL failed validation")
}
return dialer.Dial(target, http.Header(httpHeaders))
}

func (rancherClient *RancherBaseClientImpl) doGet(url string, opts *ListOpts, respObject interface{}) error {
Expand All @@ -276,11 +283,11 @@ func (rancherClient *RancherBaseClientImpl) doGet(url string, opts *ListOpts, re
}

if debug {
fmt.Println("GET " + url)
fmt.Println("GET " + safeRancherURLForLog(url))
}

client := rancherClient.newHttpClient()
req, err := http.NewRequest("GET", url, nil)
req, err := newRancherRequest(rancherClient.Opts.Url, "GET", url, nil)
if err != nil {
return err
}
Expand All @@ -304,7 +311,7 @@ func (rancherClient *RancherBaseClientImpl) doGet(url string, opts *ListOpts, re
}

if debug {
fmt.Println("Response <= " + string(byteContent))
fmt.Printf("Response <= %d bytes\n", len(byteContent))
}

if err := json.Unmarshal(byteContent, respObject); err != nil {
Expand Down Expand Up @@ -360,12 +367,11 @@ func (rancherClient *RancherBaseClientImpl) doModify(method string, url string,
}

if debug {
fmt.Println(method + " " + url)
fmt.Println("Request => " + string(bodyContent))
fmt.Printf("%s %s request=%d bytes\n", method, safeRancherURLForLog(url), len(bodyContent))
}

client := rancherClient.newHttpClient()
req, err := http.NewRequest(method, url, bytes.NewBuffer(bodyContent))
req, err := newRancherRequest(rancherClient.Opts.Url, method, url, bytes.NewBuffer(bodyContent))
if err != nil {
return err
}
Expand All @@ -391,7 +397,7 @@ func (rancherClient *RancherBaseClientImpl) doModify(method string, url string,

if len(byteContent) > 0 {
if debug {
fmt.Println("Response <= " + string(byteContent))
fmt.Printf("Response <= %d bytes\n", len(byteContent))
}
return json.Unmarshal(byteContent, respObject)
}
Expand Down Expand Up @@ -559,7 +565,7 @@ func (rancherClient *RancherBaseClientImpl) doAction(schemaType string, action s
}

client := rancherClient.newHttpClient()
req, err := http.NewRequest("POST", actionUrl, input)
req, err := newRancherRequest(rancherClient.Opts.Url, "POST", actionUrl, input)
if err != nil {
return err
}
Expand All @@ -585,7 +591,7 @@ func (rancherClient *RancherBaseClientImpl) doAction(schemaType string, action s
}

if debug {
fmt.Println("Response <= " + string(byteContent))
fmt.Printf("Response <= %d bytes\n", len(byteContent))
}

return json.Unmarshal(byteContent, respObject)
Expand Down
85 changes: 85 additions & 0 deletions third_party/go-rancher/v2/security_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package client

import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
)

var (
rancherHTTPURLPattern = regexp.MustCompile(`^https?://(?:\[[0-9A-Fa-f:.%]+\]|[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?(?:\?[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*)?$`)
rancherWebSocketURLPattern = regexp.MustCompile(`^wss?://(?:\[[0-9A-Fa-f:.%]+\]|[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?(?:\?[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*)?$`)
)

func rancherOrigin(raw string) (string, string, string, error) {
u, err := url.Parse(raw)
if err != nil || u.Opaque != "" || u.User != nil || u.Hostname() == "" {
return "", "", "", fmt.Errorf("Rancher URL must contain a valid origin")
}
scheme := strings.ToLower(u.Scheme)
switch scheme {
case "ws":
scheme = "http"
case "wss":
scheme = "https"
case "http", "https":
default:
return "", "", "", fmt.Errorf("Rancher URL has an unsupported scheme")
}
port := u.Port()
if port == "" {
if scheme == "https" {
port = "443"
} else {
port = "80"
}
}
return scheme, strings.ToLower(u.Hostname()), port, nil
}

func validateRancherRequestURL(baseURL, candidateURL string, websocketRequest bool) (string, error) {
pattern := rancherHTTPURLPattern
if websocketRequest {
pattern = rancherWebSocketURLPattern
}
if !pattern.MatchString(candidateURL) {
return "", fmt.Errorf("Rancher request URL has an unsupported format")
}
baseScheme, baseHost, basePort, err := rancherOrigin(baseURL)
if err != nil {
return "", err
}
targetScheme, targetHost, targetPort, err := rancherOrigin(candidateURL)
if err != nil {
return "", err
}
if baseScheme != targetScheme || baseHost != targetHost || basePort != targetPort {
return "", fmt.Errorf("Rancher request URL crosses the configured origin")
}
return candidateURL, nil
}

func newRancherRequest(baseURL, method, candidateURL string, body io.Reader) (*http.Request, error) {
target, err := validateRancherRequestURL(baseURL, candidateURL, false)
if err != nil {
return nil, err
}
if !rancherHTTPURLPattern.MatchString(target) {
return nil, fmt.Errorf("Rancher request URL failed validation")
}
return http.NewRequest(method, target, body)
}

func safeRancherURLForLog(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return "[invalid URL]"
}
u.User = nil
u.RawQuery = ""
u.Fragment = ""
return u.String()
}
32 changes: 32 additions & 0 deletions third_party/go-rancher/v2/security_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package client

import (
"strings"
"testing"
)

func TestValidateRancherRequestURLBindsConfiguredOrigin(t *testing.T) {
base := "https://api.example.test:8443/v2-beta"
accepted, err := validateRancherRequestURL(base, "https://api.example.test:8443/v2-beta/projects?limit=10", false)
if err != nil || accepted == "" {
t.Fatalf("same-origin URL rejected: %q, %v", accepted, err)
}
for _, candidate := range []string{
"https://metadata.example.test/latest",
"https://user:secret@api.example.test:8443/v2-beta",
"file:///etc/passwd",
} {
if _, err := validateRancherRequestURL(base, candidate, false); err == nil {
t.Fatalf("unsafe URL accepted: %s", candidate)
}
}
}

func TestSafeRancherURLForLogRemovesSecrets(t *testing.T) {
value := safeRancherURLForLog("https://user:password@example.test/path?token=secret#fragment")
for _, secret := range []string{"user", "password", "token", "secret", "fragment"} {
if strings.Contains(value, secret) {
t.Fatalf("safe URL still contains %q: %s", secret, value)
}
}
}
Loading