mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 993a9bc4b9 | |||
| 8d1ea7202a | |||
| 9adbab96af | |||
| bdd70e798a | |||
| 5feba7e3a9 | |||
| d3d9460f64 | |||
| 74f3a55c57 | |||
| 3c93d9b300 | |||
| 3c0ea0db65 | |||
| 7c03e2cbd2 | |||
| 8f25704a90 | |||
| 583bad4972 | |||
| b5f340d64c | |||
| 9cd0b7048a | |||
| 4858ce79d0 | |||
| 4090049fff | |||
| e255a7da26 | |||
| c2a3ac3991 | |||
| 2fa09e1cc6 | |||
| 0a742feb98 | |||
| 80a15547e3 | |||
| d26a8c5d44 | |||
| ca619a97bc | |||
| 25a04e0c69 | |||
| d3f9aa2ae7 | |||
| d32fb8e82c | |||
| acd17f6ab6 | |||
| 1ca841d220 | |||
| 39d60d1239 | |||
| 713a2d689e | |||
| babcd9fe2b | |||
| a1403fe968 | |||
| 1485ca0fc7 |
+12
@@ -0,0 +1,12 @@
|
||||
FROM golang:1.12 as builder
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends upx
|
||||
# Run after `apt-get update` to improve rebuild scenarios
|
||||
COPY . .
|
||||
RUN make cloudflared
|
||||
RUN upx --no-progress cloudflared
|
||||
|
||||
FROM gcr.io/distroless/base
|
||||
COPY --from=builder /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
CMD ["version"]
|
||||
Generated
+24
-30
@@ -73,15 +73,9 @@
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df"
|
||||
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
packages = [
|
||||
"http",
|
||||
"jose",
|
||||
"key",
|
||||
"oauth2",
|
||||
"oidc",
|
||||
]
|
||||
packages = ["jose"]
|
||||
pruneopts = "UT"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
@@ -93,18 +87,6 @@
|
||||
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
||||
version = "v19"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5"
|
||||
name = "github.com/coreos/pkg"
|
||||
packages = [
|
||||
"health",
|
||||
"httputil",
|
||||
"timeutil",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1"
|
||||
version = "v4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
@@ -213,12 +195,12 @@
|
||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538"
|
||||
name = "github.com/jonboulle/clockwork"
|
||||
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
|
||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
|
||||
version = "v0.1.0"
|
||||
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b"
|
||||
@@ -283,6 +265,14 @@
|
||||
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe"
|
||||
version = "v1.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11e62d6050198055e6cd87ed57e5d8c669e84f839c16e16f192374d913d1a70d"
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
@@ -359,17 +349,20 @@
|
||||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5f2aaa360f48d1711795bd88c7e45a38f86cf81e4bc01453d20983baa67e2d51"
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c"
|
||||
digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
packages = [
|
||||
"assert",
|
||||
"require",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
@@ -569,7 +562,6 @@
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode",
|
||||
"github.com/coredns/coredns/request",
|
||||
"github.com/coreos/go-oidc/jose",
|
||||
"github.com/coreos/go-oidc/oidc",
|
||||
"github.com/coreos/go-systemd/daemon",
|
||||
"github.com/elgs/gosqljson",
|
||||
"github.com/equinox-io/equinox",
|
||||
@@ -583,12 +575,14 @@
|
||||
"github.com/mattn/go-colorable",
|
||||
"github.com/miekg/dns",
|
||||
"github.com/mitchellh/go-homedir",
|
||||
"github.com/mitchellh/mapstructure",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rifflock/lfshook",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"golang.org/x/crypto/nacl/box",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
|
||||
+5
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "=1.0.3"
|
||||
version = "=1.4.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
@@ -85,3 +85,7 @@
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "=1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
version = "1.1.2"
|
||||
|
||||
@@ -31,6 +31,10 @@ clean:
|
||||
cloudflared: tunnel-deps
|
||||
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
docker build -t cloudflare/cloudflared:"$(VERSION)" .
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
go test -v -race $(VERSION_FLAGS) ./...
|
||||
@@ -70,7 +74,7 @@ tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
capnp compile -ogo -I ./tunnelrpc tunnelrpc/tunnelrpc.capnp
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
2019.8.0
|
||||
- 2019-07-11 TUN-1956: Go 1.12 update
|
||||
- 2019-07-24 TUN-1736: Missing headers when passing an invalid path
|
||||
- 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge
|
||||
- 2019-08-02 TUN-2125: Add PostgresType() to Scope
|
||||
- 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler
|
||||
- 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig
|
||||
- 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
|
||||
|
||||
2019.7.0
|
||||
- 2019-05-28 TUN-1913: Define OriginService for each type of origin
|
||||
- 2019-04-29 Build a docker container
|
||||
- 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied
|
||||
- 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname
|
||||
- 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
|
||||
- 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig
|
||||
- 2019-06-19 TUN-1976: Pass tunnel hostname through header
|
||||
- 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified
|
||||
- 2019-06-26 TUN-2005: Upgrade logrus
|
||||
- 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category
|
||||
- 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix
|
||||
- 2019-06-13 DoH: change the media type to application/dns-message
|
||||
- 2019-06-26 AUTH-1736: Better handling of token revocation
|
||||
|
||||
2019.6.0
|
||||
- 2019-05-17 TUN-1828: Update declarative tunnel config struct
|
||||
- 2019-05-29 Handle exit code on err
|
||||
- 2019-05-29 AUTH-1802: Fixed ssh-config templating
|
||||
- 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig
|
||||
- 2019-06-03 AUTH-1811: ssh-gen config fixes
|
||||
|
||||
2019.5.0
|
||||
- 2019-04-25 TUN-1781: ServeStream should return early on error
|
||||
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
|
||||
|
||||
+85
-23
@@ -4,7 +4,6 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -12,7 +11,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
cloudflaredWebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -37,6 +37,13 @@ func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
return os.Stdout.Write(p)
|
||||
}
|
||||
|
||||
// Helper to allow defering the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
func StartClient(logger *logrus.Logger, stream io.ReadWriter, options *StartOptions) error {
|
||||
@@ -90,7 +97,7 @@ func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOption
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
websocket.Stream(wsConn, conn)
|
||||
cloudflaredWebsocket.Stream(wsConn, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -98,28 +105,17 @@ func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOption
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
wsConn, resp, err := websocket.ClientConnect(req, nil)
|
||||
if err != nil && resp != nil && resp.StatusCode > 300 {
|
||||
location, err := resp.Location()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.Contains(location.String(), "cdn-cgi/access/login") {
|
||||
return nil, errors.New("not an Access redirect")
|
||||
}
|
||||
req, err := buildAccessRequest(options.OriginURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wsConn, _, err = websocket.ClientConnect(req, nil)
|
||||
wsConn, resp, err := cloudflaredWebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil && isAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,12 +123,72 @@ func createWebsocketStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &websocket.Conn{Conn: wsConn}, nil
|
||||
return &cloudflaredWebsocket.Conn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
// isAccessResponse checks the http Response to see if the url location
|
||||
// contains the Access structure.
|
||||
func isAccessResponse(resp *http.Response) bool {
|
||||
if resp == nil || resp.StatusCode <= 300 {
|
||||
return false
|
||||
}
|
||||
|
||||
location, err := resp.Location()
|
||||
if err != nil || location == nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
// a connection with the token set on the request. If it still get redirect,
|
||||
// this probably means the token in storage is invalid (expired/revoked). If that
|
||||
// happens it deletes the token and runs the connection again, so the user can
|
||||
// login again and generate a new one.
|
||||
func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
wsConn, resp, err := createAccessWebSocketStream(options)
|
||||
defer closeRespBody(resp)
|
||||
if err == nil {
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
if !isAccessResponse(resp) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Access Token is invalid for some reason. Go through regen flow
|
||||
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsConn, resp, err = createAccessWebSocketStream(options)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := buildAccessRequest(options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return cloudflaredWebsocket.ClientConnect(req, nil)
|
||||
}
|
||||
|
||||
// buildAccessRequest builds an HTTP request with the Access token set
|
||||
func buildAccessRequest(originURL string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, originURL, nil)
|
||||
func buildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,11 +200,17 @@ func buildAccessRequest(originURL string) (*http.Request, error) {
|
||||
|
||||
// We need to create a new request as FetchToken will modify req (boo mutable)
|
||||
// as it has to follow redirect on the API and such, so here we init a new one
|
||||
originRequest, err := http.NewRequest(http.MethodGet, originURL, nil)
|
||||
originRequest, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set("cf-access-token", token)
|
||||
|
||||
for k, v := range options.Headers {
|
||||
if len(v) >= 1 {
|
||||
originRequest.Header.Set(k, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
return originRequest, nil
|
||||
}
|
||||
|
||||
@@ -91,6 +91,31 @@ func TestStartServer(t *testing.T) {
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
func TestIsAccessResponse(t *testing.T) {
|
||||
validLocationHeader := http.Header{}
|
||||
validLocationHeader.Add("location", "https://test.cloudflareaccess.com/cdn-cgi/access/login/blahblah")
|
||||
invalidLocationHeader := http.Header{}
|
||||
invalidLocationHeader.Add("location", "https://google.com")
|
||||
testCases := []struct {
|
||||
Description string
|
||||
In *http.Response
|
||||
ExpectedOut bool
|
||||
}{
|
||||
{"nil response", nil, false},
|
||||
{"redirect with no location", &http.Response{StatusCode: http.StatusPermanentRedirect}, false},
|
||||
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
|
||||
{"redirect with location", &http.Response{StatusCode: http.StatusPermanentRedirect, Header: validLocationHeader}, true},
|
||||
{"redirect with invalid location", &http.Response{StatusCode: http.StatusPermanentRedirect, Header: invalidLocationHeader}, false},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
if isAccessResponse(tc.In) != tc.ExpectedOut {
|
||||
t.Fatalf("Failed case %d -- %s", i, tc.Description)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func newTestWebSocketServer() *httptest.Server {
|
||||
upgrader := ws.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
pinned_go: &pinned_go go=1.11.5-1
|
||||
pinned_go: &pinned_go go=1.12.7-1
|
||||
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
|
||||
@@ -3,10 +3,10 @@ package access
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
@@ -27,19 +27,19 @@ const (
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConfigTemplate = `
|
||||
Add this configuration block to your {{.Home}}/.ssh/config:
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
{{- if .ShortLivedCerts}}
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt cfpipe >&2 <&1'
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{.Hostname}}.me-cf_key
|
||||
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
|
||||
HostName {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{.Hostname}}-cf_key
|
||||
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
|
||||
{{- else}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
{{end}}
|
||||
`
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(logger *logrus.Logger) {
|
||||
logger.Infof("Version %s", bi.CloudflaredVersion)
|
||||
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
@@ -127,10 +127,14 @@ func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionF
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
}
|
||||
return err
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+123
-12
@@ -1,15 +1,20 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,6 +23,95 @@ const (
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()){
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
for range s.sigChannel {
|
||||
handler()
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
func (s *signalHandler) deregister() {
|
||||
signal.Stop(s.sigChannel)
|
||||
close(s.sigChannel)
|
||||
}
|
||||
|
||||
func errDeleteTokenFailed(lockFilePath string) error {
|
||||
return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
|
||||
}
|
||||
|
||||
// newLock will get a new file lock
|
||||
func newLock(path string) *lock {
|
||||
lockPath := path + ".lock"
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a path.lock file
|
||||
// if the lock file exists; start polling
|
||||
// if not, create the lock file and go through the normal flow.
|
||||
// See AUTH-1736 for the reason why we do all this
|
||||
for isTokenLocked(l.lockFilePath) {
|
||||
if l.backoff.Backoff(context.Background()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := l.deleteLockFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a lock file so other processes won't also try to get the token at
|
||||
// the same time
|
||||
if err := ioutil.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) deleteLockFile() error {
|
||||
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
||||
return errDeleteTokenFailed(l.lockFilePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *lock) Release() error {
|
||||
defer l.sigHandler.deregister()
|
||||
return l.deleteLockFile()
|
||||
}
|
||||
|
||||
// isTokenLocked checks to see if there is another process attempting to get the token already
|
||||
func isTokenLocked(lockFilePath string) bool {
|
||||
exists, err := config.FileExists(lockFilePath)
|
||||
return exists && err == nil
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
func FetchToken(appURL *url.URL) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
@@ -29,6 +123,19 @@ func FetchToken(appURL *url.URL) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileLock := newLock(path)
|
||||
|
||||
err = fileLock.Acquire()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fileLock.Release()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
@@ -55,14 +162,18 @@ func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := token.Claims()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ident, err := oidc.IdentityFromClaims(claims)
|
||||
// AUTH-1404, reauth if the token is about to expire within 15 minutes
|
||||
if err == nil && ident.ExpiresAt.After(time.Now().Add(time.Minute*15)) {
|
||||
return token.Encode(), nil
|
||||
}
|
||||
return "", err
|
||||
return token.Encode(), nil
|
||||
}
|
||||
|
||||
// RemoveTokenIfExists removes the a token from local storage if it exists
|
||||
func RemoveTokenIfExists(url *url.URL) error {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignalHandler(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
handlerRan := false
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func(){
|
||||
handlerRan = true
|
||||
done <- struct{}{}
|
||||
})
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
|
||||
// Blocks for up to one second to make sure the handler callback runs before the assert.
|
||||
select {
|
||||
case <- done:
|
||||
assert.True(t, handlerRan)
|
||||
case <- timer.C:
|
||||
t.Fail()
|
||||
}
|
||||
sigHandler.deregister()
|
||||
}
|
||||
|
||||
func TestSignalHandlerClose(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func(){done <- struct{}{}})
|
||||
sigHandler.deregister()
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
select {
|
||||
case <- done:
|
||||
t.Fail()
|
||||
case <- timer.C:
|
||||
}
|
||||
}
|
||||
+197
-18
@@ -1,6 +1,7 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -11,9 +12,16 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
|
||||
@@ -21,6 +29,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
@@ -234,9 +243,8 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
return err
|
||||
}
|
||||
|
||||
buildInfo := origin.GetBuildInfo()
|
||||
logger.Infof("Build info: %+v", *buildInfo)
|
||||
logger.Infof("Version %s", version)
|
||||
buildInfo := buildinfo.GetBuildInfo(version)
|
||||
buildInfo.Log(logger)
|
||||
logClientOptions(c)
|
||||
|
||||
if c.IsSet("proxy-dns") {
|
||||
@@ -252,16 +260,6 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
// Wait for proxy-dns to come up (if used)
|
||||
<-dnsReadySignal
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
if updater.IsAutoupdateEnabled(c) {
|
||||
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- updater.Autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC)
|
||||
}()
|
||||
}
|
||||
|
||||
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error opening metrics server listener")
|
||||
@@ -279,6 +277,33 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
go writePidFile(connectedSignal, c.String("pidfile"))
|
||||
}
|
||||
|
||||
cloudflaredID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot generate cloudflared ID")
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-shutdownC
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if c.IsSet("use-declarative-tunnels") {
|
||||
return startDeclarativeTunnel(ctx, c, cloudflaredID, buildInfo, &listeners)
|
||||
}
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
if updater.IsAutoupdateEnabled(c) {
|
||||
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners)
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
if dnsProxyStandAlone(c) {
|
||||
connectedSignal.Notify()
|
||||
@@ -287,6 +312,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
}
|
||||
|
||||
if c.IsSet("hello-world") {
|
||||
logger.Infof("hello-world set")
|
||||
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Hello World Server")
|
||||
@@ -323,7 +349,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
||||
}()
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
@@ -348,6 +374,128 @@ func Before(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startDeclarativeTunnel(ctx context.Context,
|
||||
c *cli.Context,
|
||||
cloudflaredID uuid.UUID,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
listeners *gracenet.Net,
|
||||
) error {
|
||||
reverseProxyOrigin, err := defaultOriginConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err)
|
||||
return err
|
||||
}
|
||||
reverseProxyConfig, err := pogs.NewReverseProxyConfig(
|
||||
c.String("hostname"),
|
||||
reverseProxyOrigin,
|
||||
c.Uint64("retries"),
|
||||
c.Duration("proxy-connection-timeout"),
|
||||
c.Uint64("compression-quality"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot initialize default client config because reverse proxy config is invalid")
|
||||
return err
|
||||
}
|
||||
defaultClientConfig := &pogs.ClientConfig{
|
||||
Version: pogs.InitVersion(),
|
||||
SupervisorConfig: &pogs.SupervisorConfig{
|
||||
AutoUpdateFrequency: c.Duration("autoupdate-freq"),
|
||||
MetricsUpdateFrequency: c.Duration("metrics-update-freq"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
},
|
||||
EdgeConnectionConfig: &pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: uint8(c.Int("ha-connections")),
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Timeout: c.Duration("dial-edge-timeout"),
|
||||
MaxFailedHeartbeats: c.Uint64("heartbeat-count"),
|
||||
},
|
||||
DoHProxyConfigs: []*pogs.DoHProxyConfig{},
|
||||
ReverseProxyConfigs: []*pogs.ReverseProxyConfig{reverseProxyConfig},
|
||||
}
|
||||
|
||||
autoupdater := updater.NewAutoUpdater(defaultClientConfig.SupervisorConfig.AutoUpdateFrequency, listeners)
|
||||
|
||||
originCert, err := getOriginCert(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("error getting origin cert")
|
||||
return err
|
||||
}
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to parse tag")
|
||||
return err
|
||||
}
|
||||
|
||||
var scope pogs.Scope
|
||||
if c.IsSet("group") == c.IsSet("system-name") {
|
||||
err = fmt.Errorf("exactly one of --group or --system-name must be specified")
|
||||
logger.WithError(err).Error("unable to determine scope")
|
||||
return err
|
||||
} else if c.IsSet("group") {
|
||||
scope = pogs.NewGroup(c.String("group"))
|
||||
} else {
|
||||
scope = pogs.NewSystemName(c.String("system-name"))
|
||||
}
|
||||
|
||||
cloudflaredConfig := &connection.CloudflaredConfig{
|
||||
BuildInfo: buildInfo,
|
||||
CloudflaredID: cloudflaredID,
|
||||
Scope: scope,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
serviceDiscoverer, err := serviceDiscoverer(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create service discoverer")
|
||||
return err
|
||||
}
|
||||
supervisor, err := supervisor.NewSupervisor(defaultClientConfig, originCert, toEdgeTLSConfig,
|
||||
serviceDiscoverer, cloudflaredConfig, autoupdater, updater.SupportAutoUpdate(), logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create Supervisor")
|
||||
return err
|
||||
}
|
||||
return supervisor.Run(ctx)
|
||||
}
|
||||
|
||||
func defaultOriginConfig(c *cli.Context) (pogs.OriginConfig, error) {
|
||||
if c.IsSet("hello-world") {
|
||||
return &pogs.HelloWorldOriginConfig{}, nil
|
||||
}
|
||||
originConfig := &pogs.HTTPOriginConfig{
|
||||
TCPKeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DialDualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
TLSVerify: !c.Bool("no-tls-verify"),
|
||||
OriginCAPool: c.String("origin-ca-pool"),
|
||||
OriginServerName: c.String("origin-server-name"),
|
||||
MaxIdleConnections: c.Uint64("proxy-keepalive-connections"),
|
||||
IdleConnectionTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
ProxyConnectionTimeout: c.Duration("proxy-connection-timeout"),
|
||||
ExpectContinueTimeout: c.Duration("proxy-expect-continue-timeout"),
|
||||
ChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
}
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating --unix-socket")
|
||||
}
|
||||
originConfig.URLString = unixSocket
|
||||
}
|
||||
originAddr, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
originConfig.URLString = originAddr
|
||||
return originConfig, nil
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
@@ -421,8 +569,8 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
},
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "autoupdate-freq",
|
||||
Usage: "Autoupdate frequency. Default is 24h.",
|
||||
Value: time.Hour * 24,
|
||||
Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq),
|
||||
Value: updater.DefaultCheckUpdateFreq,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
@@ -444,7 +592,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "cacert",
|
||||
Name: tlsconfig.CaCertFlag,
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
EnvVars: []string{"TUNNEL_CACERT"},
|
||||
Hidden: true,
|
||||
@@ -463,7 +611,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-ca-pool",
|
||||
Name: tlsconfig.OriginCAPoolFlag,
|
||||
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
|
||||
Hidden: shouldHide,
|
||||
@@ -636,6 +784,18 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: time.Second * 90,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-connection-timeout",
|
||||
Usage: "HTTP proxy timeout for closing an idle connection",
|
||||
Value: time.Second * 90,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-expect-continue-timeout",
|
||||
Usage: "HTTP proxy timeout for closing an idle connection",
|
||||
Value: time.Second * 90,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-dns",
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
@@ -695,5 +855,24 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "system-name",
|
||||
Usage: "Unique identifier for this cloudflared instance. It can be configured individually in the Declarative Tunnel UI. Mutually exclusive with `--group`.",
|
||||
EnvVars: []string{"TUNNEL_SYSTEM_NAME"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "group",
|
||||
Usage: "Name of a group of cloudflared instances, of which this instance should be an identical copy. They can be configured collectively in the Declarative Tunnel UI. Mutually exclusive with `--system-name`.",
|
||||
EnvVars: []string{"TUNNEL_GROUP"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "dial-edge-timeout",
|
||||
Usage: "Maximum wait time to set up a connection with the edge",
|
||||
Value: time.Second * 15,
|
||||
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ package tunnel
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -147,7 +147,7 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *origin.BuildInfo,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string, logger,
|
||||
transportLogger *logrus.Logger,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
@@ -187,7 +187,7 @@ func prepareTunnelConfig(
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := loadCertPool(c, logger)
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error loading cert pool")
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
@@ -236,7 +236,7 @@ func prepareTunnelConfig(
|
||||
return nil, errors.Wrap(err, "unable to connect to the origin")
|
||||
}
|
||||
|
||||
toEdgeTLSConfig, err := createTunnelConfig(c)
|
||||
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
@@ -274,110 +274,13 @@ func prepareTunnelConfig(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
const originCAPoolFlag = "origin-ca-pool"
|
||||
originCAPoolFilename := c.String(originCAPoolFlag)
|
||||
var originCustomCAPool []byte
|
||||
|
||||
if originCAPoolFilename != "" {
|
||||
var err error
|
||||
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, originCAPoolFlag))
|
||||
}
|
||||
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (connection.EdgeServiceDiscoverer, error) {
|
||||
// If --edge is specfied, resolve edge server addresses
|
||||
if len(c.StringSlice("edge")) > 0 {
|
||||
return connection.NewEdgeHostnameResolver(c.StringSlice("edge"))
|
||||
}
|
||||
|
||||
originCertPool, err := loadOriginCertPool(originCustomCAPool)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading the certificate pool")
|
||||
}
|
||||
|
||||
// Windows users should be notified that they can use the flag
|
||||
if runtime.GOOS == "windows" && originCAPoolFilename == "" {
|
||||
logger.Infof("cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it", originCAPoolFlag)
|
||||
}
|
||||
|
||||
return originCertPool, nil
|
||||
}
|
||||
|
||||
func loadOriginCertPool(originCAPoolPEM []byte) (*x509.CertPool, error) {
|
||||
// Get the global pool
|
||||
certPool, err := loadGlobalCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Then, add any custom origin CA pool the user may have passed
|
||||
if originCAPoolPEM != nil {
|
||||
if !certPool.AppendCertsFromPEM(originCAPoolPEM) {
|
||||
logger.Warn("could not append the provided origin CA to the cloudflared certificate pool")
|
||||
}
|
||||
}
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func loadGlobalCertPool() (*x509.CertPool, error) {
|
||||
// First, obtain the system certificate pool
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" {
|
||||
logger.WithError(err).Warn("error obtaining the system certificates")
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Next, append the Cloudflare CAs into the system pool
|
||||
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
|
||||
// Finally, add the Hello certificate into the pool (since it's self-signed)
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Hello server certificate to cloudflared certificate pool")
|
||||
}
|
||||
certPool.AddCert(helloCert)
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func createTunnelConfig(c *cli.Context) (*tls.Config, error) {
|
||||
var rootCAs []string
|
||||
if c.String("cacert") != "" {
|
||||
rootCAs = append(rootCAs, c.String("cacert"))
|
||||
}
|
||||
edgeAddrs := c.StringSlice("edge")
|
||||
|
||||
userConfig := &tlsconfig.TLSParameters{RootCAs: rootCAs}
|
||||
tlsConfig, err := tlsconfig.GetConfig(userConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig.RootCAs == nil {
|
||||
rootCAPool := x509.NewCertPool()
|
||||
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
rootCAPool.AddCert(cert)
|
||||
}
|
||||
tlsConfig.RootCAs = rootCAPool
|
||||
tlsConfig.ServerName = "cftunnel.com"
|
||||
} else if len(edgeAddrs) > 0 {
|
||||
// Set for development environments and for testing specific origintunneld instances
|
||||
tlsConfig.ServerName, _, _ = net.SplitHostPort(edgeAddrs[0])
|
||||
}
|
||||
|
||||
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
|
||||
return nil, fmt.Errorf("either ServerName or InsecureSkipVerify must be specified in the tls.Config")
|
||||
}
|
||||
return tlsConfig, nil
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return connection.NewEdgeAddrResolver(logger)
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
@@ -75,30 +77,6 @@ func Update(_ *cli.Context) error {
|
||||
return updateOutcome.Error
|
||||
}
|
||||
|
||||
func Autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
|
||||
tickC := time.Tick(freq)
|
||||
for {
|
||||
updateOutcome := loggedUpdate()
|
||||
if updateOutcome.Updated {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
pid, err := listeners.StartProcess()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unable to restart server automatically")
|
||||
return err
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
logger.Infof("PID of the new process is %d", pid)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-tickC:
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate() UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
@@ -112,7 +90,88 @@ func loggedUpdate() UpdateOutcome {
|
||||
return updateOutcome
|
||||
}
|
||||
|
||||
// AutoUpdater periodically checks for new version of cloudflared.
|
||||
type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
type configurable struct {
|
||||
enabled bool
|
||||
freq time.Duration
|
||||
}
|
||||
|
||||
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net) *AutoUpdater {
|
||||
updaterConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: freq,
|
||||
}
|
||||
if freq == 0 {
|
||||
updaterConfigurable.enabled = false
|
||||
updaterConfigurable.freq = DefaultCheckUpdateFreq
|
||||
}
|
||||
return &AutoUpdater{
|
||||
configurable: updaterConfigurable,
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.configurable.freq)
|
||||
for {
|
||||
if a.configurable.enabled {
|
||||
updateOutcome := loggedUpdate()
|
||||
if updateOutcome.Updated {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unable to restart server automatically")
|
||||
return err
|
||||
}
|
||||
// stop old process after autoupdate. Otherwise we create a new process
|
||||
// after each update
|
||||
logger.Infof("PID of the new process is %d", pid)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newConfigurable := <-a.updateConfigChan:
|
||||
ticker.Stop()
|
||||
a.configurable = newConfigurable
|
||||
ticker = time.NewTicker(a.configurable.freq)
|
||||
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
|
||||
func (a *AutoUpdater) Update(newFreq time.Duration) {
|
||||
newConfigurable := &configurable{
|
||||
enabled: true,
|
||||
freq: newFreq,
|
||||
}
|
||||
// A ero duration means autoupdate is disabled
|
||||
if newFreq == 0 {
|
||||
newConfigurable.enabled = false
|
||||
newConfigurable.freq = DefaultCheckUpdateFreq
|
||||
}
|
||||
a.updateConfigChan <- newConfigurable
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context) bool {
|
||||
if !SupportAutoUpdate() {
|
||||
return false
|
||||
}
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func SupportAutoUpdate() bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
@@ -122,8 +181,7 @@ func IsAutoupdateEnabled(c *cli.Context) bool {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
return true
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
autoupdater := NewAutoUpdater(0, listeners)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
|
||||
assert.False(t, autoupdater.configurable.enabled)
|
||||
assert.Equal(t, DefaultCheckUpdateFreq, autoupdater.configurable.freq)
|
||||
|
||||
cancel()
|
||||
// Make sure that autoupdater terminates after canceling the context
|
||||
assert.Equal(t, context.Canceled, <-errC)
|
||||
}
|
||||
+32
-113
@@ -2,14 +2,14 @@ package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 5 * time.Second
|
||||
openStreamTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
@@ -29,134 +28,54 @@ func (e dialError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
type muxerShutdownError struct{}
|
||||
|
||||
func (e muxerShutdownError) Error() string {
|
||||
return "muxer shutdown"
|
||||
type Connection struct {
|
||||
id uuid.UUID
|
||||
muxer *h2mux.Muxer
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
TLSConfig *tls.Config
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
type connectionHandler interface {
|
||||
serve(ctx context.Context) error
|
||||
connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error)
|
||||
shutdown()
|
||||
}
|
||||
|
||||
type h2muxHandler struct {
|
||||
muxer *h2mux.Muxer
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
type muxedStreamHandler struct {
|
||||
}
|
||||
|
||||
// Implements MuxedStreamHandler interface
|
||||
func (h *muxedStreamHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) serve(ctx context.Context) error {
|
||||
// Serve doesn't return until h2mux is shutdown
|
||||
if err := h.muxer.Serve(ctx); err != nil {
|
||||
return err
|
||||
func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return muxerShutdownError{}
|
||||
return &Connection{
|
||||
id: id,
|
||||
muxer: muxer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Connection) Serve(ctx context.Context) error {
|
||||
// Serve doesn't return until h2mux is shutdown
|
||||
return c.muxer.Serve(ctx)
|
||||
}
|
||||
|
||||
// Connect is used to establish connections with cloudflare's edge network
|
||||
func (h *h2muxHandler) connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error) {
|
||||
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (*pogs.ConnectResult, error) {
|
||||
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer cancel()
|
||||
conn, err := h.newRPConn(openStreamCtx)
|
||||
|
||||
rpcConn, err := c.newRPConn(openStreamCtx, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create new RPC connection")
|
||||
return nil, errors.Wrap(err, "cannot create new RPC connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
|
||||
defer rpcConn.Close()
|
||||
|
||||
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)}
|
||||
|
||||
return tsClient.Connect(ctx, parameters)
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) shutdown() {
|
||||
h.muxer.Shutdown()
|
||||
func (c *Connection) Shutdown() {
|
||||
c.muxer.Shutdown()
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) newRPConn(ctx context.Context) (*rpc.Conn, error) {
|
||||
stream, err := h.muxer.OpenStream(ctx, []h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
func (c *Connection) newRPConn(ctx context.Context, logger *logrus.Entry) (*rpc.Conn, error) {
|
||||
stream, err := c.muxer.OpenRPCStream(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(h.logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(h.logger.WithField("subsystem", "rpc-transport")),
|
||||
tunnelrpc.NewTransportLogger(logger.WithField("rpc", "connect"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(logger.WithField("rpc", "connect")),
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewConnectionHandler returns a connectionHandler, wrapping h2mux to make RPC calls
|
||||
func newH2MuxHandler(ctx context.Context,
|
||||
config *ConnectionConfig,
|
||||
edgeIP *net.TCPAddr,
|
||||
) (connectionHandler, error) {
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer dialCancel()
|
||||
dialer := net.Dialer{DualStack: true}
|
||||
plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String())
|
||||
if err != nil {
|
||||
return nil, dialError{cause: errors.Wrap(err, "DialContext error")}
|
||||
}
|
||||
edgeConn := tls.Client(plaintextEdgeConn, config.TLSConfig)
|
||||
edgeConn.SetDeadline(time.Now().Add(dialTimeout))
|
||||
err = edgeConn.Handshake()
|
||||
if err != nil {
|
||||
return nil, dialError{cause: errors.Wrap(err, "Handshake with edge error")}
|
||||
}
|
||||
// clear the deadline on the conn; h2mux has its own timeouts
|
||||
edgeConn.SetDeadline(time.Time{})
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
|
||||
Timeout: dialTimeout,
|
||||
Handler: &muxedStreamHandler{},
|
||||
IsClient: true,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &h2muxHandler{
|
||||
muxer: muxer,
|
||||
logger: config.Logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// connectionPool is a pool of connection handlers
|
||||
type connectionPool struct {
|
||||
sync.Mutex
|
||||
connectionHandlers []connectionHandler
|
||||
}
|
||||
|
||||
func (cp *connectionPool) put(h connectionHandler) {
|
||||
cp.Lock()
|
||||
defer cp.Unlock()
|
||||
cp.connectionHandlers = append(cp.connectionHandlers, h)
|
||||
}
|
||||
|
||||
func (cp *connectionPool) close() {
|
||||
cp.Lock()
|
||||
defer cp.Unlock()
|
||||
for _, h := range cp.connectionHandlers {
|
||||
h.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
+129
-18
@@ -5,10 +5,11 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,6 +23,9 @@ const (
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
|
||||
// SRV record resolution TTL
|
||||
resolveEdgeAddrTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
@@ -34,20 +38,65 @@ var friendlyDNSErrorLines = []string{
|
||||
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
|
||||
}
|
||||
|
||||
func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, error) {
|
||||
if len(addresses) > 0 {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
for _, address := range addresses {
|
||||
// Addresses specified (for testing, usually)
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
// EdgeServiceDiscoverer is an interface for looking up Cloudflare's edge network addresses
|
||||
type EdgeServiceDiscoverer interface {
|
||||
// Addr returns an address to connect to cloudflare's edge network
|
||||
Addr() *net.TCPAddr
|
||||
// AvailableAddrs returns the number of unique addresses
|
||||
AvailableAddrs() uint8
|
||||
// Refresh rediscover Cloudflare's edge network addresses
|
||||
Refresh() error
|
||||
}
|
||||
|
||||
// EdgeAddrResolver discovers the addresses of Cloudflare's edge network through SRV record.
|
||||
// It implements EdgeServiceDiscoverer interface
|
||||
type EdgeAddrResolver struct {
|
||||
sync.Mutex
|
||||
// Addrs to connect to cloudflare's edge network
|
||||
addrs []*net.TCPAddr
|
||||
// index of the next element to use in addrs
|
||||
nextAddrIndex int
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func NewEdgeAddrResolver(logger *logrus.Logger) (EdgeServiceDiscoverer, error) {
|
||||
r := &EdgeAddrResolver{
|
||||
logger: logger.WithField("subsystem", " edgeAddrResolver"),
|
||||
}
|
||||
// HA service discovery lookup
|
||||
if err := r.Refresh(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *EdgeAddrResolver) Addr() *net.TCPAddr {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
addr := r.addrs[r.nextAddrIndex]
|
||||
r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs)
|
||||
return addr
|
||||
}
|
||||
|
||||
func (r *EdgeAddrResolver) AvailableAddrs() uint8 {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return uint8(len(r.addrs))
|
||||
}
|
||||
|
||||
func (r *EdgeAddrResolver) Refresh() error {
|
||||
newAddrs, err := EdgeDiscovery(r.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.addrs = newAddrs
|
||||
r.nextAddrIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// HA service discovery lookup
|
||||
func EdgeDiscovery(logger *logrus.Entry) ([]*net.TCPAddr, error) {
|
||||
_, addrs, err := net.LookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
// Try to fall back to DoT from Cloudflare directly.
|
||||
@@ -78,7 +127,7 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var lookupErr error
|
||||
for _, addr := range addrs {
|
||||
ips, err := ResolveSRVToTCP(addr)
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
if err != nil || len(ips) == 0 {
|
||||
// don't return early, we might be able to resolve other addresses
|
||||
lookupErr = err
|
||||
@@ -86,14 +135,14 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
ips := FlattenServiceIPs(resolvedIPsPerCNAME)
|
||||
ips := flattenServiceIPs(resolvedIPsPerCNAME)
|
||||
if lookupErr == nil && len(ips) == 0 {
|
||||
return nil, fmt.Errorf("Unknown service discovery error")
|
||||
}
|
||||
return ips, lookupErr
|
||||
}
|
||||
|
||||
func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
ips, err := net.LookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -107,7 +156,7 @@ func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
|
||||
// FlattenServiceIPs transposes and flattens the input slices such that the
|
||||
// first element of the n inner slices are the first n elements of the result.
|
||||
func FlattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr {
|
||||
func flattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr {
|
||||
var result []*net.TCPAddr
|
||||
for len(ipsByService) > 0 {
|
||||
filtered := ipsByService[:0]
|
||||
@@ -141,3 +190,65 @@ func fallbackResolver(serverName, serverAddress string) *net.Resolver {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeHostnameResolver discovers the addresses of Cloudflare's edge network via a list of server hostnames.
|
||||
// It implements EdgeServiceDiscoverer interface, and is used mainly for testing connectivity.
|
||||
type EdgeHostnameResolver struct {
|
||||
sync.Mutex
|
||||
// hostnames of edge servers
|
||||
hostnames []string
|
||||
// Addrs to connect to cloudflare's edge network
|
||||
addrs []*net.TCPAddr
|
||||
// index of the next element to use in addrs
|
||||
nextAddrIndex int
|
||||
}
|
||||
|
||||
func NewEdgeHostnameResolver(edgeHostnames []string) (EdgeServiceDiscoverer, error) {
|
||||
r := &EdgeHostnameResolver{
|
||||
hostnames: edgeHostnames,
|
||||
}
|
||||
if err := r.Refresh(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *EdgeHostnameResolver) Addr() *net.TCPAddr {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
addr := r.addrs[r.nextAddrIndex]
|
||||
r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs)
|
||||
return addr
|
||||
}
|
||||
|
||||
func (r *EdgeHostnameResolver) AvailableAddrs() uint8 {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return uint8(len(r.addrs))
|
||||
}
|
||||
|
||||
func (r *EdgeHostnameResolver) Refresh() error {
|
||||
newAddrs, err := ResolveAddrs(r.hostnames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.addrs = newAddrs
|
||||
r.nextAddrIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
|
||||
// of the hostname's IP addresses
|
||||
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,26 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockEdgeServiceDiscoverer struct {
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Addr() *net.TCPAddr {
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 63102,
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() uint8 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Refresh() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFlattenServiceIPs(t *testing.T) {
|
||||
result := FlattenServiceIPs([][]*net.TCPAddr{
|
||||
result := flattenServiceIPs([][]*net.TCPAddr{
|
||||
[]*net.TCPAddr{
|
||||
&net.TCPAddr{Port: 1},
|
||||
&net.TCPAddr{Port: 2},
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
)
|
||||
|
||||
// EdgeManager manages connections with the edge
|
||||
type EdgeManager struct {
|
||||
// streamHandler handles stream opened by the edge
|
||||
streamHandler h2mux.MuxedStreamHandler
|
||||
// TLSConfig is the TLS configuration to connect with edge
|
||||
tlsConfig *tls.Config
|
||||
// cloudflaredConfig is the cloudflared configuration that is determined when the process first starts
|
||||
cloudflaredConfig *CloudflaredConfig
|
||||
// serviceDiscoverer returns the next edge addr to connect to
|
||||
serviceDiscoverer EdgeServiceDiscoverer
|
||||
// state is attributes of ConnectionManager that can change during runtime.
|
||||
state *edgeManagerState
|
||||
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// EdgeConnectionManagerConfigurable is the configurable attributes of a EdgeConnectionManager
|
||||
type EdgeManagerConfigurable struct {
|
||||
TunnelHostnames []h2mux.TunnelHostname
|
||||
*pogs.EdgeConnectionConfig
|
||||
}
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
CloudflaredID uuid.UUID
|
||||
Tags []pogs.Tag
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
Scope pogs.Scope
|
||||
}
|
||||
|
||||
func NewEdgeManager(
|
||||
streamHandler h2mux.MuxedStreamHandler,
|
||||
edgeConnMgrConfigurable *EdgeManagerConfigurable,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer EdgeServiceDiscoverer,
|
||||
cloudflaredConfig *CloudflaredConfig,
|
||||
logger *logrus.Logger,
|
||||
) *EdgeManager {
|
||||
return &EdgeManager{
|
||||
streamHandler: streamHandler,
|
||||
tlsConfig: tlsConfig,
|
||||
cloudflaredConfig: cloudflaredConfig,
|
||||
serviceDiscoverer: serviceDiscoverer,
|
||||
state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential),
|
||||
logger: logger.WithField("subsystem", "connectionManager"),
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EdgeManager) Run(ctx context.Context) error {
|
||||
defer em.shutdown()
|
||||
|
||||
resolveEdgeIPTicker := time.Tick(resolveEdgeAddrTTL)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "EdgeConnectionManager terminated")
|
||||
case <-resolveEdgeIPTicker:
|
||||
if err := em.serviceDiscoverer.Refresh(); err != nil {
|
||||
em.logger.WithError(err).Warn("Cannot refresh Cloudflare edge addresses")
|
||||
}
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
// Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted
|
||||
// in shouldCreateConnection or shouldReduceConnection calculation
|
||||
if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) {
|
||||
if err := em.newConnection(ctx); err != nil {
|
||||
em.logger.WithError(err).Error("cannot create new connection")
|
||||
}
|
||||
} else if em.state.shouldReduceConnection() {
|
||||
if err := em.closeConnection(ctx); err != nil {
|
||||
em.logger.WithError(err).Error("cannot close connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurable) {
|
||||
em.logger.Infof("New edge connection manager configuration %+v", newConfigurable)
|
||||
em.state.updateConfigurable(newConfigurable)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) newConnection(ctx context.Context) error {
|
||||
edgeIP := em.serviceDiscoverer.Addr()
|
||||
edgeConn, err := em.dialEdge(ctx, edgeIP)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial edge error")
|
||||
}
|
||||
configurable := em.state.getConfigurable()
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
|
||||
Timeout: configurable.Timeout,
|
||||
Handler: em.streamHandler,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: configurable.HeartbeatInterval,
|
||||
MaxHeartbeats: configurable.MaxFailedHeartbeats,
|
||||
Logger: em.logger.WithField("subsystem", "muxer"),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't perform handshake with edge")
|
||||
}
|
||||
|
||||
h2muxConn, err := newConnection(muxer, edgeIP)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't create h2mux connection")
|
||||
}
|
||||
|
||||
go em.serveConn(ctx, h2muxConn)
|
||||
|
||||
connResult, err := h2muxConn.Connect(ctx, &pogs.ConnectParameters{
|
||||
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
|
||||
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
|
||||
NumPreviousAttempts: 0,
|
||||
OriginCert: em.state.getUserCredential(),
|
||||
Scope: em.cloudflaredConfig.Scope,
|
||||
Tags: em.cloudflaredConfig.Tags,
|
||||
}, em.logger)
|
||||
if err != nil {
|
||||
h2muxConn.Shutdown()
|
||||
return errors.Wrap(err, "couldn't connect to edge")
|
||||
}
|
||||
|
||||
if connErr := connResult.Err; connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
return errors.Wrap(connErr, em.noRetryMessage())
|
||||
}
|
||||
return errors.Wrapf(connErr, "edge responded with RetryAfter=%v", connErr.RetryAfter)
|
||||
}
|
||||
|
||||
em.state.newConnection(h2muxConn)
|
||||
em.logger.Infof("connected to %s", connResult.ServerInfo.LocationName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (em *EdgeManager) closeConnection(ctx context.Context) error {
|
||||
conn := em.state.getFirstConnection()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("no connection to close")
|
||||
}
|
||||
conn.Shutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) {
|
||||
err := conn.Serve(ctx)
|
||||
em.logger.WithError(err).Warn("Connection closed")
|
||||
em.state.closeConnection(conn)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) dialEdge(ctx context.Context, edgeIP *net.TCPAddr) (*tls.Conn, error) {
|
||||
timeout := em.state.getConfigurable().Timeout
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
|
||||
defer dialCancel()
|
||||
|
||||
dialer := net.Dialer{DualStack: true}
|
||||
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String())
|
||||
if err != nil {
|
||||
return nil, dialError{cause: errors.Wrap(err, "DialContext error")}
|
||||
}
|
||||
tlsEdgeConn := tls.Client(edgeConn, em.tlsConfig)
|
||||
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
if err = tlsEdgeConn.Handshake(); err != nil {
|
||||
return nil, dialError{cause: errors.Wrap(err, "Handshake with edge error")}
|
||||
}
|
||||
// clear the deadline on the conn; h2mux has its own timeouts
|
||||
tlsEdgeConn.SetDeadline(time.Time{})
|
||||
return tlsEdgeConn, nil
|
||||
}
|
||||
|
||||
func (em *EdgeManager) noRetryMessage() string {
|
||||
messageTemplate := "cloudflared could not register an Argo Tunnel on your account. Please confirm the following before trying again:" +
|
||||
"1. You have Argo Smart Routing enabled in your account, See Enable Argo section of %s." +
|
||||
"2. Your credential at %s is still valid. See %s."
|
||||
return fmt.Sprintf(messageTemplate, quickStartLink, em.state.getConfigurable().UserCredentialPath, faqLink)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) shutdown() {
|
||||
em.state.shutdown()
|
||||
}
|
||||
|
||||
type edgeManagerState struct {
|
||||
sync.RWMutex
|
||||
configurable *EdgeManagerConfigurable
|
||||
userCredential []byte
|
||||
conns map[uuid.UUID]*Connection
|
||||
}
|
||||
|
||||
func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCredential []byte) *edgeManagerState {
|
||||
return &edgeManagerState{
|
||||
configurable: configurable,
|
||||
userCredential: userCredential,
|
||||
conns: make(map[uuid.UUID]*Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs uint8) bool {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
expectedHAConns := ems.configurable.NumHAConnections
|
||||
if availableEdgeAddrs < expectedHAConns {
|
||||
expectedHAConns = availableEdgeAddrs
|
||||
}
|
||||
return uint8(len(ems.conns)) < expectedHAConns
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldReduceConnection() bool {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
return uint8(len(ems.conns)) > ems.configurable.NumHAConnections
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) newConnection(conn *Connection) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
ems.conns[conn.id] = conn
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) closeConnection(conn *Connection) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
delete(ems.conns, conn.id)
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getFirstConnection() *Connection {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
|
||||
for _, conn := range ems.conns {
|
||||
return conn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shutdown() {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
for _, conn := range ems.conns {
|
||||
conn.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getConfigurable() *EdgeManagerConfigurable {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
return ems.configurable
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) updateConfigurable(newConfigurable *EdgeManagerConfigurable) {
|
||||
ems.Lock()
|
||||
defer ems.Unlock()
|
||||
ems.configurable = newConfigurable
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) getUserCredential() []byte {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
return ems.userCredential
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
configurable = &EdgeManagerConfigurable{
|
||||
[]h2mux.TunnelHostname{
|
||||
"http.example.com",
|
||||
"ws.example.com",
|
||||
"hello.example.com",
|
||||
},
|
||||
&pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: 1,
|
||||
HeartbeatInterval: 1 * time.Second,
|
||||
Timeout: 5 * time.Second,
|
||||
MaxFailedHeartbeats: 3,
|
||||
UserCredentialPath: "/etc/cloudflared/cert.pem",
|
||||
},
|
||||
}
|
||||
cloudflaredConfig = &CloudflaredConfig{
|
||||
CloudflaredID: uuid.New(),
|
||||
Tags: []pogs.Tag{
|
||||
{Name: "pool", Value: "east-6"},
|
||||
},
|
||||
BuildInfo: &buildinfo.BuildInfo{
|
||||
GoOS: "linux",
|
||||
GoVersion: "1.12",
|
||||
GoArch: "amd64",
|
||||
CloudflaredVersion: "2019.6.0",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type mockStreamHandler struct {
|
||||
}
|
||||
|
||||
func (msh *mockStreamHandler) ServeStream(*h2mux.MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockEdgeManager() *EdgeManager {
|
||||
return NewEdgeManager(
|
||||
&mockStreamHandler{},
|
||||
configurable,
|
||||
[]byte{},
|
||||
nil,
|
||||
&mockEdgeServiceDiscoverer{},
|
||||
cloudflaredConfig,
|
||||
logrus.New(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestUpdateConfigurable(t *testing.T) {
|
||||
m := mockEdgeManager()
|
||||
newConfigurable := &EdgeManagerConfigurable{
|
||||
[]h2mux.TunnelHostname{
|
||||
"second.example.com",
|
||||
},
|
||||
&pogs.EdgeConnectionConfig{
|
||||
NumHAConnections: 2,
|
||||
},
|
||||
}
|
||||
m.UpdateConfigurable(newConfigurable)
|
||||
|
||||
assert.Equal(t, newConfigurable, m.state.getConfigurable())
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Waiting time before retrying a failed tunnel connection
|
||||
reconnectDuration = time.Second * 10
|
||||
// SRV record resolution TTL
|
||||
resolveTTL = time.Hour
|
||||
// Interval between establishing new connection
|
||||
connectionInterval = time.Second
|
||||
)
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
ConnectionConfig *ConnectionConfig
|
||||
OriginCert []byte
|
||||
Tags []tunnelpogs.Tag
|
||||
EdgeAddrs []string
|
||||
HAConnections uint
|
||||
Logger *logrus.Logger
|
||||
CloudflaredVersion string
|
||||
}
|
||||
|
||||
// Supervisor is a stateful object that manages connections with the edge
|
||||
type Supervisor struct {
|
||||
config *CloudflaredConfig
|
||||
state *supervisorState
|
||||
connErrors chan error
|
||||
}
|
||||
|
||||
type supervisorState struct {
|
||||
// IPs to connect to cloudflare's edge network
|
||||
edgeIPs []*net.TCPAddr
|
||||
// index of the next element to use in edgeIPs
|
||||
nextEdgeIPIndex int
|
||||
// last time edgeIPs were refreshed
|
||||
lastResolveTime time.Time
|
||||
// ID of this cloudflared instance
|
||||
cloudflaredID uuid.UUID
|
||||
// connectionPool is a pool of connectionHandlers that can be used to make RPCs
|
||||
connectionPool *connectionPool
|
||||
}
|
||||
|
||||
func (s *supervisorState) getNextEdgeIP() *net.TCPAddr {
|
||||
ip := s.edgeIPs[s.nextEdgeIPIndex%len(s.edgeIPs)]
|
||||
s.nextEdgeIPIndex++
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewSupervisor(config *CloudflaredConfig) *Supervisor {
|
||||
return &Supervisor{
|
||||
config: config,
|
||||
state: &supervisorState{
|
||||
connectionPool: &connectionPool{},
|
||||
},
|
||||
connErrors: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(); err != nil {
|
||||
logger.WithError(err).Error("Failed to get edge IPs")
|
||||
return err
|
||||
}
|
||||
defer s.state.connectionPool.close()
|
||||
|
||||
var currentConnectionCount uint
|
||||
expectedConnectionCount := s.config.HAConnections
|
||||
if uint(len(s.state.edgeIPs)) < s.config.HAConnections {
|
||||
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, len(s.state.edgeIPs))
|
||||
expectedConnectionCount = uint(len(s.state.edgeIPs))
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case connErr := <-s.connErrors:
|
||||
logger.WithError(connErr).Warnf("Connection dropped unexpectedly")
|
||||
currentConnectionCount--
|
||||
default:
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
if currentConnectionCount < expectedConnectionCount {
|
||||
h, err := newH2MuxHandler(ctx, s.config.ConnectionConfig, s.state.getNextEdgeIP())
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create new connection handler")
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
s.connErrors <- h.serve(ctx)
|
||||
}()
|
||||
connResult, err := s.connect(ctx, s.config, s.state.cloudflaredID, h)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Failed to connect to cloudflared's edge network")
|
||||
h.shutdown()
|
||||
continue
|
||||
}
|
||||
if connErr := connResult.Err; connErr != nil && !connErr.ShouldRetry {
|
||||
logger.WithError(connErr).Errorf("Server respond with don't retry to connect")
|
||||
h.shutdown()
|
||||
return err
|
||||
}
|
||||
logger.Infof("Connected to %s", connResult.ServerInfo.LocationName)
|
||||
s.state.connectionPool.put(h)
|
||||
currentConnectionCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize() error {
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to resolve cloudflare edge network address")
|
||||
}
|
||||
s.state.edgeIPs = edgeIPs
|
||||
s.state.lastResolveTime = time.Now()
|
||||
cloudflaredID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate cloudflared ID")
|
||||
}
|
||||
s.state.cloudflaredID = cloudflaredID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) connect(ctx context.Context,
|
||||
config *CloudflaredConfig,
|
||||
cloudflaredID uuid.UUID,
|
||||
h connectionHandler,
|
||||
) (*tunnelpogs.ConnectResult, error) {
|
||||
connectParameters := &tunnelpogs.ConnectParameters{
|
||||
OriginCert: config.OriginCert,
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: 0,
|
||||
CloudflaredVersion: config.CloudflaredVersion,
|
||||
}
|
||||
return h.connect(ctx, connectParameters)
|
||||
}
|
||||
@@ -94,6 +94,14 @@ type Header struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
func RPCHeaders() []Header {
|
||||
return []Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}
|
||||
}
|
||||
|
||||
// Handshake establishes a muxed connection with the peer.
|
||||
// After the handshake completes, it is possible to open and accept streams.
|
||||
func Handshake(
|
||||
@@ -414,6 +422,41 @@ func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
|
||||
stream := &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: m.config.DefaultWindowSize,
|
||||
receiveWindowCurrentMax: m.config.DefaultWindowSize,
|
||||
receiveWindowMax: m.config.MaxWindowSize,
|
||||
sendWindow: m.config.DefaultWindowSize,
|
||||
readyList: m.readyList,
|
||||
writeHeaders: RPCHeaders(),
|
||||
dictionaries: m.muxReader.dictionaries,
|
||||
}
|
||||
|
||||
select {
|
||||
// Will be received by mux writer
|
||||
case <-ctx.Done():
|
||||
return nil, ErrOpenStreamTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: nil}:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ErrResponseHeadersTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case <-stream.responseHeadersReceived:
|
||||
return stream, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) Metrics() *MuxerMetrics {
|
||||
return m.muxMetricsUpdater.metrics()
|
||||
}
|
||||
|
||||
+31
-1
@@ -68,7 +68,8 @@ type MuxedStream struct {
|
||||
sentEOF bool
|
||||
// true if the peer sent us an EOF
|
||||
receivedEOF bool
|
||||
|
||||
// If valid, tunnelHostname is used to identify which origin service is the intended recipient of the request
|
||||
tunnelHostname TunnelHostname
|
||||
// Compression-related fields
|
||||
receivedUseDict bool
|
||||
method string
|
||||
@@ -77,6 +78,16 @@ type MuxedStream struct {
|
||||
dictionaries h2Dictionaries
|
||||
}
|
||||
|
||||
type TunnelHostname string
|
||||
|
||||
func (th TunnelHostname) String() string {
|
||||
return string(th)
|
||||
}
|
||||
|
||||
func (th TunnelHostname) IsSet() bool {
|
||||
return th != ""
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Read(p []byte) (n int, err error) {
|
||||
var readBuffer ReadWriteClosedCloser
|
||||
if s.dictionaries.read != nil {
|
||||
@@ -185,6 +196,25 @@ func (s *MuxedStream) WriteHeaders(headers []Header) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRPCStream returns if the stream is used to transport RPC.
|
||||
func (s *MuxedStream) IsRPCStream() bool {
|
||||
rpcHeaders := RPCHeaders()
|
||||
if len(s.Headers) != len(rpcHeaders) {
|
||||
return false
|
||||
}
|
||||
// The headers order matters, so RPC stream should be opened with OpenRPCStream method and let MuxWriter serializes the headers.
|
||||
for i, rpcHeader := range rpcHeaders {
|
||||
if s.Headers[i] != rpcHeader {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *MuxedStream) TunnelHostname() TunnelHostname {
|
||||
return s.tunnelHostname
|
||||
}
|
||||
|
||||
func (s *MuxedStream) getReceiveWindow() uint32 {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
|
||||
@@ -98,3 +98,30 @@ func TestMuxedStreamEOF(t *testing.T) {
|
||||
assert.Equal(t, 0, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRPCStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
stream *MuxedStream
|
||||
isRPCStream bool
|
||||
}{
|
||||
{
|
||||
stream: &MuxedStream{},
|
||||
isRPCStream: false,
|
||||
},
|
||||
{
|
||||
stream: &MuxedStream{Headers: RPCHeaders()},
|
||||
isRPCStream: true,
|
||||
},
|
||||
{
|
||||
stream: &MuxedStream{Headers: []Header{
|
||||
{Name: ":method", Value: "rpc"},
|
||||
{Name: ":scheme", Value: "Capnp"},
|
||||
{Name: ":path", Value: "/"},
|
||||
}},
|
||||
isRPCStream: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isRPCStream, test.stream.IsRPCStream())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import (
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
CloudflaredProxyTunnelHostnameHeader = "cf-cloudflared-proxy-tunnel-hostname"
|
||||
)
|
||||
|
||||
type MuxReader struct {
|
||||
// f is used to read HTTP2 frames.
|
||||
f *http2.Framer
|
||||
@@ -235,6 +239,8 @@ func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
|
||||
if r.dictionaries.write != nil {
|
||||
continue
|
||||
}
|
||||
case CloudflaredProxyTunnelHostnameHeader:
|
||||
stream.tunnelHostname = TunnelHostname(header.Value)
|
||||
}
|
||||
headers = append(headers, Header{Name: header.Name, Value: header.Value})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
methodHeader = Header{
|
||||
Name: ":method",
|
||||
Value: "GET",
|
||||
}
|
||||
schemeHeader = Header{
|
||||
Name: ":scheme",
|
||||
Value: "https",
|
||||
}
|
||||
pathHeader = Header{
|
||||
Name: ":path",
|
||||
Value: "/api/tunnels",
|
||||
}
|
||||
tunnelHostnameHeader = Header{
|
||||
Name: CloudflaredProxyTunnelHostnameHeader,
|
||||
Value: "tunnel.example.com",
|
||||
}
|
||||
respStatusHeader = Header{
|
||||
Name: ":status",
|
||||
Value: "200",
|
||||
}
|
||||
)
|
||||
|
||||
type mockOriginStreamHandler struct {
|
||||
stream *MuxedStream
|
||||
}
|
||||
|
||||
func (mosh *mockOriginStreamHandler) ServeStream(stream *MuxedStream) error {
|
||||
mosh.stream = stream
|
||||
// Echo tunnel hostname in header
|
||||
stream.WriteHeaders([]Header{respStatusHeader})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCloudflaredProxyTunnelHostnameHeader(stream *MuxedStream) string {
|
||||
for _, header := range stream.Headers {
|
||||
if header.Name == CloudflaredProxyTunnelHostnameHeader {
|
||||
return header.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func assertOpenStreamSucceed(t *testing.T, stream *MuxedStream, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, stream.Headers, 1)
|
||||
assert.Equal(t, respStatusHeader, stream.Headers[0])
|
||||
}
|
||||
|
||||
func TestMissingHeaders(t *testing.T) {
|
||||
originHandler := &mockOriginStreamHandler{}
|
||||
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
|
||||
muxPair.Serve(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
reqHeaders := []Header{
|
||||
{
|
||||
Name: "content-type",
|
||||
Value: "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
// Request doesn't contain CloudflaredProxyTunnelHostnameHeader
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
|
||||
assertOpenStreamSucceed(t, stream, err)
|
||||
|
||||
assert.Empty(t, originHandler.stream.method)
|
||||
assert.Empty(t, originHandler.stream.path)
|
||||
assert.False(t, originHandler.stream.TunnelHostname().IsSet())
|
||||
}
|
||||
|
||||
func TestReceiveHeaderData(t *testing.T) {
|
||||
originHandler := &mockOriginStreamHandler{}
|
||||
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
|
||||
muxPair.Serve(t)
|
||||
|
||||
reqHeaders := []Header{
|
||||
methodHeader,
|
||||
schemeHeader,
|
||||
pathHeader,
|
||||
tunnelHostnameHeader,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
reqHeaders = append(reqHeaders, tunnelHostnameHeader)
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, reqHeaders, nil)
|
||||
assertOpenStreamSucceed(t, stream, err)
|
||||
|
||||
assert.Equal(t, methodHeader.Value, originHandler.stream.method)
|
||||
assert.Equal(t, pathHeader.Value, originHandler.stream.path)
|
||||
assert.True(t, originHandler.stream.TunnelHostname().IsSet())
|
||||
assert.Equal(t, tunnelHostnameHeader.Value, originHandler.stream.TunnelHostname().String())
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
}
|
||||
|
||||
func GetBuildInfo() *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
|
||||
@@ -34,6 +36,8 @@ type Supervisor struct {
|
||||
// currently-connecting tunnels to finish connecting so we can reset backoff timer
|
||||
nextConnectedIndex int
|
||||
nextConnectedSignal chan struct{}
|
||||
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
type resolveResult struct {
|
||||
@@ -51,6 +55,7 @@ func NewSupervisor(config *TunnelConfig) *Supervisor {
|
||||
config: config,
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
logger: config.Logger.WithField("subsystem", "supervisor"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +129,10 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
logger := s.config.Logger
|
||||
edgeIPs, err := connection.ResolveEdgeIPs(logger, s.config.EdgeAddrs)
|
||||
logger := s.logger
|
||||
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
|
||||
if err != nil {
|
||||
logger.Infof("ResolveEdgeIPs err")
|
||||
return err
|
||||
@@ -215,6 +222,15 @@ func (s *Supervisor) getEdgeIP(index int) *net.TCPAddr {
|
||||
return s.edgeIPs[index%len(s.edgeIPs)]
|
||||
}
|
||||
|
||||
func (s *Supervisor) resolveEdgeIPs() ([]*net.TCPAddr, error) {
|
||||
// If --edge is specfied, resolve edge server addresses
|
||||
if len(s.config.EdgeAddrs) > 0 {
|
||||
return connection.ResolveAddrs(s.config.EdgeAddrs)
|
||||
}
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return connection.EdgeDiscovery(s.logger)
|
||||
}
|
||||
|
||||
func (s *Supervisor) refreshEdgeIPs() {
|
||||
if s.resolverC != nil {
|
||||
return
|
||||
@@ -224,7 +240,7 @@ func (s *Supervisor) refreshEdgeIPs() {
|
||||
}
|
||||
s.resolverC = make(chan resolveResult)
|
||||
go func() {
|
||||
edgeIPs, err := connection.ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs)
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
+8
-84
@@ -14,9 +14,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
@@ -41,7 +42,7 @@ const (
|
||||
)
|
||||
|
||||
type TunnelConfig struct {
|
||||
BuildInfo *BuildInfo
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
ClientID string
|
||||
ClientTlsConfig *tls.Config
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
@@ -139,44 +140,8 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
}
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal *signal.Signal) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-shutdownC
|
||||
cancel()
|
||||
}()
|
||||
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a user specified negative HAConnections, we will treat it as requesting 1 connection
|
||||
if config.HAConnections > 1 {
|
||||
if config.UseDeclarativeTunnel {
|
||||
return connection.NewSupervisor(&connection.CloudflaredConfig{
|
||||
ConnectionConfig: &connection.ConnectionConfig{
|
||||
TLSConfig: config.TlsConfig,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.Logger.WithField("subsystem", "connection_supervisor"),
|
||||
},
|
||||
OriginCert: config.OriginCert,
|
||||
Tags: config.Tags,
|
||||
EdgeAddrs: config.EdgeAddrs,
|
||||
HAConnections: uint(config.HAConnections),
|
||||
Logger: config.Logger,
|
||||
CloudflaredVersion: config.ReportedVersion,
|
||||
}).Run(ctx)
|
||||
}
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal, u)
|
||||
} else {
|
||||
addrs, err := connection.ResolveEdgeIPs(config.Logger, config.EdgeAddrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal, u)
|
||||
}
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal, cloudflaredID)
|
||||
}
|
||||
|
||||
func ServeTunnelLoop(ctx context.Context,
|
||||
@@ -471,39 +436,6 @@ func LogServerInfo(
|
||||
metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName)
|
||||
}
|
||||
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
switch header.Name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
u, err := url.Parse(header.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable path")
|
||||
}
|
||||
resolved := h1.URL.ResolveReference(u)
|
||||
// prevent escaping base URL
|
||||
if !strings.HasPrefix(resolved.String(), h1.URL.String()) {
|
||||
return fmt.Errorf("invalid path")
|
||||
}
|
||||
h1.URL = resolved
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
default:
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
|
||||
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
|
||||
for headerName, headerValues := range h1.Header {
|
||||
@@ -514,10 +446,6 @@ func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
|
||||
return
|
||||
}
|
||||
|
||||
func FindCfRayHeader(h1 *http.Request) string {
|
||||
return h1.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
type TunnelHandler struct {
|
||||
originUrl string
|
||||
muxer *h2mux.Muxer
|
||||
@@ -605,8 +533,8 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
return reqErr
|
||||
}
|
||||
|
||||
cfRay := FindCfRayHeader(req)
|
||||
lbProbe := isLBProbeRequest(req)
|
||||
cfRay := streamhandler.FindCfRayHeader(req)
|
||||
lbProbe := streamhandler.IsLBProbeRequest(req)
|
||||
h.logRequest(req, cfRay, lbProbe)
|
||||
|
||||
var resp *http.Response
|
||||
@@ -629,7 +557,7 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request,
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = streamhandler.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
@@ -759,10 +687,6 @@ func uint8ToString(input uint8) string {
|
||||
return strconv.FormatUint(uint64(input), 10)
|
||||
}
|
||||
|
||||
func isLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func asciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// Package client defines and implements interface to proxy to HTTP, websocket and hello world origins
|
||||
package originservice
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// OriginService is an interface to proxy requests to different type of origins
|
||||
type OriginService interface {
|
||||
Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error)
|
||||
URL() *url.URL
|
||||
Summary() string
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// HTTPService talks to origin using HTTP/HTTPS
|
||||
type HTTPService struct {
|
||||
client http.RoundTripper
|
||||
originURL *url.URL
|
||||
chunkedEncoding bool
|
||||
}
|
||||
|
||||
func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
|
||||
return &HTTPService{
|
||||
client: transport,
|
||||
originURL: url,
|
||||
chunkedEncoding: chunkedEncoding,
|
||||
}
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if !hc.chunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
req.ContentLength = int64(cLength)
|
||||
}
|
||||
}
|
||||
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp, err := hc.client.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error proxying request to HTTP origin")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to HTTP origin")
|
||||
}
|
||||
if isEventStream(resp) {
|
||||
writeEventStream(stream, resp.Body)
|
||||
} else {
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (hc *HTTPService) URL() *url.URL {
|
||||
return hc.originURL
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Summary() string {
|
||||
return fmt.Sprintf("HTTP service listening on %s", hc.originURL)
|
||||
}
|
||||
|
||||
func (hc *HTTPService) Shutdown() {}
|
||||
|
||||
// WebsocketService talks to origin using WS/WSS
|
||||
type WebsocketService struct {
|
||||
tlsConfig *tls.Config
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
}
|
||||
|
||||
func NewWebSocketService(tlsConfig *tls.Config, url *url.URL) (OriginService, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot start Websocket Proxy Server")
|
||||
}
|
||||
shutdownC := make(chan struct{})
|
||||
go func() {
|
||||
websocket.StartProxyServer(log.CreateLogger(), listener, url.String(), shutdownC)
|
||||
}()
|
||||
return &WebsocketService{
|
||||
tlsConfig: tlsConfig,
|
||||
originURL: url,
|
||||
shutdownC: shutdownC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
if !websocket.IsWebSocketUpgrade(req) {
|
||||
return nil, fmt.Errorf("request is not a websocket connection")
|
||||
}
|
||||
conn, response, err := websocket.ClientConnect(req, wsc.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = stream.WriteHeaders(h1ResponseToH2Response(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to websocket origin")
|
||||
}
|
||||
// Copy to/from stream to the undelying connection. Use the underlying
|
||||
// connection because cloudflared doesn't operate on the message themselves
|
||||
websocket.Stream(conn.UnderlyingConn(), stream)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) URL() *url.URL {
|
||||
return wsc.originURL
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Summary() string {
|
||||
return fmt.Sprintf("Websocket listening on %s", wsc.originURL)
|
||||
}
|
||||
|
||||
func (wsc *WebsocketService) Shutdown() {
|
||||
close(wsc.shutdownC)
|
||||
}
|
||||
|
||||
// HelloWorldService talks to the hello world example origin
|
||||
type HelloWorldService struct {
|
||||
client http.RoundTripper
|
||||
listener net.Listener
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
}
|
||||
|
||||
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
|
||||
listener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot start Hello World Server")
|
||||
}
|
||||
shutdownC := make(chan struct{})
|
||||
go func() {
|
||||
hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC)
|
||||
}()
|
||||
return &HelloWorldService{
|
||||
client: transport,
|
||||
listener: listener,
|
||||
originURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: listener.Addr().String(),
|
||||
},
|
||||
shutdownC: shutdownC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
resp, err := hwc.client.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error proxying request to Hello World origin")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error writing response header to Hello World origin")
|
||||
}
|
||||
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) URL() *url.URL {
|
||||
return hwc.originURL
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Summary() string {
|
||||
return fmt.Sprintf("Hello World service listening on %s", hwc.originURL)
|
||||
}
|
||||
|
||||
func (hwc *HelloWorldService) Shutdown() {
|
||||
hwc.listener.Close()
|
||||
}
|
||||
|
||||
func isEventStream(resp *http.Response) bool {
|
||||
// Check if content-type is text/event-stream. We need to check if the header value starts with text/event-stream
|
||||
// because text/event-stream; charset=UTF-8 is also valid
|
||||
// Ref: https://tools.ietf.org/html/rfc7231#section-3.1.1.1
|
||||
for _, contentType := range resp.Header["content-type"] {
|
||||
if strings.HasPrefix(strings.ToLower(contentType), "text/event-stream") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeEventStream(stream *h2mux.MuxedStream, respBody io.ReadCloser) {
|
||||
reader := bufio.NewReader(respBody)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
stream.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
func h1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
|
||||
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
|
||||
for headerName, headerValues := range h1.Header {
|
||||
for _, headerValue := range headerValues {
|
||||
h2 = append(h2, h2mux.Header{Name: strings.ToLower(headerName), Value: headerValue})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package originservice
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsEventStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
resp *http.Response
|
||||
isEventStream bool
|
||||
}{
|
||||
{
|
||||
resp: &http.Response{},
|
||||
isEventStream: false,
|
||||
},
|
||||
{
|
||||
// isEventStream checks all headers
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"accept": []string{"text/html"},
|
||||
"content-type": []string{"text/event-stream"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{"Text/event-stream;charset=utf-8"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{"appication/json", "text/html", "Text/event-stream;charset=utf-8"},
|
||||
},
|
||||
},
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
// Not an event stream because the content-type value doesn't start with text/event-stream
|
||||
resp: &http.Response{
|
||||
Header: http.Header{
|
||||
"content-type": []string{" text/event-stream"},
|
||||
},
|
||||
},
|
||||
isEventStream: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isEventStream, isEventStream(test.resp), "Header: %v", test.resp.Header)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
)
|
||||
|
||||
func FindCfRayHeader(h1 *http.Request) string {
|
||||
return h1.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func IsLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url.String(), h2mux.MuxedStreamReader{MuxedStream: stream})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unexpected error from http.NewRequest")
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
switch header.Name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
h1.URL.Path = header.Value
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
default:
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{
|
||||
h2mux.Header{
|
||||
Name: "Mock header 1",
|
||||
Value: "Mock value 1",
|
||||
},
|
||||
h2mux.Header{
|
||||
Name: "Mock header 2",
|
||||
Value: "Mock value 2",
|
||||
},
|
||||
},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header 1": []string{"Mock value 1"},
|
||||
"Mock header 2": []string{"Mock value 2"},
|
||||
}, request.Header)
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.Equal(t, http.Header{}, request.Header)
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{
|
||||
h2mux.Header{
|
||||
Name: ":path",
|
||||
Value: "//bad_path/",
|
||||
},
|
||||
h2mux.Header{
|
||||
Name: "Mock header",
|
||||
Value: "Mock value",
|
||||
},
|
||||
},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, request.URL.String(), "http://example.com//bad_path/")
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelhostnamemapper"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
statusPseudoHeader = ":status"
|
||||
)
|
||||
|
||||
type httpErrorStatus struct {
|
||||
status string
|
||||
text []byte
|
||||
}
|
||||
|
||||
var (
|
||||
statusBadRequest = newHTTPErrorStatus(http.StatusBadRequest)
|
||||
statusNotFound = newHTTPErrorStatus(http.StatusNotFound)
|
||||
statusBadGateway = newHTTPErrorStatus(http.StatusBadGateway)
|
||||
)
|
||||
|
||||
func newHTTPErrorStatus(status int) *httpErrorStatus {
|
||||
return &httpErrorStatus{
|
||||
status: strconv.Itoa(status),
|
||||
text: []byte(http.StatusText(status)),
|
||||
}
|
||||
}
|
||||
|
||||
// StreamHandler handles new stream opened by the edge. The streams can be used to proxy requests or make RPC.
|
||||
type StreamHandler struct {
|
||||
// newConfigChan is a send-only channel to notify Supervisor of a new ClientConfig
|
||||
newConfigChan chan<- *pogs.ClientConfig
|
||||
// useConfigResultChan is a receive-only channel for Supervisor to communicate the result of applying a new ClientConfig
|
||||
useConfigResultChan <-chan *pogs.UseConfigurationResult
|
||||
// originMapper maps tunnel hostname to origin service
|
||||
tunnelHostnameMapper *tunnelhostnamemapper.TunnelHostnameMapper
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// NewStreamHandler creates a new StreamHandler
|
||||
func NewStreamHandler(newConfigChan chan<- *pogs.ClientConfig,
|
||||
useConfigResultChan <-chan *pogs.UseConfigurationResult,
|
||||
logger *logrus.Logger,
|
||||
) *StreamHandler {
|
||||
return &StreamHandler{
|
||||
newConfigChan: newConfigChan,
|
||||
useConfigResultChan: useConfigResultChan,
|
||||
tunnelHostnameMapper: tunnelhostnamemapper.NewTunnelHostnameMapper(),
|
||||
logger: logger.WithField("subsystem", "streamHandler"),
|
||||
}
|
||||
}
|
||||
|
||||
// UseConfiguration implements ClientService
|
||||
func (s *StreamHandler) UseConfiguration(ctx context.Context, config *pogs.ClientConfig) (*pogs.UseConfigurationResult, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := fmt.Errorf("Timeout while sending new config to Supervisor")
|
||||
s.logger.Error(err)
|
||||
return nil, err
|
||||
case s.newConfigChan <- config:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := fmt.Errorf("Timeout applying new configuration")
|
||||
s.logger.Error(err)
|
||||
return nil, err
|
||||
case result := <-s.useConfigResultChan:
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig replaces current originmapper mapping with mappings from newConfig
|
||||
func (s *StreamHandler) UpdateConfig(newConfig []*pogs.ReverseProxyConfig) (failedConfigs []*pogs.FailedConfig) {
|
||||
// TODO: TUN-1968: Gracefully apply new config
|
||||
s.tunnelHostnameMapper.DeleteAll()
|
||||
for _, tunnelConfig := range newConfig {
|
||||
tunnelHostname := tunnelConfig.TunnelHostname
|
||||
originSerice, err := tunnelConfig.OriginConfigUnmarshaler.OriginConfig.Service()
|
||||
if err != nil {
|
||||
s.logger.WithField("tunnelHostname", tunnelHostname).WithError(err).Error("Invalid origin service config")
|
||||
failedConfigs = append(failedConfigs, &pogs.FailedConfig{
|
||||
Config: tunnelConfig,
|
||||
Reason: tunnelConfig.FailReason(err),
|
||||
})
|
||||
continue
|
||||
}
|
||||
s.tunnelHostnameMapper.Add(tunnelConfig.TunnelHostname, originSerice)
|
||||
s.logger.WithField("tunnelHostname", tunnelHostname).Infof("New origin service config: %v", originSerice.Summary())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ServeStream implements MuxedStreamHandler interface
|
||||
func (s *StreamHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
if stream.IsRPCStream() {
|
||||
return s.serveRPC(stream)
|
||||
}
|
||||
if err := s.serveRequest(stream); err != nil {
|
||||
s.logger.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) serveRPC(stream *h2mux.MuxedStream) error {
|
||||
stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "200"}})
|
||||
main := pogs.ClientService_ServerToClient(s)
|
||||
rpcLogger := s.logger.WithField("subsystem", "clientserver-rpc")
|
||||
rpcConn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(rpcLogger, rpc.StreamTransport(stream)),
|
||||
rpc.MainInterface(main.Client),
|
||||
tunnelrpc.ConnLog(s.logger.WithField("subsystem", "clientserver-rpc-transport")),
|
||||
)
|
||||
return rpcConn.Wait()
|
||||
}
|
||||
|
||||
func (s *StreamHandler) serveRequest(stream *h2mux.MuxedStream) error {
|
||||
tunnelHostname := stream.TunnelHostname()
|
||||
if !tunnelHostname.IsSet() {
|
||||
s.writeErrorStatus(stream, statusBadRequest)
|
||||
return fmt.Errorf("stream doesn't have tunnelHostname")
|
||||
}
|
||||
|
||||
originService, ok := s.tunnelHostnameMapper.Get(tunnelHostname)
|
||||
if !ok {
|
||||
s.writeErrorStatus(stream, statusNotFound)
|
||||
return fmt.Errorf("cannot map tunnel hostname %s to origin", tunnelHostname)
|
||||
}
|
||||
|
||||
req, err := createRequest(stream, originService.URL())
|
||||
if err != nil {
|
||||
s.writeErrorStatus(stream, statusBadRequest)
|
||||
return errors.Wrap(err, "cannot create request")
|
||||
}
|
||||
|
||||
logger := s.requestLogger(req, tunnelHostname)
|
||||
logger.Debugf("Request Headers %+v", req.Header)
|
||||
|
||||
resp, err := originService.Proxy(stream, req)
|
||||
if err != nil {
|
||||
s.writeErrorStatus(stream, statusBadGateway)
|
||||
return errors.Wrap(err, "cannot proxy request")
|
||||
}
|
||||
|
||||
logger.WithField("status", resp.Status).Debugf("Response Headers %+v", resp.Header)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamHandler) requestLogger(req *http.Request, tunnelHostname h2mux.TunnelHostname) *logrus.Entry {
|
||||
cfRay := FindCfRayHeader(req)
|
||||
lbProbe := IsLBProbeRequest(req)
|
||||
logger := s.logger.WithField("tunnelHostname", tunnelHostname)
|
||||
if cfRay != "" {
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else if lbProbe {
|
||||
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else {
|
||||
logger.Warnf("Requests %v does not have CF-RAY header. Please open a support ticket with Cloudflare.", req)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func (s *StreamHandler) writeErrorStatus(stream *h2mux.MuxedStream, status *httpErrorStatus) {
|
||||
stream.WriteHeaders([]h2mux.Header{
|
||||
{
|
||||
Name: statusPseudoHeader,
|
||||
Value: status.status,
|
||||
},
|
||||
})
|
||||
stream.Write(status.text)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
testOpenStreamTimeout = time.Millisecond * 5000
|
||||
testHandshakeTimeout = time.Millisecond * 1000
|
||||
)
|
||||
|
||||
var (
|
||||
testTunnelHostname = h2mux.TunnelHostname("123.cftunnel.com")
|
||||
baseHeaders = []h2mux.Header{
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":scheme", Value: "http"},
|
||||
{Name: ":authority", Value: "example.com"},
|
||||
{Name: ":path", Value: "/"},
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{
|
||||
Name: h2mux.CloudflaredProxyTunnelHostnameHeader,
|
||||
Value: testTunnelHostname.String(),
|
||||
}
|
||||
)
|
||||
|
||||
func TestServeRequest(t *testing.T) {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
|
||||
message := []byte("Hello cloudflared")
|
||||
httpServer := httptest.NewServer(&mockHTTPHandler{message})
|
||||
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfigUnmarshaler: &pogs.OriginConfigUnmarshaler{
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: httpServer.URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusOK, stream.Headers)
|
||||
assertRespBody(t, message, stream)
|
||||
}
|
||||
|
||||
func TestServeBadRequest(t *testing.T) {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No tunnel hostname header, expect to get 400 Bad Request
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
|
||||
// No mapping for the tunnel hostname, expect to get 404 Not Found
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusNotFound, stream.Headers)
|
||||
assertRespBody(t, statusNotFound.text, stream)
|
||||
|
||||
// Nothing listening on empty url, so proxy would fail. Expect to get 502 Bad Gateway
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfigUnmarshaler: &pogs.OriginConfigUnmarshaler{
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadGateway, stream.Headers)
|
||||
assertRespBody(t, statusBadGateway.text, stream)
|
||||
|
||||
// Invalid content-length, wouldn't not be able to create a request. Expect to get 400 Bad Request
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func assertStatusHeader(t *testing.T, expectedStatus int, headers []h2mux.Header) {
|
||||
assert.Equal(t, statusPseudoHeader, headers[0].Name)
|
||||
assert.Equal(t, strconv.Itoa(expectedStatus), headers[0].Value)
|
||||
}
|
||||
|
||||
func assertRespBody(t *testing.T, expectedRespBody []byte, stream *h2mux.MuxedStream) {
|
||||
respBody := make([]byte, len(expectedRespBody))
|
||||
_, err := stream.Read(respBody)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRespBody, respBody)
|
||||
}
|
||||
|
||||
type DefaultMuxerPair struct {
|
||||
OriginMuxConfig h2mux.MuxerConfig
|
||||
OriginMux *h2mux.Muxer
|
||||
OriginConn net.Conn
|
||||
EdgeMuxConfig h2mux.MuxerConfig
|
||||
EdgeMux *h2mux.Muxer
|
||||
EdgeConn net.Conn
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewDefaultMuxerPair(t assert.TestingT, h h2mux.MuxedStreamHandler) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: h2mux.MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
Handler: h,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: logrus.NewEntry(logrus.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: h2mux.MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: logrus.NewEntry(logrus.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Handshake() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
|
||||
defer cancel()
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() (err error) {
|
||||
p.EdgeMux, err = h2mux.Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
|
||||
return errors.Wrap(err, "edge handshake failure")
|
||||
})
|
||||
errGroup.Go(func() (err error) {
|
||||
p.OriginMux, err = h2mux.Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
|
||||
return errors.Wrap(err, "origin handshake failure")
|
||||
})
|
||||
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Serve(t assert.TestingT) {
|
||||
ctx := context.Background()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
err := p.EdgeMux.Serve(ctx)
|
||||
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
|
||||
t.Errorf("error in edge muxer Serve(): %s", err)
|
||||
}
|
||||
p.OriginMux.Shutdown()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
err := p.OriginMux.Serve(ctx)
|
||||
if err != nil && err != io.EOF && err != io.ErrClosedPipe {
|
||||
t.Errorf("error in origin muxer Serve(): %s", err)
|
||||
}
|
||||
p.EdgeMux.Shutdown()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
// notify when both muxes have stopped serving
|
||||
wg.Wait()
|
||||
close(p.doneC)
|
||||
}()
|
||||
}
|
||||
|
||||
type mockHTTPHandler struct {
|
||||
message []byte
|
||||
}
|
||||
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(mth.message)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Supervisor struct {
|
||||
connManager *connection.EdgeManager
|
||||
streamHandler *streamhandler.StreamHandler
|
||||
autoupdater *updater.AutoUpdater
|
||||
supportAutoupdate bool
|
||||
newConfigChan <-chan *pogs.ClientConfig
|
||||
useConfigResultChan chan<- *pogs.UseConfigurationResult
|
||||
state *state
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func NewSupervisor(
|
||||
defaultClientConfig *pogs.ClientConfig,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer connection.EdgeServiceDiscoverer,
|
||||
cloudflaredConfig *connection.CloudflaredConfig,
|
||||
autoupdater *updater.AutoUpdater,
|
||||
supportAutoupdate bool,
|
||||
logger *logrus.Logger,
|
||||
) (*Supervisor, error) {
|
||||
newConfigChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger)
|
||||
invalidConfigs := streamHandler.UpdateConfig(defaultClientConfig.ReverseProxyConfigs)
|
||||
|
||||
if len(invalidConfigs) > 0 {
|
||||
for _, invalidConfig := range invalidConfigs {
|
||||
logger.Errorf("Tunnel %+v is invalid, reason: %s", invalidConfig.Config, invalidConfig.Reason)
|
||||
}
|
||||
return nil, fmt.Errorf("At least 1 Tunnel config is invalid")
|
||||
}
|
||||
|
||||
tunnelHostnames := make([]h2mux.TunnelHostname, len(defaultClientConfig.ReverseProxyConfigs))
|
||||
for i, reverseProxyConfig := range defaultClientConfig.ReverseProxyConfigs {
|
||||
tunnelHostnames[i] = reverseProxyConfig.TunnelHostname
|
||||
}
|
||||
defaultEdgeMgrConfigurable := &connection.EdgeManagerConfigurable{
|
||||
TunnelHostnames: tunnelHostnames,
|
||||
EdgeConnectionConfig: defaultClientConfig.EdgeConnectionConfig,
|
||||
}
|
||||
return &Supervisor{
|
||||
connManager: connection.NewEdgeManager(streamHandler, defaultEdgeMgrConfigurable, userCredential, tlsConfig,
|
||||
serviceDiscoverer, cloudflaredConfig, logger),
|
||||
streamHandler: streamHandler,
|
||||
autoupdater: autoupdater,
|
||||
supportAutoupdate: supportAutoupdate,
|
||||
newConfigChan: newConfigChan,
|
||||
useConfigResultChan: useConfigResultChan,
|
||||
state: newState(defaultClientConfig),
|
||||
logger: logger.WithField("subsystem", "supervisor"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context) error {
|
||||
errGroup, groupCtx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.connManager.Run(groupCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.listenToNewConfig(groupCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return s.listenToShutdownSignal(groupCtx)
|
||||
})
|
||||
|
||||
if s.supportAutoupdate {
|
||||
errGroup.Go(func() error {
|
||||
return s.autoupdater.Run(groupCtx)
|
||||
})
|
||||
}
|
||||
|
||||
err := errGroup.Wait()
|
||||
s.logger.Warnf("Supervisor terminated, reason: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Supervisor) listenToShutdownSignal(serveCtx context.Context) error {
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
return serveCtx.Err()
|
||||
case sig := <-signals:
|
||||
return fmt.Errorf("received %v signal", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) listenToNewConfig(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newConfig := <-s.newConfigChan:
|
||||
s.useConfigResultChan <- s.notifySubsystemsNewConfig(newConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) notifySubsystemsNewConfig(newConfig *pogs.ClientConfig) *pogs.UseConfigurationResult {
|
||||
s.logger.Infof("Received configuration %v", newConfig.Version)
|
||||
if s.state.hasAppliedVersion(newConfig.Version) {
|
||||
s.logger.Infof("%v has been applied", newConfig.Version)
|
||||
return &pogs.UseConfigurationResult{
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
s.state.updateConfig(newConfig)
|
||||
var tunnelHostnames []h2mux.TunnelHostname
|
||||
for _, tunnelConfig := range newConfig.ReverseProxyConfigs {
|
||||
tunnelHostnames = append(tunnelHostnames, tunnelConfig.TunnelHostname)
|
||||
}
|
||||
// Update connManager configurable
|
||||
s.connManager.UpdateConfigurable(&connection.EdgeManagerConfigurable{
|
||||
TunnelHostnames: tunnelHostnames,
|
||||
EdgeConnectionConfig: newConfig.EdgeConnectionConfig,
|
||||
})
|
||||
// Update streamHandler tunnelHostnameMapper mapping
|
||||
failedConfigs := s.streamHandler.UpdateConfig(newConfig.ReverseProxyConfigs)
|
||||
|
||||
if s.supportAutoupdate {
|
||||
s.autoupdater.Update(newConfig.SupervisorConfig.AutoUpdateFrequency)
|
||||
}
|
||||
|
||||
return &pogs.UseConfigurationResult{
|
||||
Success: len(failedConfigs) == 0,
|
||||
FailedConfigs: failedConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
type state struct {
|
||||
sync.RWMutex
|
||||
currentConfig *pogs.ClientConfig
|
||||
}
|
||||
|
||||
func newState(currentConfig *pogs.ClientConfig) *state {
|
||||
return &state{
|
||||
currentConfig: currentConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) hasAppliedVersion(incomingVersion pogs.Version) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.currentConfig.Version.IsNewerOrEqual(incomingVersion)
|
||||
}
|
||||
|
||||
func (s *state) updateConfig(newConfig *pogs.ClientConfig) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.currentConfig = newConfig
|
||||
}
|
||||
@@ -2,10 +2,22 @@ package tlsconfig
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
OriginCAPoolFlag = "origin-ca-pool"
|
||||
CaCertFlag = "cacert"
|
||||
)
|
||||
|
||||
// CertReloader can load and reload a TLS certificate from a particular filepath.
|
||||
@@ -51,3 +63,139 @@ func (cr *CertReloader) LoadCert() error {
|
||||
cr.certificate = &cert
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadOriginCA(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
var originCustomCAPool []byte
|
||||
|
||||
originCAPoolFilename := c.String(OriginCAPoolFlag)
|
||||
if originCAPoolFilename != "" {
|
||||
var err error
|
||||
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, OriginCAPoolFlag))
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := loadOriginCertPool(originCustomCAPool, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading the certificate pool")
|
||||
}
|
||||
|
||||
// Windows users should be notified that they can use the flag
|
||||
if runtime.GOOS == "windows" && originCAPoolFilename == "" {
|
||||
logger.Infof("cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it", OriginCAPoolFlag)
|
||||
}
|
||||
|
||||
return originCertPool, nil
|
||||
}
|
||||
|
||||
func LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) {
|
||||
// First, obtain the system certificate pool
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Next, append the Cloudflare CAs into the system pool
|
||||
cfRootCA, err := GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
|
||||
if originCAFilename == "" {
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
customOriginCA, err := ioutil.ReadFile(originCAFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s", originCAFilename))
|
||||
}
|
||||
|
||||
if !certPool.AppendCertsFromPEM(customOriginCA) {
|
||||
return nil, fmt.Errorf("error appending custom CA to cert pool")
|
||||
}
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) {
|
||||
var rootCAs []string
|
||||
if c.String(CaCertFlag) != "" {
|
||||
rootCAs = append(rootCAs, c.String(CaCertFlag))
|
||||
}
|
||||
|
||||
userConfig := &TLSParameters{RootCAs: rootCAs}
|
||||
tlsConfig, err := GetConfig(userConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tlsConfig.RootCAs == nil {
|
||||
rootCAPool := x509.NewCertPool()
|
||||
cfRootCA, err := GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
rootCAPool.AddCert(cert)
|
||||
}
|
||||
tlsConfig.RootCAs = rootCAPool
|
||||
tlsConfig.ServerName = "cftunnel.com"
|
||||
} else if edgeAddrs := c.StringSlice("edge"); len(edgeAddrs) > 0 {
|
||||
// Set for development environments and for testing specific origintunneld instances
|
||||
tlsConfig.ServerName, _, _ = net.SplitHostPort(edgeAddrs[0])
|
||||
}
|
||||
|
||||
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
|
||||
return nil, fmt.Errorf("either ServerName or InsecureSkipVerify must be specified in the tls.Config")
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func loadOriginCertPool(originCAPoolPEM []byte, logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
// Get the global pool
|
||||
certPool, err := loadGlobalCertPool(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Then, add any custom origin CA pool the user may have passed
|
||||
if originCAPoolPEM != nil {
|
||||
if !certPool.AppendCertsFromPEM(originCAPoolPEM) {
|
||||
logger.Warn("could not append the provided origin CA to the cloudflared certificate pool")
|
||||
}
|
||||
}
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func loadGlobalCertPool(logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
// First, obtain the system certificate pool
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" { // See https://github.com/golang/go/issues/16736
|
||||
logger.WithError(err).Warn("error obtaining the system certificates")
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Next, append the Cloudflare CAs into the system pool
|
||||
cfRootCA, err := GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
|
||||
// Finally, add the Hello certificate into the pool (since it's self-signed)
|
||||
helloCert, err := GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Hello server certificate to cloudflared certificate pool")
|
||||
}
|
||||
certPool.AddCert(helloCert)
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func (u *UpstreamHTTPS) exchangeWireformat(msg []byte) ([]byte, error) {
|
||||
return nil, errors.Wrap(err, "failed to create an HTTPS request")
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/dns-udpwireformat")
|
||||
req.Header.Add("Content-Type", "application/dns-message")
|
||||
req.Host = u.endpoint.Hostname()
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package tunnelhostnamemapper
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/originservice"
|
||||
)
|
||||
|
||||
// TunnelHostnameMapper maps TunnelHostname to an OriginService
|
||||
type TunnelHostnameMapper struct {
|
||||
sync.RWMutex
|
||||
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
|
||||
}
|
||||
|
||||
func NewTunnelHostnameMapper() *TunnelHostnameMapper {
|
||||
return &TunnelHostnameMapper{
|
||||
tunnelHostnameToOrigin: make(map[h2mux.TunnelHostname]originservice.OriginService),
|
||||
}
|
||||
}
|
||||
|
||||
// Get an OriginService given a TunnelHostname
|
||||
func (om *TunnelHostnameMapper) Get(key h2mux.TunnelHostname) (originservice.OriginService, bool) {
|
||||
om.RLock()
|
||||
defer om.RUnlock()
|
||||
originService, ok := om.tunnelHostnameToOrigin[key]
|
||||
return originService, ok
|
||||
}
|
||||
|
||||
// Add a mapping. If there is already an OriginService with this key, shutdown the old origin service and replace it
|
||||
// with the new one
|
||||
func (om *TunnelHostnameMapper) Add(key h2mux.TunnelHostname, os originservice.OriginService) {
|
||||
om.Lock()
|
||||
defer om.Unlock()
|
||||
if oldOS, ok := om.tunnelHostnameToOrigin[key]; ok {
|
||||
oldOS.Shutdown()
|
||||
}
|
||||
om.tunnelHostnameToOrigin[key] = os
|
||||
}
|
||||
|
||||
// DeleteAll mappings, and shutdown all OriginService
|
||||
func (om *TunnelHostnameMapper) DeleteAll() {
|
||||
om.Lock()
|
||||
defer om.Unlock()
|
||||
for key, os := range om.tunnelHostnameToOrigin {
|
||||
os.Shutdown()
|
||||
delete(om.tunnelHostnameToOrigin, key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package tunnelhostnamemapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/originservice"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
routines = 1000
|
||||
)
|
||||
|
||||
func TestTunnelHostnameMapperConcurrentAccess(t *testing.T) {
|
||||
thm := NewTunnelHostnameMapper()
|
||||
|
||||
concurrentOps(t, func(i int) {
|
||||
// om is empty
|
||||
os, ok := thm.Get(tunnelHostname(i))
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, os)
|
||||
})
|
||||
|
||||
firstURL, err := url.Parse("https://127.0.0.1:8080")
|
||||
assert.NoError(t, err)
|
||||
httpOS := originservice.NewHTTPService(http.DefaultTransport, firstURL, false)
|
||||
concurrentOps(t, func(i int) {
|
||||
thm.Add(tunnelHostname(i), httpOS)
|
||||
})
|
||||
|
||||
concurrentOps(t, func(i int) {
|
||||
os, ok := thm.Get(tunnelHostname(i))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, httpOS, os)
|
||||
})
|
||||
|
||||
secondURL, err := url.Parse("https://127.0.0.1:8080")
|
||||
assert.NoError(t, err)
|
||||
secondHTTPOS := originservice.NewHTTPService(http.DefaultTransport, secondURL, true)
|
||||
concurrentOps(t, func(i int) {
|
||||
// Add should httpOS with secondHTTPOS
|
||||
thm.Add(tunnelHostname(i), secondHTTPOS)
|
||||
})
|
||||
|
||||
concurrentOps(t, func(i int) {
|
||||
os, ok := thm.Get(tunnelHostname(i))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, secondHTTPOS, os)
|
||||
})
|
||||
|
||||
thm.DeleteAll()
|
||||
assert.Empty(t, thm.tunnelHostnameToOrigin)
|
||||
}
|
||||
|
||||
func concurrentOps(t *testing.T, f func(i int)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(routines)
|
||||
for i := 0; i < routines; i++ {
|
||||
go func(i int) {
|
||||
f(i)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func tunnelHostname(i int) h2mux.TunnelHostname {
|
||||
return h2mux.TunnelHostname(fmt.Sprintf("%d.cftunnel.com", i))
|
||||
}
|
||||
+495
-117
@@ -2,108 +2,320 @@ package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/originservice"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
"zombiezen.com/go/capnproto2/server"
|
||||
)
|
||||
|
||||
///
|
||||
/// Structs
|
||||
///
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
Timestamp time.Time
|
||||
AutoUpdateFrequency time.Duration
|
||||
MetricsUpdateFrequency time.Duration
|
||||
HeartbeatInterval time.Duration
|
||||
MaxFailedHeartbeats uint64
|
||||
GracePeriod time.Duration
|
||||
DoHProxyConfigs []*DoHProxyConfig
|
||||
ReverseProxyConfigs []*ReverseProxyConfig
|
||||
// ClientConfig is a collection of FallibleConfig that determines how cloudflared should function
|
||||
type ClientConfig struct {
|
||||
Version Version `json:"version"`
|
||||
SupervisorConfig *SupervisorConfig `json:"supervisor_config"`
|
||||
EdgeConnectionConfig *EdgeConnectionConfig `json:"edge_connection_config"`
|
||||
DoHProxyConfigs []*DoHProxyConfig `json:"doh_proxy_configs"`
|
||||
ReverseProxyConfigs []*ReverseProxyConfig `json:"reverse_proxy_configs"`
|
||||
}
|
||||
|
||||
type UseConfigurationResult struct {
|
||||
Success bool
|
||||
ErrorMessage string
|
||||
// Version type models the version of a ClientConfig
|
||||
type Version uint64
|
||||
|
||||
func InitVersion() Version {
|
||||
return Version(0)
|
||||
}
|
||||
|
||||
func (v Version) IsNewerOrEqual(comparedVersion Version) bool {
|
||||
return v >= comparedVersion
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("Version: %d", v)
|
||||
}
|
||||
|
||||
// FallibleConfig is an interface implemented by configs that cloudflared might not be able to apply
|
||||
type FallibleConfig interface {
|
||||
FailReason(err error) string
|
||||
}
|
||||
|
||||
// SupervisorConfig specifies config of components managed by Supervisor other than ConnectionManager
|
||||
type SupervisorConfig struct {
|
||||
AutoUpdateFrequency time.Duration `json:"auto_update_frequency"`
|
||||
MetricsUpdateFrequency time.Duration `json:"metrics_update_frequency"`
|
||||
GracePeriod time.Duration `json:"grace_period"`
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for SupervisorConfig
|
||||
func (sc *SupervisorConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply SupervisorConfig, err: %v", err)
|
||||
}
|
||||
|
||||
// EdgeConnectionConfig specifies what parameters and how may connections should ConnectionManager establish with edge
|
||||
type EdgeConnectionConfig struct {
|
||||
NumHAConnections uint8 `json:"num_ha_connections"`
|
||||
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
MaxFailedHeartbeats uint64 `json:"max_failed_heartbeats"`
|
||||
UserCredentialPath string `json:"user_credential_path"`
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for EdgeConnectionConfig
|
||||
func (cmc *EdgeConnectionConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply EdgeConnectionConfig, err: %v", err)
|
||||
}
|
||||
|
||||
// DoHProxyConfig is configuration for DNS over HTTPS service
|
||||
type DoHProxyConfig struct {
|
||||
ListenHost string
|
||||
ListenPort uint16
|
||||
Upstreams []string
|
||||
ListenHost string `json:"listen_host"`
|
||||
ListenPort uint16 `json:"listen_port"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for DoHProxyConfig
|
||||
func (dpc *DoHProxyConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply DoHProxyConfig, err: %v", err)
|
||||
}
|
||||
|
||||
// ReverseProxyConfig how and for what hostnames can this cloudflared proxy
|
||||
type ReverseProxyConfig struct {
|
||||
TunnelID string
|
||||
Origin OriginConfig
|
||||
Retries uint64
|
||||
ConnectionTimeout time.Duration
|
||||
ChunkedEncoding bool
|
||||
CompressionQuality uint64
|
||||
TunnelHostname h2mux.TunnelHostname `json:"tunnel_hostname"`
|
||||
OriginConfigUnmarshaler *OriginConfigUnmarshaler `json:"origin_config"`
|
||||
Retries uint64 `json:"retries"`
|
||||
ConnectionTimeout time.Duration `json:"connection_timeout"`
|
||||
CompressionQuality uint64 `json:"compression_quality"`
|
||||
}
|
||||
|
||||
func NewReverseProxyConfig(
|
||||
tunnelHostname string,
|
||||
originConfig OriginConfig,
|
||||
retries uint64,
|
||||
connectionTimeout time.Duration,
|
||||
compressionQuality uint64,
|
||||
) (*ReverseProxyConfig, error) {
|
||||
if originConfig == nil {
|
||||
return nil, fmt.Errorf("NewReverseProxyConfig: originConfigUnmarshaler was null")
|
||||
}
|
||||
return &ReverseProxyConfig{
|
||||
TunnelHostname: h2mux.TunnelHostname(tunnelHostname),
|
||||
OriginConfigUnmarshaler: &OriginConfigUnmarshaler{originConfig},
|
||||
Retries: retries,
|
||||
ConnectionTimeout: connectionTimeout,
|
||||
CompressionQuality: compressionQuality,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for ReverseProxyConfig
|
||||
func (rpc *ReverseProxyConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply ReverseProxyConfig, err: %v", err)
|
||||
}
|
||||
|
||||
//go-sumtype:decl OriginConfig
|
||||
type OriginConfig interface {
|
||||
// Service returns a OriginService used to proxy to the origin
|
||||
Service() (originservice.OriginService, error)
|
||||
// go-sumtype requires at least one unexported method, otherwise it will complain that interface is not sealed
|
||||
isOriginConfig()
|
||||
}
|
||||
|
||||
type originType int
|
||||
|
||||
const (
|
||||
httpType originType = iota
|
||||
wsType
|
||||
helloWorldType
|
||||
)
|
||||
|
||||
func (ot originType) String() string {
|
||||
switch ot {
|
||||
case httpType:
|
||||
return "Http"
|
||||
case wsType:
|
||||
return "WebSocket"
|
||||
case helloWorldType:
|
||||
return "HelloWorld"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type HTTPOriginConfig struct {
|
||||
URL string `capnp:"url"`
|
||||
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"`
|
||||
DialDualStack bool
|
||||
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"`
|
||||
TLSVerify bool `capnp:"tlsVerify"`
|
||||
OriginCAPool string
|
||||
OriginServerName string
|
||||
MaxIdleConnections uint64
|
||||
IdleConnectionTimeout time.Duration
|
||||
URLString string `capnp:"urlString" mapstructure:"url_string"`
|
||||
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive" mapstructure:"tcp_keep_alive"`
|
||||
DialDualStack bool `mapstructure:"dial_dual_stack"`
|
||||
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout" mapstructure:"tls_handshake_timeout"`
|
||||
TLSVerify bool `capnp:"tlsVerify" mapstructure:"tls_verify"`
|
||||
OriginCAPool string `mapstructure:"origin_ca_pool"`
|
||||
OriginServerName string `mapstructure:"origin_server_name"`
|
||||
MaxIdleConnections uint64 `mapstructure:"max_idle_connections"`
|
||||
IdleConnectionTimeout time.Duration `mapstructure:"idle_connection_timeout"`
|
||||
ProxyConnectionTimeout time.Duration `mapstructure:"proxy_connection_timeout"`
|
||||
ExpectContinueTimeout time.Duration `mapstructure:"expect_continue_timeout"`
|
||||
ChunkedEncoding bool `mapstructure:"chunked_encoding"`
|
||||
}
|
||||
|
||||
func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
|
||||
rootCAs, err := tlsconfig.LoadCustomOriginCA(hc.OriginCAPool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialContext := (&net.Dialer{
|
||||
Timeout: hc.ProxyConnectionTimeout,
|
||||
KeepAlive: hc.TCPKeepAlive,
|
||||
DualStack: hc.DialDualStack,
|
||||
}).DialContext
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialContext,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
ServerName: hc.OriginServerName,
|
||||
InsecureSkipVerify: hc.TLSVerify,
|
||||
},
|
||||
TLSHandshakeTimeout: hc.TLSHandshakeTimeout,
|
||||
MaxIdleConns: int(hc.MaxIdleConnections),
|
||||
IdleConnTimeout: hc.IdleConnectionTimeout,
|
||||
ExpectContinueTimeout: hc.ExpectContinueTimeout,
|
||||
}
|
||||
url, err := url.Parse(hc.URLString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "%s is not a valid URL", hc.URLString)
|
||||
}
|
||||
if url.Scheme == "unix" {
|
||||
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialContext(ctx, "unix", url.Host)
|
||||
}
|
||||
}
|
||||
return originservice.NewHTTPService(transport, url, hc.ChunkedEncoding), nil
|
||||
}
|
||||
|
||||
func (_ *HTTPOriginConfig) isOriginConfig() {}
|
||||
|
||||
type UnixSocketOriginConfig struct {
|
||||
Path string
|
||||
type WebSocketOriginConfig struct {
|
||||
URLString string `capnp:"urlString" mapstructure:"url_string"`
|
||||
TLSVerify bool `capnp:"tlsVerify" mapstructure:"tls_verify"`
|
||||
OriginCAPool string `mapstructure:"origin_ca_pool"`
|
||||
OriginServerName string `mapstructure:"origin_server_name"`
|
||||
}
|
||||
|
||||
func (_ *UnixSocketOriginConfig) isOriginConfig() {}
|
||||
func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error) {
|
||||
rootCAs, err := tlsconfig.LoadCustomOriginCA(wsc.OriginCAPool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
ServerName: wsc.OriginServerName,
|
||||
InsecureSkipVerify: wsc.TLSVerify,
|
||||
}
|
||||
|
||||
type WebSocketOriginConfig struct {
|
||||
URL string `capnp:"url"`
|
||||
url, err := url.Parse(wsc.URLString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "%s is not a valid URL", wsc.URLString)
|
||||
}
|
||||
return originservice.NewWebSocketService(tlsConfig, url)
|
||||
}
|
||||
|
||||
func (_ *WebSocketOriginConfig) isOriginConfig() {}
|
||||
|
||||
type HelloWorldOriginConfig struct{}
|
||||
|
||||
func (_ *HelloWorldOriginConfig) Service() (originservice.OriginService, error) {
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get Hello World server certificate")
|
||||
}
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(helloCert)
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
},
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
return originservice.NewHelloWorldService(transport)
|
||||
}
|
||||
|
||||
func (_ *HelloWorldOriginConfig) isOriginConfig() {}
|
||||
|
||||
///
|
||||
/// Boilerplate to convert between these structs and the primitive structs generated by capnp-go
|
||||
///
|
||||
/*
|
||||
* Boilerplate to convert between these structs and the primitive structs
|
||||
* generated by capnp-go.
|
||||
* Mnemonics for variable names in this section:
|
||||
* - `p` is for POGS (plain old Go struct)
|
||||
* - `s` (and `ss`) is for "capnp.Struct", which is the fundamental type
|
||||
* underlying the capnp-go data structures.
|
||||
*/
|
||||
|
||||
func MarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig, p *CloudflaredConfig) error {
|
||||
s.SetTimestamp(p.Timestamp.UnixNano())
|
||||
s.SetAutoUpdateFrequency(p.AutoUpdateFrequency.Nanoseconds())
|
||||
s.SetMetricsUpdateFrequency(p.MetricsUpdateFrequency.Nanoseconds())
|
||||
s.SetHeartbeatInterval(p.HeartbeatInterval.Nanoseconds())
|
||||
s.SetMaxFailedHeartbeats(p.MaxFailedHeartbeats)
|
||||
s.SetGracePeriod(p.GracePeriod.Nanoseconds())
|
||||
err := marshalDoHProxyConfigs(s, p.DoHProxyConfigs)
|
||||
func MarshalClientConfig(s tunnelrpc.ClientConfig, p *ClientConfig) error {
|
||||
s.SetVersion(uint64(p.Version))
|
||||
|
||||
supervisorConfig, err := s.NewSupervisorConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to get SupervisorConfig")
|
||||
}
|
||||
err = marshalReverseProxyConfigs(s, p.ReverseProxyConfigs)
|
||||
if err = MarshalSupervisorConfig(supervisorConfig, p.SupervisorConfig); err != nil {
|
||||
return errors.Wrap(err, "MarshalSupervisorConfig error")
|
||||
}
|
||||
|
||||
edgeConnectionConfig, err := s.NewEdgeConnectionConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to get EdgeConnectionConfig")
|
||||
}
|
||||
if err := MarshalEdgeConnectionConfig(edgeConnectionConfig, p.EdgeConnectionConfig); err != nil {
|
||||
return errors.Wrap(err, "MarshalEdgeConnectionConfig error")
|
||||
}
|
||||
|
||||
if err := marshalDoHProxyConfigs(s, p.DoHProxyConfigs); err != nil {
|
||||
return errors.Wrap(err, "marshalDoHProxyConfigs error")
|
||||
}
|
||||
if err := marshalReverseProxyConfigs(s, p.ReverseProxyConfigs); err != nil {
|
||||
return errors.Wrap(err, "marshalReverseProxyConfigs error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig, dohProxyConfigs []*DoHProxyConfig) error {
|
||||
func MarshalSupervisorConfig(s tunnelrpc.SupervisorConfig, p *SupervisorConfig) error {
|
||||
if err := pogs.Insert(tunnelrpc.SupervisorConfig_TypeID, s.Struct, p); err != nil {
|
||||
return errors.Wrap(err, "failed to insert SupervisorConfig")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MarshalEdgeConnectionConfig(s tunnelrpc.EdgeConnectionConfig, p *EdgeConnectionConfig) error {
|
||||
if err := pogs.Insert(tunnelrpc.EdgeConnectionConfig_TypeID, s.Struct, p); err != nil {
|
||||
return errors.Wrap(err, "failed to insert EdgeConnectionConfig")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalDoHProxyConfigs(s tunnelrpc.ClientConfig, dohProxyConfigs []*DoHProxyConfig) error {
|
||||
capnpList, err := s.NewDohProxyConfigs(int32(len(dohProxyConfigs)))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -117,7 +329,7 @@ func marshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig, dohProxyConfigs []*Do
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig, reverseProxyConfigs []*ReverseProxyConfig) error {
|
||||
func marshalReverseProxyConfigs(s tunnelrpc.ClientConfig, reverseProxyConfigs []*ReverseProxyConfig) error {
|
||||
capnpList, err := s.NewReverseProxyConfigs(int32(len(reverseProxyConfigs)))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -131,28 +343,54 @@ func marshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig, reverseProxyConfi
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig) (*CloudflaredConfig, error) {
|
||||
p := new(CloudflaredConfig)
|
||||
p.Timestamp = time.Unix(0, s.Timestamp()).UTC()
|
||||
p.AutoUpdateFrequency = time.Duration(s.AutoUpdateFrequency())
|
||||
p.MetricsUpdateFrequency = time.Duration(s.MetricsUpdateFrequency())
|
||||
p.HeartbeatInterval = time.Duration(s.HeartbeatInterval())
|
||||
p.MaxFailedHeartbeats = s.MaxFailedHeartbeats()
|
||||
p.GracePeriod = time.Duration(s.GracePeriod())
|
||||
dohProxyConfigs, err := unmarshalDoHProxyConfigs(s)
|
||||
func UnmarshalClientConfig(s tunnelrpc.ClientConfig) (*ClientConfig, error) {
|
||||
p := new(ClientConfig)
|
||||
p.Version = Version(s.Version())
|
||||
|
||||
supervisorConfig, err := s.SupervisorConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "failed to get SupervisorConfig")
|
||||
}
|
||||
p.DoHProxyConfigs = dohProxyConfigs
|
||||
reverseProxyConfigs, err := unmarshalReverseProxyConfigs(s)
|
||||
p.SupervisorConfig, err = UnmarshalSupervisorConfig(supervisorConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "UnmarshalSupervisorConfig error")
|
||||
}
|
||||
p.ReverseProxyConfigs = reverseProxyConfigs
|
||||
|
||||
edgeConnectionConfig, err := s.EdgeConnectionConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get ConnectionManagerConfig")
|
||||
}
|
||||
p.EdgeConnectionConfig, err = UnmarshalEdgeConnectionConfig(edgeConnectionConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "UnmarshalConnectionManagerConfig error")
|
||||
}
|
||||
|
||||
p.DoHProxyConfigs, err = unmarshalDoHProxyConfigs(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshalDoHProxyConfigs error")
|
||||
}
|
||||
|
||||
p.ReverseProxyConfigs, err = unmarshalReverseProxyConfigs(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unmarshalReverseProxyConfigs error")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func UnmarshalSupervisorConfig(s tunnelrpc.SupervisorConfig) (*SupervisorConfig, error) {
|
||||
p := new(SupervisorConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.SupervisorConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func unmarshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*DoHProxyConfig, error) {
|
||||
func UnmarshalEdgeConnectionConfig(s tunnelrpc.EdgeConnectionConfig) (*EdgeConnectionConfig, error) {
|
||||
p := new(EdgeConnectionConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.EdgeConnectionConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func unmarshalDoHProxyConfigs(s tunnelrpc.ClientConfig) ([]*DoHProxyConfig, error) {
|
||||
var result []*DoHProxyConfig
|
||||
marshalledDoHProxyConfigs, err := s.DohProxyConfigs()
|
||||
if err != nil {
|
||||
@@ -169,7 +407,7 @@ func unmarshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*DoHProxyConfig,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func unmarshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*ReverseProxyConfig, error) {
|
||||
func unmarshalReverseProxyConfigs(s tunnelrpc.ClientConfig) ([]*ReverseProxyConfig, error) {
|
||||
var result []*ReverseProxyConfig
|
||||
marshalledReverseProxyConfigs, err := s.ReverseProxyConfigs()
|
||||
if err != nil {
|
||||
@@ -187,13 +425,38 @@ func unmarshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*ReverseProx
|
||||
}
|
||||
|
||||
func MarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult, p *UseConfigurationResult) error {
|
||||
return pogs.Insert(tunnelrpc.UseConfigurationResult_TypeID, s.Struct, p)
|
||||
capnpList, err := s.NewFailedConfigs(int32(len(p.FailedConfigs)))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot create new FailedConfigs")
|
||||
}
|
||||
for i, unmarshalledFailedConfig := range p.FailedConfigs {
|
||||
err := MarshalFailedConfig(capnpList.At(i), unmarshalledFailedConfig)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Cannot MarshalFailedConfig at index %d", i)
|
||||
}
|
||||
}
|
||||
s.SetSuccess(p.Success)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult) (*UseConfigurationResult, error) {
|
||||
p := new(UseConfigurationResult)
|
||||
err := pogs.Extract(p, tunnelrpc.UseConfigurationResult_TypeID, s.Struct)
|
||||
return p, err
|
||||
var failedConfigs []*FailedConfig
|
||||
marshalledFailedConfigs, err := s.FailedConfigs()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get FailedConfigs")
|
||||
}
|
||||
for i := 0; i < marshalledFailedConfigs.Len(); i++ {
|
||||
ss := marshalledFailedConfigs.At(i)
|
||||
failedConfig, err := UnmarshalFailedConfig(ss)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Cannot UnmarshalFailedConfig at index %d", i)
|
||||
}
|
||||
failedConfigs = append(failedConfigs, failedConfig)
|
||||
}
|
||||
p.FailedConfigs = failedConfigs
|
||||
p.Success = s.Success()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func MarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig, p *DoHProxyConfig) error {
|
||||
@@ -207,49 +470,48 @@ func UnmarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig) (*DoHProxyConfig, error
|
||||
}
|
||||
|
||||
func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyConfig) error {
|
||||
s.SetTunnelID(p.TunnelID)
|
||||
switch config := p.Origin.(type) {
|
||||
s.SetTunnelHostname(p.TunnelHostname.String())
|
||||
switch config := p.OriginConfigUnmarshaler.OriginConfig.(type) {
|
||||
case *HTTPOriginConfig:
|
||||
ss, err := s.Origin().NewHttp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalHTTPOriginConfig(ss, config)
|
||||
case *UnixSocketOriginConfig:
|
||||
ss, err := s.Origin().NewSocket()
|
||||
if err != nil {
|
||||
if err := MarshalHTTPOriginConfig(ss, config); err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalUnixSocketOriginConfig(ss, config)
|
||||
case *WebSocketOriginConfig:
|
||||
ss, err := s.Origin().NewWebsocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalWebSocketOriginConfig(ss, config)
|
||||
if err := MarshalWebSocketOriginConfig(ss, config); err != nil {
|
||||
return err
|
||||
}
|
||||
case *HelloWorldOriginConfig:
|
||||
ss, err := s.Origin().NewHelloWorld()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalHelloWorldOriginConfig(ss, config)
|
||||
if err := MarshalHelloWorldOriginConfig(ss, config); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown type for config: %T", config)
|
||||
}
|
||||
s.SetRetries(p.Retries)
|
||||
s.SetConnectionTimeout(p.ConnectionTimeout.Nanoseconds())
|
||||
s.SetChunkedEncoding(p.ChunkedEncoding)
|
||||
s.SetCompressionQuality(p.CompressionQuality)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyConfig, error) {
|
||||
p := new(ReverseProxyConfig)
|
||||
tunnelID, err := s.TunnelID()
|
||||
tunnelHostname, err := s.TunnelHostname()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.TunnelID = tunnelID
|
||||
p.TunnelHostname = h2mux.TunnelHostname(tunnelHostname)
|
||||
switch s.Origin().Which() {
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_http:
|
||||
ss, err := s.Origin().Http()
|
||||
@@ -260,17 +522,7 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_socket:
|
||||
ss, err := s.Origin().Socket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := UnmarshalUnixSocketOriginConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
p.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{config}
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_websocket:
|
||||
ss, err := s.Origin().Websocket()
|
||||
if err != nil {
|
||||
@@ -280,7 +532,7 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
p.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{config}
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_helloWorld:
|
||||
ss, err := s.Origin().HelloWorld()
|
||||
if err != nil {
|
||||
@@ -290,11 +542,10 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
p.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{config}
|
||||
}
|
||||
p.Retries = s.Retries()
|
||||
p.ConnectionTimeout = time.Duration(s.ConnectionTimeout())
|
||||
p.ChunkedEncoding = s.ChunkedEncoding()
|
||||
p.CompressionQuality = s.CompressionQuality()
|
||||
return p, nil
|
||||
}
|
||||
@@ -309,16 +560,6 @@ func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig,
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig, p *UnixSocketOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig) (*UnixSocketOriginConfig, error) {
|
||||
p := new(UnixSocketOriginConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig, p *WebSocketOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
@@ -339,31 +580,30 @@ func UnmarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig) (*Hello
|
||||
return p, err
|
||||
}
|
||||
|
||||
type CloudflaredServer interface {
|
||||
UseConfiguration(ctx context.Context, config *CloudflaredConfig) (*CloudflaredConfig, error)
|
||||
GetConfiguration(ctx context.Context) (*CloudflaredConfig, error)
|
||||
type ClientService interface {
|
||||
UseConfiguration(ctx context.Context, config *ClientConfig) (*UseConfigurationResult, error)
|
||||
}
|
||||
|
||||
type CloudflaredServer_PogsClient struct {
|
||||
type ClientService_PogsClient struct {
|
||||
Client capnp.Client
|
||||
Conn *rpc.Conn
|
||||
}
|
||||
|
||||
func (c *CloudflaredServer_PogsClient) Close() error {
|
||||
func (c *ClientService_PogsClient) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *CloudflaredServer_PogsClient) UseConfiguration(
|
||||
func (c *ClientService_PogsClient) UseConfiguration(
|
||||
ctx context.Context,
|
||||
config *CloudflaredConfig,
|
||||
config *ClientConfig,
|
||||
) (*UseConfigurationResult, error) {
|
||||
client := tunnelrpc.CloudflaredServer{Client: c.Client}
|
||||
promise := client.UseConfiguration(ctx, func(p tunnelrpc.CloudflaredServer_useConfiguration_Params) error {
|
||||
cloudflaredConfig, err := p.NewCloudflaredConfig()
|
||||
client := tunnelrpc.ClientService{Client: c.Client}
|
||||
promise := client.UseConfiguration(ctx, func(p tunnelrpc.ClientService_useConfiguration_Params) error {
|
||||
clientServiceConfig, err := p.NewClientServiceConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalCloudflaredConfig(cloudflaredConfig, config)
|
||||
return MarshalClientConfig(clientServiceConfig, config)
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
@@ -371,3 +611,141 @@ func (c *CloudflaredServer_PogsClient) UseConfiguration(
|
||||
}
|
||||
return UnmarshalUseConfigurationResult(retval)
|
||||
}
|
||||
|
||||
func ClientService_ServerToClient(s ClientService) tunnelrpc.ClientService {
|
||||
return tunnelrpc.ClientService_ServerToClient(ClientService_PogsImpl{s})
|
||||
}
|
||||
|
||||
type ClientService_PogsImpl struct {
|
||||
impl ClientService
|
||||
}
|
||||
|
||||
func (i ClientService_PogsImpl) UseConfiguration(p tunnelrpc.ClientService_useConfiguration) error {
|
||||
config, err := p.Params.ClientServiceConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot get CloudflaredConfig parameter")
|
||||
}
|
||||
pogsConfig, err := UnmarshalClientConfig(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot unmarshal tunnelrpc.CloudflaredConfig to *CloudflaredConfig")
|
||||
}
|
||||
server.Ack(p.Options)
|
||||
userConfigResult, err := i.impl.UseConfiguration(p.Ctx, pogsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := p.Results.NewResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalUseConfigurationResult(result, userConfigResult)
|
||||
}
|
||||
|
||||
type UseConfigurationResult struct {
|
||||
Success bool
|
||||
FailedConfigs []*FailedConfig
|
||||
}
|
||||
|
||||
type FailedConfig struct {
|
||||
Config FallibleConfig
|
||||
Reason string
|
||||
}
|
||||
|
||||
func MarshalFailedConfig(s tunnelrpc.FailedConfig, p *FailedConfig) error {
|
||||
switch config := p.Config.(type) {
|
||||
case *SupervisorConfig:
|
||||
ss, err := s.Config().NewSupervisor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalSupervisorConfig(ss, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *EdgeConnectionConfig:
|
||||
ss, err := s.Config().EdgeConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalEdgeConnectionConfig(ss, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *DoHProxyConfig:
|
||||
ss, err := s.Config().NewDoh()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalDoHProxyConfig(ss, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *ReverseProxyConfig:
|
||||
ss, err := s.Config().NewReverseProxy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalReverseProxyConfig(ss, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown type for Config: %T", config)
|
||||
}
|
||||
s.SetReason(p.Reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalFailedConfig(s tunnelrpc.FailedConfig) (*FailedConfig, error) {
|
||||
p := new(FailedConfig)
|
||||
switch s.Config().Which() {
|
||||
case tunnelrpc.FailedConfig_config_Which_supervisor:
|
||||
ss, err := s.Config().Supervisor()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get SupervisorConfig from Config")
|
||||
}
|
||||
config, err := UnmarshalSupervisorConfig(ss)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot UnmarshalSupervisorConfig")
|
||||
}
|
||||
p.Config = config
|
||||
case tunnelrpc.FailedConfig_config_Which_edgeConnection:
|
||||
ss, err := s.Config().EdgeConnection()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get ConnectionManager from Config")
|
||||
}
|
||||
config, err := UnmarshalEdgeConnectionConfig(ss)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot UnmarshalConnectionManagerConfig")
|
||||
}
|
||||
p.Config = config
|
||||
case tunnelrpc.FailedConfig_config_Which_doh:
|
||||
ss, err := s.Config().Doh()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get Doh from Config")
|
||||
}
|
||||
config, err := UnmarshalDoHProxyConfig(ss)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot UnmarshalDoHProxyConfig")
|
||||
}
|
||||
p.Config = config
|
||||
case tunnelrpc.FailedConfig_config_Which_reverseProxy:
|
||||
ss, err := s.Config().ReverseProxy()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get ReverseProxy from Config")
|
||||
}
|
||||
config, err := UnmarshalReverseProxyConfig(ss)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot UnmarshalReverseProxyConfig")
|
||||
}
|
||||
p.Config = config
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown type for FailedConfig: %v", s.Config().Which())
|
||||
}
|
||||
reason, err := s.Reason()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get Reason")
|
||||
}
|
||||
p.Reason = reason
|
||||
return p, nil
|
||||
}
|
||||
|
||||
+193
-71
@@ -1,6 +1,8 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -10,44 +12,63 @@ import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
func TestCloudflaredConfig(t *testing.T) {
|
||||
addDoHProxyConfigs := func(c *CloudflaredConfig) {
|
||||
// Assert *HTTPOriginConfig implements OriginConfig
|
||||
var _ OriginConfig = (*HTTPOriginConfig)(nil)
|
||||
|
||||
// Assert *WebSocketOriginConfig implements OriginConfig
|
||||
var _ OriginConfig = (*WebSocketOriginConfig)(nil)
|
||||
|
||||
// Assert *HelloWorldOriginConfig implements OriginConfig
|
||||
var _ OriginConfig = (*HelloWorldOriginConfig)(nil)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
firstVersion := InitVersion()
|
||||
secondVersion := Version(1)
|
||||
assert.False(t, firstVersion.IsNewerOrEqual(secondVersion))
|
||||
assert.True(t, secondVersion.IsNewerOrEqual(firstVersion))
|
||||
assert.True(t, secondVersion.IsNewerOrEqual(secondVersion))
|
||||
}
|
||||
|
||||
func TestClientConfig(t *testing.T) {
|
||||
addDoHProxyConfigs := func(c *ClientConfig) {
|
||||
c.DoHProxyConfigs = []*DoHProxyConfig{
|
||||
sampleDoHProxyConfig(),
|
||||
}
|
||||
}
|
||||
addReverseProxyConfigs := func(c *CloudflaredConfig) {
|
||||
addReverseProxyConfigs := func(c *ClientConfig) {
|
||||
c.ReverseProxyConfigs = []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleHTTPOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleUnixSocketOriginConfig()
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleHTTPOriginConfig()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleWebSocketOriginConfig()
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleHTTPOriginConfigUnixPath()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleWebSocketOriginConfig()}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []*CloudflaredConfig{
|
||||
sampleCloudflaredConfig(),
|
||||
sampleCloudflaredConfig(addDoHProxyConfigs),
|
||||
sampleCloudflaredConfig(addReverseProxyConfigs),
|
||||
sampleCloudflaredConfig(addDoHProxyConfigs, addReverseProxyConfigs),
|
||||
testCases := []*ClientConfig{
|
||||
sampleClientConfig(),
|
||||
sampleClientConfig(addDoHProxyConfigs),
|
||||
sampleClientConfig(addReverseProxyConfigs),
|
||||
sampleClientConfig(addDoHProxyConfigs, addReverseProxyConfigs),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewCloudflaredConfig(seg)
|
||||
capnpEntity, err := tunnelrpc.NewClientConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalCloudflaredConfig(capnpEntity, testCase)
|
||||
err = MarshalClientConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalCloudflaredConfig(capnpEntity)
|
||||
result, err := UnmarshalClientConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
@@ -61,8 +82,17 @@ func TestUseConfigurationResult(t *testing.T) {
|
||||
Success: true,
|
||||
},
|
||||
&UseConfigurationResult{
|
||||
Success: false,
|
||||
ErrorMessage: "the quick brown fox jumped over the lazy dogs",
|
||||
Success: false,
|
||||
FailedConfigs: []*FailedConfig{
|
||||
{
|
||||
Config: sampleReverseProxyConfig(),
|
||||
Reason: "Invalid certificate",
|
||||
},
|
||||
{
|
||||
Config: sampleDoHProxyConfig(),
|
||||
Reason: "Cannot listen on port 53",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
@@ -112,13 +142,13 @@ func TestReverseProxyConfig(t *testing.T) {
|
||||
testCases := []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleHTTPOriginConfig()
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleHTTPOriginConfig()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleUnixSocketOriginConfig()
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleHTTPOriginConfigUnixPath()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleWebSocketOriginConfig()
|
||||
c.OriginConfigUnmarshaler = &OriginConfigUnmarshaler{sampleWebSocketOriginConfig()}
|
||||
}),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
@@ -161,28 +191,6 @@ func TestHTTPOriginConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixSocketOriginConfig(t *testing.T) {
|
||||
testCases := []*UnixSocketOriginConfig{
|
||||
sampleUnixSocketOriginConfig(),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewUnixSocketOriginConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalUnixSocketOriginConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalUnixSocketOriginConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketOriginConfig(t *testing.T) {
|
||||
testCases := []*WebSocketOriginConfig{
|
||||
sampleWebSocketOriginConfig(),
|
||||
@@ -205,48 +213,85 @@ func TestWebSocketOriginConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginConfigInvalidURL(t *testing.T) {
|
||||
invalidConfigs := []OriginConfig{
|
||||
&HTTPOriginConfig{
|
||||
// this url doesn't have a scheme
|
||||
URLString: "127.0.0.1:36192",
|
||||
},
|
||||
&WebSocketOriginConfig{
|
||||
URLString: "127.0.0.1:36192",
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range invalidConfigs {
|
||||
service, err := config.Service()
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, service)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Functions to generate sample data for ease of testing
|
||||
//
|
||||
// There's one "sample" function per struct type. Each goes like this:
|
||||
// 1. Initialize an instance of the relevant struct.
|
||||
// 2. Ensure the instance has no zero-valued fields. (This catches the
|
||||
// error-case where a field was added, but we forgot to add code to
|
||||
// marshal/unmarshal this field in CapnProto.)
|
||||
// 3. Apply one or more "override" functions (which accept a
|
||||
// pointer-to-struct, so they can mutate the instance).
|
||||
|
||||
func sampleCloudflaredConfig(overrides ...func(*CloudflaredConfig)) *CloudflaredConfig {
|
||||
// strip the location and monotonic clock reading so that assert.Equals()
|
||||
// will work correctly
|
||||
now := time.Now().UTC().Round(0)
|
||||
sample := &CloudflaredConfig{
|
||||
Timestamp: now,
|
||||
AutoUpdateFrequency: 21 * time.Hour,
|
||||
MetricsUpdateFrequency: 11 * time.Minute,
|
||||
HeartbeatInterval: 5 * time.Second,
|
||||
MaxFailedHeartbeats: 9001,
|
||||
GracePeriod: 31 * time.Second,
|
||||
// sampleClientConfig initializes a new ClientConfig literal,
|
||||
// applies any number of overrides to it, and returns it.
|
||||
func sampleClientConfig(overrides ...func(*ClientConfig)) *ClientConfig {
|
||||
sample := &ClientConfig{
|
||||
Version: Version(1337),
|
||||
SupervisorConfig: &SupervisorConfig{
|
||||
AutoUpdateFrequency: 21 * time.Hour,
|
||||
MetricsUpdateFrequency: 11 * time.Minute,
|
||||
GracePeriod: 31 * time.Second,
|
||||
},
|
||||
EdgeConnectionConfig: &EdgeConnectionConfig{
|
||||
NumHAConnections: 49,
|
||||
Timeout: 9 * time.Second,
|
||||
HeartbeatInterval: 5 * time.Second,
|
||||
MaxFailedHeartbeats: 9001,
|
||||
},
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
// sampleDoHProxyConfig initializes a new DoHProxyConfig struct,
|
||||
// applies any number of overrides to it, and returns it.
|
||||
func sampleDoHProxyConfig(overrides ...func(*DoHProxyConfig)) *DoHProxyConfig {
|
||||
sample := &DoHProxyConfig{
|
||||
ListenHost: "127.0.0.1",
|
||||
ListenPort: 53,
|
||||
Upstreams: []string{"https://1.example.com", "https://2.example.com"},
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
// sampleReverseProxyConfig initializes a new ReverseProxyConfig struct,
|
||||
// applies any number of overrides to it, and returns it.
|
||||
func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReverseProxyConfig {
|
||||
sample := &ReverseProxyConfig{
|
||||
TunnelID: "hijk",
|
||||
Origin: &HelloWorldOriginConfig{},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
ChunkedEncoding: false,
|
||||
CompressionQuality: 4,
|
||||
TunnelHostname: "hijk.example.com",
|
||||
OriginConfigUnmarshaler: &OriginConfigUnmarshaler{&HelloWorldOriginConfig{}},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
CompressionQuality: 4,
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
@@ -255,26 +300,42 @@ func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReversePr
|
||||
|
||||
func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
|
||||
sample := &HTTPOriginConfig{
|
||||
URL: "https://example.com",
|
||||
TCPKeepAlive: 7 * time.Second,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: 11 * time.Second,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "/etc/cert.pem",
|
||||
OriginServerName: "secure.example.com",
|
||||
MaxIdleConnections: 19,
|
||||
IdleConnectionTimeout: 17 * time.Second,
|
||||
URLString: "https.example.com",
|
||||
TCPKeepAlive: 7 * time.Second,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: 11 * time.Second,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "/etc/cert.pem",
|
||||
OriginServerName: "secure.example.com",
|
||||
MaxIdleConnections: 19,
|
||||
IdleConnectionTimeout: 17 * time.Second,
|
||||
ProxyConnectionTimeout: 15 * time.Second,
|
||||
ExpectContinueTimeout: 21 * time.Second,
|
||||
ChunkedEncoding: true,
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleUnixSocketOriginConfig(overrides ...func(*UnixSocketOriginConfig)) *UnixSocketOriginConfig {
|
||||
sample := &UnixSocketOriginConfig{
|
||||
Path: "/var/lib/file.sock",
|
||||
func sampleHTTPOriginConfigUnixPath(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
|
||||
sample := &HTTPOriginConfig{
|
||||
URLString: "unix:/var/lib/file.sock",
|
||||
TCPKeepAlive: 7 * time.Second,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: 11 * time.Second,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "/etc/cert.pem",
|
||||
OriginServerName: "secure.example.com",
|
||||
MaxIdleConnections: 19,
|
||||
IdleConnectionTimeout: 17 * time.Second,
|
||||
ProxyConnectionTimeout: 15 * time.Second,
|
||||
ExpectContinueTimeout: 21 * time.Second,
|
||||
ChunkedEncoding: true,
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
@@ -283,10 +344,71 @@ func sampleUnixSocketOriginConfig(overrides ...func(*UnixSocketOriginConfig)) *U
|
||||
|
||||
func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig {
|
||||
sample := &WebSocketOriginConfig{
|
||||
URL: "ssh://example.com",
|
||||
URLString: "ssh://example.com",
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "/etc/cert.pem",
|
||||
OriginServerName: "secure.example.com",
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func (c *ClientConfig) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{"DoHProxyConfigs", "ReverseProxyConfigs"})
|
||||
}
|
||||
|
||||
func (c *DoHProxyConfig) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
|
||||
}
|
||||
|
||||
func (c *ReverseProxyConfig) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
|
||||
}
|
||||
|
||||
func (c *HTTPOriginConfig) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
|
||||
}
|
||||
|
||||
func (c *WebSocketOriginConfig) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
|
||||
}
|
||||
|
||||
// ensureNoZeroFieldsInSample checks that all fields in the sample struct,
|
||||
// except those listed in `allowedZeroFieldNames`, are initialized to nonzero
|
||||
// values. Note that the value has to be a pointer for reflection to work
|
||||
// correctly:
|
||||
// e := &ExampleStruct{ ... }
|
||||
// ensureNoZeroFieldsInSample(reflect.ValueOf(e), []string{})
|
||||
//
|
||||
// Context:
|
||||
// Our tests work by taking a sample struct and marshalling/unmarshalling it.
|
||||
// This makes them easy to write, but introduces some risk: if we don't
|
||||
// include a field in the sample value, it won't be covered under tests.
|
||||
// This check reduces that risk by requiring fields to be either initialized
|
||||
// or explicitly uninitialized.
|
||||
func ensureNoZeroFieldsInSample(ptrToSampleValue reflect.Value, allowedZeroFieldNames []string) {
|
||||
sampleValue := ptrToSampleValue.Elem()
|
||||
structType := ptrToSampleValue.Type().Elem()
|
||||
|
||||
allowedZeroFieldSet := make(map[string]bool)
|
||||
for _, name := range allowedZeroFieldNames {
|
||||
if _, ok := structType.FieldByName(name); !ok {
|
||||
panic(fmt.Sprintf("struct %v has no field %v", structType.Name(), name))
|
||||
}
|
||||
allowedZeroFieldSet[name] = true
|
||||
}
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
if allowedZeroFieldSet[structType.Field(i).Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
zeroValue := reflect.Zero(structType.Field(i).Type)
|
||||
if reflect.DeepEqual(zeroValue.Interface(), sampleValue.Field(i).Interface()) {
|
||||
panic(fmt.Sprintf("In the sample value for struct %v, field %v was not initialized", structType.Name(), structType.Field(i).Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+142
-21
@@ -2,6 +2,7 @@ package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
@@ -127,52 +128,172 @@ func UnmarshalServerInfo(s tunnelrpc.ServerInfo) (*ServerInfo, error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
//go-sumtype:decl Scope
|
||||
type Scope interface {
|
||||
Value() string
|
||||
PostgresType() string
|
||||
isScope()
|
||||
}
|
||||
|
||||
type SystemName struct {
|
||||
systemName string
|
||||
}
|
||||
|
||||
func NewSystemName(systemName string) *SystemName {
|
||||
return &SystemName{systemName: systemName}
|
||||
}
|
||||
|
||||
func (s *SystemName) Value() string { return s.systemName }
|
||||
func (s *SystemName) PostgresType() string { return "system_name" }
|
||||
|
||||
func (_ *SystemName) isScope() {}
|
||||
|
||||
type Group struct {
|
||||
group string
|
||||
}
|
||||
|
||||
func NewGroup(group string) *Group {
|
||||
return &Group{group: group}
|
||||
}
|
||||
|
||||
func (g *Group) Value() string { return g.group }
|
||||
func (g *Group) PostgresType() string { return "group" }
|
||||
|
||||
func (_ *Group) isScope() {}
|
||||
|
||||
func MarshalScope(s tunnelrpc.Scope, p Scope) error {
|
||||
ss := s.Value()
|
||||
switch scope := p.(type) {
|
||||
case *SystemName:
|
||||
ss.SetSystemName(scope.systemName)
|
||||
case *Group:
|
||||
ss.SetGroup(scope.group)
|
||||
default:
|
||||
return fmt.Errorf("unexpected Scope value: %v", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalScope(s tunnelrpc.Scope) (Scope, error) {
|
||||
ss := s.Value()
|
||||
switch ss.Which() {
|
||||
case tunnelrpc.Scope_value_Which_systemName:
|
||||
systemName, err := ss.SystemName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewSystemName(systemName), nil
|
||||
case tunnelrpc.Scope_value_Which_group:
|
||||
group, err := ss.Group()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewGroup(group), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected Scope tag: %v", ss.Which())
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectParameters struct {
|
||||
OriginCert []byte
|
||||
CloudflaredID uuid.UUID
|
||||
NumPreviousAttempts uint8
|
||||
Tags []Tag
|
||||
CloudflaredVersion string
|
||||
}
|
||||
|
||||
// CapnpConnectParameters is ConnectParameters represented in Cap'n Proto build-in types
|
||||
type CapnpConnectParameters struct {
|
||||
OriginCert []byte
|
||||
CloudflaredID []byte
|
||||
NumPreviousAttempts uint8
|
||||
Tags []Tag
|
||||
CloudflaredVersion string
|
||||
Scope Scope
|
||||
}
|
||||
|
||||
func MarshalConnectParameters(s tunnelrpc.CapnpConnectParameters, p *ConnectParameters) error {
|
||||
if err := s.SetOriginCert(p.OriginCert); err != nil {
|
||||
return err
|
||||
}
|
||||
cloudflaredIDBytes, err := p.CloudflaredID.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
capnpConnectParameters := &CapnpConnectParameters{
|
||||
OriginCert: p.OriginCert,
|
||||
CloudflaredID: cloudflaredIDBytes,
|
||||
NumPreviousAttempts: p.NumPreviousAttempts,
|
||||
CloudflaredVersion: p.CloudflaredVersion,
|
||||
if err := s.SetCloudflaredID(cloudflaredIDBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
return pogs.Insert(tunnelrpc.CapnpConnectParameters_TypeID, s.Struct, capnpConnectParameters)
|
||||
s.SetNumPreviousAttempts(p.NumPreviousAttempts)
|
||||
if len(p.Tags) > 0 {
|
||||
tagsCapnpList, err := s.NewTags(int32(len(p.Tags)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, tag := range p.Tags {
|
||||
tagCapnp := tagsCapnpList.At(i)
|
||||
if err := tagCapnp.SetName(tag.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tagCapnp.SetValue(tag.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.SetCloudflaredVersion(p.CloudflaredVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := s.NewScope()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalScope(scope, p.Scope)
|
||||
}
|
||||
|
||||
func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectParameters, error) {
|
||||
p := new(CapnpConnectParameters)
|
||||
err := pogs.Extract(p, tunnelrpc.CapnpConnectParameters_TypeID, s.Struct)
|
||||
originCert, err := s.OriginCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cloudflaredID, err := uuid.FromBytes(p.CloudflaredID)
|
||||
|
||||
cloudflaredIDBytes, err := s.CloudflaredID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cloudflaredID, err := uuid.FromBytes(cloudflaredIDBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagsCapnpList, err := s.Tags()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tags []Tag
|
||||
for i := 0; i < tagsCapnpList.Len(); i++ {
|
||||
tagCapnp := tagsCapnpList.At(i)
|
||||
name, err := tagCapnp.Name()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value, err := tagCapnp.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, Tag{Name: name, Value: value})
|
||||
}
|
||||
|
||||
cloudflaredVersion, err := s.CloudflaredVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeCapnp, err := s.Scope()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := UnmarshalScope(scopeCapnp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ConnectParameters{
|
||||
OriginCert: p.OriginCert,
|
||||
OriginCert: originCert,
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: p.NumPreviousAttempts,
|
||||
CloudflaredVersion: p.CloudflaredVersion,
|
||||
NumPreviousAttempts: s.NumPreviousAttempts(),
|
||||
Tags: tags,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
Scope: scope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
// Assert *SystemName implements Scope
|
||||
var _ Scope = (*SystemName)(nil)
|
||||
|
||||
// Assert *Group implements Scope
|
||||
var _ Scope = (*Group)(nil)
|
||||
|
||||
func TestScope(t *testing.T) {
|
||||
testCases := []Scope{
|
||||
&SystemName{systemName: "my_system"},
|
||||
&Group{group: "my_group"},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewScope(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalScope(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalScope(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectParameters(t *testing.T) {
|
||||
testCases := []*ConnectParameters{
|
||||
sampleConnectParameters(),
|
||||
sampleConnectParameters(func(c *ConnectParameters) {
|
||||
c.Scope = &SystemName{systemName: "my_system"}
|
||||
}),
|
||||
sampleConnectParameters(func(c *ConnectParameters) {
|
||||
c.Tags = nil
|
||||
}),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewCapnpConnectParameters(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalConnectParameters(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalConnectParameters(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func sampleConnectParameters(overrides ...func(*ConnectParameters)) *ConnectParameters {
|
||||
cloudflaredID, err := uuid.Parse("ED7BA470-8E54-465E-825C-99712043E01C")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sample := &ConnectParameters{
|
||||
OriginCert: []byte("my-origin-cert"),
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: 19,
|
||||
Tags: []Tag{
|
||||
Tag{
|
||||
Name: "provision-method",
|
||||
Value: "new",
|
||||
},
|
||||
},
|
||||
CloudflaredVersion: "7.0",
|
||||
Scope: &Group{group: "my_group"},
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func (c *ConnectParameters) ensureNoZeroFields() {
|
||||
ensureNoZeroFieldsInSample(reflect.ValueOf(c), []string{})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ScopeUnmarshaler can marshal a Scope pog from JSON.
|
||||
type ScopeUnmarshaler struct {
|
||||
Scope Scope
|
||||
}
|
||||
|
||||
// UnmarshalJSON takes in a JSON string, and attempts to marshal it into a Scope.
|
||||
// If successful, the Scope member of this ScopeUnmarshaler is set and nil is returned.
|
||||
// If unsuccessful, returns an error.
|
||||
func (su *ScopeUnmarshaler) UnmarshalJSON(b []byte) error {
|
||||
var scopeJSON map[string]interface{}
|
||||
if err := json.Unmarshal(b, &scopeJSON); err != nil {
|
||||
return errors.Wrapf(err, "cannot unmarshal %s into scopeJSON", string(b))
|
||||
}
|
||||
|
||||
if group, ok := scopeJSON["group"]; ok {
|
||||
if val, ok := group.(string); ok {
|
||||
su.Scope = NewGroup(val)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("JSON should have been a Scope, but the 'group' key contained %v", group)
|
||||
}
|
||||
|
||||
if systemName, ok := scopeJSON["system_name"]; ok {
|
||||
if val, ok := systemName.(string); ok {
|
||||
su.Scope = NewSystemName(val)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("JSON should have been a Scope, but the 'system_name' key contained %v", systemName)
|
||||
}
|
||||
|
||||
return fmt.Errorf("JSON should have been an object with one root key, either 'system_name' or 'group'")
|
||||
}
|
||||
|
||||
type OriginConfigUnmarshaler struct {
|
||||
OriginConfig OriginConfig
|
||||
}
|
||||
|
||||
func (ocu *OriginConfigUnmarshaler) UnmarshalJSON(b []byte) error {
|
||||
var originJSON map[string]interface{}
|
||||
if err := json.Unmarshal(b, &originJSON); err != nil {
|
||||
return errors.Wrapf(err, "cannot unmarshal %s into originJSON", string(b))
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[httpType.String()]; ok {
|
||||
httpOriginConfig := &HTTPOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, httpOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into HTTPOriginConfig", originConfig)
|
||||
}
|
||||
ocu.OriginConfig = httpOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[wsType.String()]; ok {
|
||||
wsOriginConfig := &WebSocketOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, wsOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into WebSocketOriginConfig", originConfig)
|
||||
}
|
||||
ocu.OriginConfig = wsOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[helloWorldType.String()]; ok {
|
||||
helloWorldOriginConfig := &HelloWorldOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, helloWorldOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into HelloWorldOriginConfig", originConfig)
|
||||
}
|
||||
ocu.OriginConfig = helloWorldOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %s into OriginConfig", string(b))
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeUnmarshaler_UnmarshalJSON(t *testing.T) {
|
||||
type fields struct {
|
||||
Scope Scope
|
||||
}
|
||||
type args struct {
|
||||
b []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
wantScope Scope
|
||||
}{
|
||||
{
|
||||
name: "group_successful",
|
||||
args: args{b: []byte(`{"group": "my-group"}`)},
|
||||
wantScope: NewGroup("my-group"),
|
||||
},
|
||||
{
|
||||
name: "system_name_successful",
|
||||
args: args{b: []byte(`{"system_name": "my-computer"}`)},
|
||||
wantScope: NewSystemName("my-computer"),
|
||||
},
|
||||
{
|
||||
name: "not_a_scope",
|
||||
args: args{b: []byte(`{"x": "y"}`)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "malformed_group",
|
||||
args: args{b: []byte(`{"group": ["a", "b"]}`)},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
su := &ScopeUnmarshaler{
|
||||
Scope: tt.fields.Scope,
|
||||
}
|
||||
err := su.UnmarshalJSON(tt.args.b)
|
||||
if !tt.wantErr {
|
||||
if err != nil {
|
||||
t.Errorf("ScopeUnmarshaler.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !eqScope(tt.wantScope, su.Scope) {
|
||||
t.Errorf("Wanted scope %v but got scope %v", tt.wantScope, su.Scope)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalOrigin(t *testing.T) {
|
||||
tests := []struct {
|
||||
jsonLiteral string
|
||||
exceptedOriginConfig OriginConfig
|
||||
}{
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"Http":{
|
||||
"url_string":"https://127.0.0.1:8080",
|
||||
"tcp_keep_alive":30000000000,
|
||||
"dial_dual_stack":true,
|
||||
"tls_handshake_timeout":10000000000,
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"",
|
||||
"origin_server_name":"",
|
||||
"max_idle_connections":100,
|
||||
"idle_connection_timeout":90000000000,
|
||||
"proxy_connection_timeout":90000000000,
|
||||
"expect_continue_timeout":90000000000,
|
||||
"chunked_encoding":true
|
||||
}
|
||||
}`,
|
||||
exceptedOriginConfig: &HTTPOriginConfig{
|
||||
URLString: "https://127.0.0.1:8080",
|
||||
TCPKeepAlive: time.Second * 30,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "",
|
||||
OriginServerName: "",
|
||||
MaxIdleConnections: 100,
|
||||
IdleConnectionTimeout: time.Second * 90,
|
||||
ProxyConnectionTimeout: time.Second * 90,
|
||||
ExpectContinueTimeout: time.Second * 90,
|
||||
ChunkedEncoding: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"WebSocket":{
|
||||
"url_string":"https://127.0.0.1:9090",
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"",
|
||||
"origin_server_name":"ws.example.com"
|
||||
}
|
||||
}`,
|
||||
exceptedOriginConfig: &WebSocketOriginConfig{
|
||||
URLString: "https://127.0.0.1:9090",
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "",
|
||||
OriginServerName: "ws.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"HelloWorld": {}
|
||||
}`,
|
||||
exceptedOriginConfig: &HelloWorldOriginConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
originConfigJSON := strings.ReplaceAll(strings.ReplaceAll(test.jsonLiteral, "\n", ""), "\t", "")
|
||||
var OriginConfigUnmarshaler OriginConfigUnmarshaler
|
||||
err := json.Unmarshal([]byte(originConfigJSON), &OriginConfigUnmarshaler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.exceptedOriginConfig, OriginConfigUnmarshaler.OriginConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalClientConfig(t *testing.T) {
|
||||
prettyClientConfigJSON := `{
|
||||
"version":10,
|
||||
"supervisor_config":{
|
||||
"auto_update_frequency":86400000000000,
|
||||
"metrics_update_frequency":300000000000,
|
||||
"grace_period":30000000000
|
||||
},
|
||||
"edge_connection_config":{
|
||||
"num_ha_connections":4,
|
||||
"heartbeat_interval":5000000000,
|
||||
"timeout":30000000000,
|
||||
"max_failed_heartbeats":5,
|
||||
"user_credential_path":"~/.cloudflared/cert.pem"
|
||||
},
|
||||
"doh_proxy_configs":[{
|
||||
"listen_host": "localhost",
|
||||
"listen_port": 53,
|
||||
"upstreams": ["https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"]
|
||||
}],
|
||||
"reverse_proxy_configs":[{
|
||||
"tunnel_hostname":"sdfjadk33.cftunnel.com",
|
||||
"origin_config":{
|
||||
"Http":{
|
||||
"url_string":"https://127.0.0.1:8080",
|
||||
"tcp_keep_alive":30000000000,
|
||||
"dial_dual_stack":true,
|
||||
"tls_handshake_timeout":10000000000,
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"",
|
||||
"origin_server_name":"",
|
||||
"max_idle_connections":100,
|
||||
"idle_connection_timeout":90000000000,
|
||||
"proxy_connection_timeout":90000000000,
|
||||
"expect_continue_timeout":90000000000,
|
||||
"chunked_encoding":true
|
||||
}
|
||||
},
|
||||
"retries":5,
|
||||
"connection_timeout":30,
|
||||
"compression_quality":0
|
||||
}]
|
||||
}`
|
||||
// replace new line and tab
|
||||
clientConfigJSON := strings.ReplaceAll(strings.ReplaceAll(prettyClientConfigJSON, "\n", ""), "\t", "")
|
||||
|
||||
var clientConfig ClientConfig
|
||||
err := json.Unmarshal([]byte(clientConfigJSON), &clientConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, Version(10), clientConfig.Version)
|
||||
|
||||
supervisorConfig := SupervisorConfig{
|
||||
AutoUpdateFrequency: time.Hour * 24,
|
||||
MetricsUpdateFrequency: time.Second * 300,
|
||||
GracePeriod: time.Second * 30,
|
||||
}
|
||||
assert.Equal(t, supervisorConfig, *clientConfig.SupervisorConfig)
|
||||
|
||||
edgeConnectionConfig := EdgeConnectionConfig{
|
||||
NumHAConnections: 4,
|
||||
HeartbeatInterval: time.Second * 5,
|
||||
Timeout: time.Second * 30,
|
||||
MaxFailedHeartbeats: 5,
|
||||
UserCredentialPath: "~/.cloudflared/cert.pem",
|
||||
}
|
||||
assert.Equal(t, edgeConnectionConfig, *clientConfig.EdgeConnectionConfig)
|
||||
|
||||
dohProxyConfig := DoHProxyConfig{
|
||||
ListenHost: "localhost",
|
||||
ListenPort: 53,
|
||||
Upstreams: []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"},
|
||||
}
|
||||
|
||||
assert.Len(t, clientConfig.DoHProxyConfigs, 1)
|
||||
assert.Equal(t, dohProxyConfig, *clientConfig.DoHProxyConfigs[0])
|
||||
|
||||
reverseProxyConfig := ReverseProxyConfig{
|
||||
TunnelHostname: "sdfjadk33.cftunnel.com",
|
||||
OriginConfigUnmarshaler: &OriginConfigUnmarshaler{
|
||||
OriginConfig: &HTTPOriginConfig{
|
||||
URLString: "https://127.0.0.1:8080",
|
||||
TCPKeepAlive: time.Second * 30,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "",
|
||||
OriginServerName: "",
|
||||
MaxIdleConnections: 100,
|
||||
IdleConnectionTimeout: time.Second * 90,
|
||||
ProxyConnectionTimeout: time.Second * 90,
|
||||
ExpectContinueTimeout: time.Second * 90,
|
||||
ChunkedEncoding: true,
|
||||
},
|
||||
},
|
||||
Retries: 5,
|
||||
ConnectionTimeout: 30,
|
||||
CompressionQuality: 0,
|
||||
}
|
||||
|
||||
assert.Len(t, clientConfig.ReverseProxyConfigs, 1)
|
||||
assert.Equal(t, reverseProxyConfig, *clientConfig.ReverseProxyConfigs[0])
|
||||
}
|
||||
|
||||
func eqScope(s1, s2 Scope) bool {
|
||||
return s1.Value() == s2.Value() && s1.PostgresType() == s2.PostgresType()
|
||||
}
|
||||
+95
-43
@@ -57,6 +57,17 @@ struct CapnpConnectParameters {
|
||||
tags @3 :List(Tag);
|
||||
# release version of cloudflared
|
||||
cloudflaredVersion @4 :Text;
|
||||
# identifier for this cloudflared instance
|
||||
scope @5 :Scope;
|
||||
}
|
||||
|
||||
struct Scope {
|
||||
value :union {
|
||||
# Standalone instance
|
||||
systemName @0 :Text;
|
||||
# Associated with a group of identical cloudflared instances
|
||||
group @1 :Text;
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectResult {
|
||||
@@ -72,83 +83,103 @@ struct ConnectError {
|
||||
shouldRetry @2 :Bool;
|
||||
}
|
||||
|
||||
struct CloudflaredConfig {
|
||||
# Timestamp (in ns) of this configuration. Any configuration supplied to
|
||||
# useConfiguration() with an older timestamp should be ignored.
|
||||
timestamp @0 :Int64;
|
||||
struct ClientConfig {
|
||||
# Version of this configuration. This value is opaque, but is guaranteed
|
||||
# to monotonically increase in value. Any configuration supplied to
|
||||
# useConfiguration() with a smaller `version` should be ignored.
|
||||
version @0 :UInt64;
|
||||
# supervisorConfig is configuration for supervisor, the component that manages connection manager,
|
||||
# autoupdater and metrics server
|
||||
supervisorConfig @1 :SupervisorConfig;
|
||||
# edgeConnectionConfig is configuration for connection manager, the componenet that manages connections with the edge
|
||||
edgeConnectionConfig @2 :EdgeConnectionConfig;
|
||||
# Configuration for cloudflared to run as a DNS-over-HTTPS proxy.
|
||||
# cloudflared CLI option: `proxy-dns`
|
||||
dohProxyConfigs @3 :List(DoHProxyConfig);
|
||||
# Configuration for cloudflared to run as an HTTP reverse proxy.
|
||||
reverseProxyConfigs @4 :List(ReverseProxyConfig);
|
||||
}
|
||||
|
||||
struct SupervisorConfig {
|
||||
# Frequency (in ns) to check Equinox for updates.
|
||||
# Zero means auto-update is disabled.
|
||||
# cloudflared CLI option: `autoupdate-freq`
|
||||
autoUpdateFrequency @1 :Int64;
|
||||
autoUpdateFrequency @0 :Int64;
|
||||
# Frequency (in ns) to update connection-based metrics.
|
||||
# cloudflared CLI option: `metrics-update-freq`
|
||||
metricsUpdateFrequency @2 :Int64;
|
||||
# interval (in ns) between heartbeats with the Cloudflare edge
|
||||
# cloudflared CLI option: `heartbeat-interval`
|
||||
heartbeatInterval @3 :Int64;
|
||||
# Minimum number of unacked heartbeats for cloudflared to send before
|
||||
# closing the connection to the edge.
|
||||
# cloudflared CLI option: `heartbeat-count`
|
||||
maxFailedHeartbeats @4 :UInt64;
|
||||
metricsUpdateFrequency @1 :Int64;
|
||||
# Time (in ns) to continue serving requests after cloudflared receives its
|
||||
# first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to
|
||||
# shutdown immediately. For example, this field can be used to gracefully
|
||||
# transition traffic to another cloudflared instance.
|
||||
# cloudflared CLI option: `grace-period`
|
||||
gracePeriod @5 :Int64;
|
||||
# Configuration for cloudflared to run as a DNS-over-HTTPS proxy.
|
||||
# cloudflared CLI option: `proxy-dns`
|
||||
dohProxyConfigs @6 :List(DoHProxyConfig);
|
||||
# Configuration for cloudflared to run as an HTTP reverse proxy.
|
||||
reverseProxyConfigs @7 :List(ReverseProxyConfig);
|
||||
gracePeriod @2 :Int64;
|
||||
}
|
||||
|
||||
struct EdgeConnectionConfig {
|
||||
# cloudflared CLI option: `ha-connections`
|
||||
numHAConnections @0 :UInt8;
|
||||
# Interval (in ns) between heartbeats with the Cloudflare edge
|
||||
# cloudflared CLI option: `heartbeat-interval`
|
||||
heartbeatInterval @1 :Int64;
|
||||
# Maximum wait time to connect with the edge.
|
||||
timeout @2 :Int64;
|
||||
# Number of unacked heartbeats for cloudflared to send before
|
||||
# closing the connection to the edge.
|
||||
# cloudflared CLI option: `heartbeat-count`
|
||||
maxFailedHeartbeats @3 :UInt64;
|
||||
# Absolute path of the file containing certificate and token to connect with the edge
|
||||
userCredentialPath @4 :Text;
|
||||
}
|
||||
|
||||
struct ReverseProxyConfig {
|
||||
tunnelID @0 :Text;
|
||||
tunnelHostname @0 :Text;
|
||||
origin :union {
|
||||
http @1 :HTTPOriginConfig;
|
||||
socket @2 :UnixSocketOriginConfig;
|
||||
websocket @3 :WebSocketOriginConfig;
|
||||
helloWorld @4 :HelloWorldOriginConfig;
|
||||
websocket @2 :WebSocketOriginConfig;
|
||||
helloWorld @3 :HelloWorldOriginConfig;
|
||||
}
|
||||
# Maximum number of retries for connection/protocol errors.
|
||||
# cloudflared CLI option: `retries`
|
||||
retries @5 :UInt64;
|
||||
retries @4 :UInt64;
|
||||
# maximum time (in ns) for cloudflared to wait to establish a connection
|
||||
# to the origin. Zero means no timeout.
|
||||
# cloudflared CLI option: `proxy-connect-timeout`
|
||||
connectionTimeout @6 :Int64;
|
||||
# Whether cloudflared should allow chunked transfer encoding to the
|
||||
# origin. (This should be disabled for WSGI origins, for example.)
|
||||
# negation of cloudflared CLI option: `no-chunked-encoding`
|
||||
chunkedEncoding @7 :Bool;
|
||||
connectionTimeout @5 :Int64;
|
||||
# (beta) Use cross-stream compression instead of HTTP compression.
|
||||
# 0=off, 1=low, 2=medium, 3=high.
|
||||
# For more context see the mapping here: https://github.com/cloudflare/cloudflared/blob/2019.3.2/h2mux/h2_dictionaries.go#L62
|
||||
# cloudflared CLI option: `compression-quality`
|
||||
compressionQuality @8 :UInt64;
|
||||
compressionQuality @6 :UInt64;
|
||||
}
|
||||
|
||||
struct UnixSocketOriginConfig {
|
||||
# path to the socket file.
|
||||
# cloudflared will send data to this socket via a Unix socket connection.
|
||||
# cloudflared CLI option: `unix-socket`
|
||||
path @0 :Text;
|
||||
}
|
||||
|
||||
#
|
||||
struct WebSocketOriginConfig {
|
||||
# URI of the origin service.
|
||||
# cloudflared will start a websocket server that forwards data to this URI
|
||||
# cloudflared CLI option: `url`
|
||||
# cloudflared logic: https://github.com/cloudflare/cloudflared/blob/2019.3.2/cmd/cloudflared/tunnel/cmd.go#L304
|
||||
url @0 :Text;
|
||||
urlString @0 :Text;
|
||||
# Whether cloudflared should verify TLS connections to the origin.
|
||||
# negation of cloudflared CLI option: `no-tls-verify`
|
||||
tlsVerify @1 :Bool;
|
||||
# originCAPool specifies the root CA that cloudflared should use when
|
||||
# verifying TLS connections to the origin.
|
||||
# - if tlsVerify is false, originCAPool will be ignored.
|
||||
# - if tlsVerify is true and originCAPool is empty, the system CA pool
|
||||
# will be loaded if possible.
|
||||
# - if tlsVerify is true and originCAPool is non-empty, cloudflared will
|
||||
# treat it as the filepath to the root CA.
|
||||
# cloudflared CLI option: `origin-ca-pool`
|
||||
originCAPool @2 :Text;
|
||||
# Hostname to use when verifying TLS connections to the origin.
|
||||
# cloudflared CLI option: `origin-server-name`
|
||||
originServerName @3 :Text;
|
||||
}
|
||||
|
||||
struct HTTPOriginConfig {
|
||||
# HTTP(S) URL of the origin service.
|
||||
# cloudflared CLI option: `url`
|
||||
url @0 :Text;
|
||||
urlString @0 :Text;
|
||||
# the TCP keep-alive period (in ns) for an active network connection.
|
||||
# Zero means keep-alives are not enabled.
|
||||
# cloudflared CLI option: `proxy-tcp-keepalive`
|
||||
@@ -184,6 +215,17 @@ struct HTTPOriginConfig {
|
||||
# idle before closing itself. Zero means no timeout.
|
||||
# cloudflared CLI option: `proxy-keepalive-timeout`
|
||||
idleConnectionTimeout @8 :Int64;
|
||||
# maximum amount of time a dial will wait for a connect to complete.
|
||||
proxyConnectionTimeout @9 :Int64;
|
||||
# The amount of time to wait for origin's first response headers after fully
|
||||
# writing the request headers if the request has an "Expect: 100-continue" header.
|
||||
# Zero means no timeout and causes the body to be sent immediately, without
|
||||
# waiting for the server to approve.
|
||||
expectContinueTimeout @10 :Int64;
|
||||
# Whether cloudflared should allow chunked transfer encoding to the
|
||||
# origin. (This should be disabled for WSGI origins, for example.)
|
||||
# negation of cloudflared CLI option: `no-chunked-encoding`
|
||||
chunkedEncoding @11 :Bool;
|
||||
}
|
||||
|
||||
# configuration for cloudflared to provide a DNS over HTTPS proxy server
|
||||
@@ -220,7 +262,17 @@ struct ServerInfo {
|
||||
|
||||
struct UseConfigurationResult {
|
||||
success @0 :Bool;
|
||||
errorMessage @1 :Text;
|
||||
failedConfigs @1 :List(FailedConfig);
|
||||
}
|
||||
|
||||
struct FailedConfig {
|
||||
config :union {
|
||||
supervisor @0 :SupervisorConfig;
|
||||
edgeConnection @1 :EdgeConnectionConfig;
|
||||
doh @2 :DoHProxyConfig;
|
||||
reverseProxy @3 :ReverseProxyConfig;
|
||||
}
|
||||
reason @4 :Text;
|
||||
}
|
||||
|
||||
interface TunnelServer {
|
||||
@@ -230,6 +282,6 @@ interface TunnelServer {
|
||||
connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult);
|
||||
}
|
||||
|
||||
interface CloudflaredServer {
|
||||
useConfiguration @0 (cloudflaredConfig :CloudflaredConfig) -> (result :UseConfigurationResult);
|
||||
interface ClientService {
|
||||
useConfiguration @0 (clientServiceConfig :ClientConfig) -> (result :UseConfigurationResult);
|
||||
}
|
||||
|
||||
+1294
-539
File diff suppressed because it is too large
Load Diff
+12
-106
@@ -180,33 +180,6 @@ func TestValidateHTTPService_HTTP2HTTP(t *testing.T) {
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
|
||||
}
|
||||
|
||||
// Happy path 2: originURL is HTTPS, and HTTPS connections work
|
||||
@@ -235,36 +208,6 @@ func TestValidateHTTPService_HTTPS2HTTPS(t *testing.T) {
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
}
|
||||
|
||||
// Error path 1: originURL is HTTPS, but HTTP connections work
|
||||
@@ -293,37 +236,6 @@ func TestValidateHTTPService_HTTPS2HTTP(t *testing.T) {
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
|
||||
}
|
||||
|
||||
// Error path 2: originURL is HTTP, but HTTPS connections work
|
||||
@@ -352,21 +264,11 @@ func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
}
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
// this will fail if the client follows the 302
|
||||
// Ensure the client does not follow 302 responses
|
||||
func TestValidateHTTPService_NoFollowRedirects(t *testing.T) {
|
||||
hostname := "example.com"
|
||||
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
@@ -381,12 +283,12 @@ func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
assert.NoError(t, ValidateHTTPService(redirectServer.URL, hostname, redirectClient.Transport))
|
||||
}
|
||||
|
||||
// error path 3: origin URL is nonresponsive
|
||||
// Ensure validation times out when origin URL is nonresponsive
|
||||
func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
originURL := "https://127.0.0.1/"
|
||||
originURL := "http://127.0.0.1/"
|
||||
hostname := "example.com"
|
||||
oldValidationTimeout := validationTimeout
|
||||
defer func() {
|
||||
@@ -394,7 +296,10 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
}()
|
||||
validationTimeout = 500 * time.Millisecond
|
||||
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Use createMockServerAndClient, not createSecureMockServerAndClient.
|
||||
// The latter will bail with HTTP 400 immediately on an http:// request,
|
||||
// which defeats the purpose of a 'nonresponsive origin' test.
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
@@ -409,6 +314,7 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
err = ValidateHTTPService(originURL, hostname, client.Transport)
|
||||
fmt.Println(err)
|
||||
if err, ok := err.(net.Error); assert.True(t, ok) {
|
||||
assert.True(t, err.Timeout())
|
||||
}
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Client interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Package http is DEPRECATED. Use net/http instead.
|
||||
package http
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func WriteError(w http.ResponseWriter, code int, msg string) {
|
||||
e := struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: msg,
|
||||
}
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Printf("go-oidc: failed to marshal %#v: %v", e, err)
|
||||
code = http.StatusInternalServerError
|
||||
b = []byte(`{"error":"server_error"}`)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// BasicAuth parses a username and password from the request's
|
||||
// Authorization header. This was pulled from golang master:
|
||||
// https://codereview.appspot.com/76540043
|
||||
func BasicAuth(r *http.Request) (username, password string, ok bool) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Basic ") {
|
||||
return
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
|
||||
func cacheControlMaxAge(hdr string) (time.Duration, bool, error) {
|
||||
for _, field := range strings.Split(hdr, ",") {
|
||||
parts := strings.SplitN(strings.TrimSpace(field), "=", 2)
|
||||
k := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
if k != "max-age" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return 0, false, errors.New("max-age has no value")
|
||||
}
|
||||
|
||||
v := strings.TrimSpace(parts[1])
|
||||
if v == "" {
|
||||
return 0, false, errors.New("max-age has empty value")
|
||||
}
|
||||
|
||||
age, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
if age <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
return time.Duration(age) * time.Second, true, nil
|
||||
}
|
||||
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
func expires(date, expires string) (time.Duration, bool, error) {
|
||||
if date == "" || expires == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var te time.Time
|
||||
var err error
|
||||
if expires == "0" {
|
||||
return 0, false, nil
|
||||
}
|
||||
te, err = time.Parse(time.RFC1123, expires)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
td, err := time.Parse(time.RFC1123, date)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
ttl := te.Sub(td)
|
||||
|
||||
// headers indicate data already expired, caller should not
|
||||
// have to care about this case
|
||||
if ttl <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
return ttl, true, nil
|
||||
}
|
||||
|
||||
func Cacheable(hdr http.Header) (time.Duration, bool, error) {
|
||||
ttl, ok, err := cacheControlMaxAge(hdr.Get("Cache-Control"))
|
||||
if err != nil || ok {
|
||||
return ttl, ok, err
|
||||
}
|
||||
|
||||
return expires(hdr.Get("Date"), hdr.Get("Expires"))
|
||||
}
|
||||
|
||||
// MergeQuery appends additional query values to an existing URL.
|
||||
func MergeQuery(u url.URL, q url.Values) url.URL {
|
||||
uv := u.Query()
|
||||
for k, vs := range q {
|
||||
for _, v := range vs {
|
||||
uv.Add(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = uv.Encode()
|
||||
return u
|
||||
}
|
||||
|
||||
// NewResourceLocation appends a resource id to the end of the requested URL path.
|
||||
func NewResourceLocation(reqURL *url.URL, id string) string {
|
||||
var u url.URL
|
||||
u = *reqURL
|
||||
u.Path = path.Join(u.Path, id)
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// CopyRequest returns a clone of the provided *http.Request.
|
||||
// The returned object is a shallow copy of the struct and a
|
||||
// deep copy of its Header field.
|
||||
func CopyRequest(r *http.Request) *http.Request {
|
||||
r2 := *r
|
||||
r2.Header = make(http.Header)
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = s
|
||||
}
|
||||
return &r2
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ParseNonEmptyURL checks that a string is a parsable URL which is also not empty
|
||||
// since `url.Parse("")` does not return an error. Must contian a scheme and a host.
|
||||
func ParseNonEmptyURL(u string) (*url.URL, error) {
|
||||
if u == "" {
|
||||
return nil, errors.New("url is empty")
|
||||
}
|
||||
|
||||
ur, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ur.Scheme == "" {
|
||||
return nil, errors.New("url scheme is empty")
|
||||
}
|
||||
|
||||
if ur.Host == "" {
|
||||
return nil, errors.New("url host is empty")
|
||||
}
|
||||
|
||||
return ur, nil
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Package key is DEPRECATED. Use github.com/coreos/go-oidc instead.
|
||||
package key
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
func NewPublicKey(jwk jose.JWK) *PublicKey {
|
||||
return &PublicKey{jwk: jwk}
|
||||
}
|
||||
|
||||
type PublicKey struct {
|
||||
jwk jose.JWK
|
||||
}
|
||||
|
||||
func (k *PublicKey) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&k.jwk)
|
||||
}
|
||||
|
||||
func (k *PublicKey) UnmarshalJSON(data []byte) error {
|
||||
var jwk jose.JWK
|
||||
if err := json.Unmarshal(data, &jwk); err != nil {
|
||||
return err
|
||||
}
|
||||
k.jwk = jwk
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *PublicKey) ID() string {
|
||||
return k.jwk.ID
|
||||
}
|
||||
|
||||
func (k *PublicKey) Verifier() (jose.Verifier, error) {
|
||||
return jose.NewVerifierRSA(k.jwk)
|
||||
}
|
||||
|
||||
type PrivateKey struct {
|
||||
KeyID string
|
||||
PrivateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *PrivateKey) ID() string {
|
||||
return k.KeyID
|
||||
}
|
||||
|
||||
func (k *PrivateKey) Signer() jose.Signer {
|
||||
return jose.NewSignerRSA(k.ID(), *k.PrivateKey)
|
||||
}
|
||||
|
||||
func (k *PrivateKey) JWK() jose.JWK {
|
||||
return jose.JWK{
|
||||
ID: k.KeyID,
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Exponent: k.PrivateKey.PublicKey.E,
|
||||
Modulus: k.PrivateKey.PublicKey.N,
|
||||
}
|
||||
}
|
||||
|
||||
type KeySet interface {
|
||||
ExpiresAt() time.Time
|
||||
}
|
||||
|
||||
type PublicKeySet struct {
|
||||
keys []PublicKey
|
||||
index map[string]*PublicKey
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func NewPublicKeySet(jwks []jose.JWK, exp time.Time) *PublicKeySet {
|
||||
keys := make([]PublicKey, len(jwks))
|
||||
index := make(map[string]*PublicKey)
|
||||
for i, jwk := range jwks {
|
||||
keys[i] = *NewPublicKey(jwk)
|
||||
index[keys[i].ID()] = &keys[i]
|
||||
}
|
||||
return &PublicKeySet{
|
||||
keys: keys,
|
||||
index: index,
|
||||
expiresAt: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) ExpiresAt() time.Time {
|
||||
return s.expiresAt
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) Keys() []PublicKey {
|
||||
return s.keys
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) Key(id string) *PublicKey {
|
||||
return s.index[id]
|
||||
}
|
||||
|
||||
type PrivateKeySet struct {
|
||||
keys []*PrivateKey
|
||||
ActiveKeyID string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func NewPrivateKeySet(keys []*PrivateKey, exp time.Time) *PrivateKeySet {
|
||||
return &PrivateKeySet{
|
||||
keys: keys,
|
||||
ActiveKeyID: keys[0].ID(),
|
||||
expiresAt: exp.UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) Keys() []*PrivateKey {
|
||||
return s.keys
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) ExpiresAt() time.Time {
|
||||
return s.expiresAt
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) Active() *PrivateKey {
|
||||
for i, k := range s.keys {
|
||||
if k.ID() == s.ActiveKeyID {
|
||||
return s.keys[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GeneratePrivateKeyFunc func() (*PrivateKey, error)
|
||||
|
||||
func GeneratePrivateKey() (*PrivateKey, error) {
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyID := make([]byte, 20)
|
||||
if _, err := io.ReadFull(rand.Reader, keyID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := PrivateKey{
|
||||
KeyID: hex.EncodeToString(keyID),
|
||||
PrivateKey: pk,
|
||||
}
|
||||
|
||||
return &k, nil
|
||||
}
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/pkg/health"
|
||||
)
|
||||
|
||||
type PrivateKeyManager interface {
|
||||
ExpiresAt() time.Time
|
||||
Signer() (jose.Signer, error)
|
||||
JWKs() ([]jose.JWK, error)
|
||||
PublicKeys() ([]PublicKey, error)
|
||||
|
||||
WritableKeySetRepo
|
||||
health.Checkable
|
||||
}
|
||||
|
||||
func NewPrivateKeyManager() PrivateKeyManager {
|
||||
return &privateKeyManager{
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type privateKeyManager struct {
|
||||
keySet *PrivateKeySet
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) ExpiresAt() time.Time {
|
||||
if m.keySet == nil {
|
||||
return m.clock.Now().UTC()
|
||||
}
|
||||
|
||||
return m.keySet.ExpiresAt()
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Signer() (jose.Signer, error) {
|
||||
if err := m.Healthy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.keySet.Active().Signer(), nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) JWKs() ([]jose.JWK, error) {
|
||||
if err := m.Healthy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := m.keySet.Keys()
|
||||
jwks := make([]jose.JWK, len(keys))
|
||||
for i, k := range keys {
|
||||
jwks[i] = k.JWK()
|
||||
}
|
||||
return jwks, nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) PublicKeys() ([]PublicKey, error) {
|
||||
jwks, err := m.JWKs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]PublicKey, len(jwks))
|
||||
for i, jwk := range jwks {
|
||||
keys[i] = *NewPublicKey(jwk)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Healthy() error {
|
||||
if m.keySet == nil {
|
||||
return errors.New("private key manager uninitialized")
|
||||
}
|
||||
|
||||
if len(m.keySet.Keys()) == 0 {
|
||||
return errors.New("private key manager zero keys")
|
||||
}
|
||||
|
||||
if m.keySet.ExpiresAt().Before(m.clock.Now().UTC()) {
|
||||
return errors.New("private key manager keys expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Set(keySet KeySet) error {
|
||||
privKeySet, ok := keySet.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
|
||||
m.keySet = privKeySet
|
||||
return nil
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrorNoKeys = errors.New("no keys found")
|
||||
|
||||
type WritableKeySetRepo interface {
|
||||
Set(KeySet) error
|
||||
}
|
||||
|
||||
type ReadableKeySetRepo interface {
|
||||
Get() (KeySet, error)
|
||||
}
|
||||
|
||||
type PrivateKeySetRepo interface {
|
||||
WritableKeySetRepo
|
||||
ReadableKeySetRepo
|
||||
}
|
||||
|
||||
func NewPrivateKeySetRepo() PrivateKeySetRepo {
|
||||
return &memPrivateKeySetRepo{}
|
||||
}
|
||||
|
||||
type memPrivateKeySetRepo struct {
|
||||
mu sync.RWMutex
|
||||
pks PrivateKeySet
|
||||
}
|
||||
|
||||
func (r *memPrivateKeySetRepo) Set(ks KeySet) error {
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
} else if pks == nil {
|
||||
return errors.New("nil KeySet")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.pks = *pks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *memPrivateKeySetRepo) Get() (KeySet, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.pks.keys == nil {
|
||||
return nil, ErrorNoKeys
|
||||
}
|
||||
return KeySet(&r.pks), nil
|
||||
}
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
ptime "github.com/coreos/pkg/timeutil"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorPrivateKeysExpired = errors.New("private keys have expired")
|
||||
)
|
||||
|
||||
func NewPrivateKeyRotator(repo PrivateKeySetRepo, ttl time.Duration) *PrivateKeyRotator {
|
||||
return &PrivateKeyRotator{
|
||||
repo: repo,
|
||||
ttl: ttl,
|
||||
|
||||
keep: 2,
|
||||
generateKey: GeneratePrivateKey,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type PrivateKeyRotator struct {
|
||||
repo PrivateKeySetRepo
|
||||
generateKey GeneratePrivateKeyFunc
|
||||
clock clockwork.Clock
|
||||
keep int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) expiresAt() time.Time {
|
||||
return r.clock.Now().UTC().Add(r.ttl)
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) Healthy() error {
|
||||
pks, err := r.privateKeySet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.clock.Now().After(pks.ExpiresAt()) {
|
||||
return ErrorPrivateKeysExpired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) privateKeySet() (*PrivateKeySet, error) {
|
||||
ks, err := r.repo.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
return pks, nil
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) nextRotation() (time.Duration, error) {
|
||||
pks, err := r.privateKeySet()
|
||||
if err == ErrorNoKeys {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
now := r.clock.Now()
|
||||
|
||||
// Ideally, we want to rotate after half the TTL has elapsed.
|
||||
idealRotationTime := pks.ExpiresAt().Add(-r.ttl / 2)
|
||||
|
||||
// If we are past the ideal rotation time, rotate immediatly.
|
||||
return max(0, idealRotationTime.Sub(now)), nil
|
||||
}
|
||||
|
||||
func max(a, b time.Duration) time.Duration {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) Run() chan struct{} {
|
||||
attempt := func() {
|
||||
k, err := r.generateKey()
|
||||
if err != nil {
|
||||
log.Printf("go-oidc: failed generating signing key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
exp := r.expiresAt()
|
||||
if err := rotatePrivateKeys(r.repo, k, r.keep, exp); err != nil {
|
||||
log.Printf("go-oidc: key rotation failed: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
var nextRotation time.Duration
|
||||
var sleep time.Duration
|
||||
var err error
|
||||
for {
|
||||
if nextRotation, err = r.nextRotation(); err == nil {
|
||||
break
|
||||
}
|
||||
sleep = ptime.ExpBackoff(sleep, time.Minute)
|
||||
log.Printf("go-oidc: error getting nextRotation, retrying in %v: %v", sleep, err)
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-r.clock.After(nextRotation):
|
||||
attempt()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func rotatePrivateKeys(repo PrivateKeySetRepo, k *PrivateKey, keep int, exp time.Time) error {
|
||||
ks, err := repo.Get()
|
||||
if err != nil && err != ErrorNoKeys {
|
||||
return err
|
||||
}
|
||||
|
||||
var keys []*PrivateKey
|
||||
if ks != nil {
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
keys = pks.Keys()
|
||||
}
|
||||
|
||||
keys = append([]*PrivateKey{k}, keys...)
|
||||
if l := len(keys); l > keep {
|
||||
keys = keys[0:keep]
|
||||
}
|
||||
|
||||
nks := PrivateKeySet{
|
||||
keys: keys,
|
||||
ActiveKeyID: k.ID(),
|
||||
expiresAt: exp,
|
||||
}
|
||||
|
||||
return repo.Set(KeySet(&nks))
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/pkg/timeutil"
|
||||
)
|
||||
|
||||
func NewKeySetSyncer(r ReadableKeySetRepo, w WritableKeySetRepo) *KeySetSyncer {
|
||||
return &KeySetSyncer{
|
||||
readable: r,
|
||||
writable: w,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type KeySetSyncer struct {
|
||||
readable ReadableKeySetRepo
|
||||
writable WritableKeySetRepo
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func (s *KeySetSyncer) Run() chan struct{} {
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
var failing bool
|
||||
var next time.Duration
|
||||
for {
|
||||
exp, err := syncKeySet(s.readable, s.writable, s.clock)
|
||||
if err != nil || exp == 0 {
|
||||
if !failing {
|
||||
failing = true
|
||||
next = time.Second
|
||||
} else {
|
||||
next = timeutil.ExpBackoff(next, time.Minute)
|
||||
}
|
||||
if exp == 0 {
|
||||
log.Printf("Synced to already expired key set, retrying in %v: %v", next, err)
|
||||
|
||||
} else {
|
||||
log.Printf("Failed syncing key set, retrying in %v: %v", next, err)
|
||||
}
|
||||
} else {
|
||||
failing = false
|
||||
next = exp / 2
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.clock.After(next):
|
||||
continue
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func Sync(r ReadableKeySetRepo, w WritableKeySetRepo) (time.Duration, error) {
|
||||
return syncKeySet(r, w, clockwork.NewRealClock())
|
||||
}
|
||||
|
||||
// syncKeySet copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire.
|
||||
// If keyset has already expired, returns a zero duration.
|
||||
func syncKeySet(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) {
|
||||
var ks KeySet
|
||||
ks, err = r.Get()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ks == nil {
|
||||
err = errors.New("no source KeySet")
|
||||
return
|
||||
}
|
||||
|
||||
if err = w.Set(ks); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
if ks.ExpiresAt().After(now) {
|
||||
exp = ks.ExpiresAt().Sub(now)
|
||||
}
|
||||
return
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Package oauth2 is DEPRECATED. Use golang.org/x/oauth instead.
|
||||
package oauth2
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package oauth2
|
||||
|
||||
const (
|
||||
ErrorAccessDenied = "access_denied"
|
||||
ErrorInvalidClient = "invalid_client"
|
||||
ErrorInvalidGrant = "invalid_grant"
|
||||
ErrorInvalidRequest = "invalid_request"
|
||||
ErrorServerError = "server_error"
|
||||
ErrorUnauthorizedClient = "unauthorized_client"
|
||||
ErrorUnsupportedGrantType = "unsupported_grant_type"
|
||||
ErrorUnsupportedResponseType = "unsupported_response_type"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Type string `json:"error"`
|
||||
Description string `json:"error_description,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.Description != "" {
|
||||
return e.Type + ": " + e.Description
|
||||
}
|
||||
return e.Type
|
||||
}
|
||||
|
||||
func NewError(typ string) *Error {
|
||||
return &Error{Type: typ}
|
||||
}
|
||||
-416
@@ -1,416 +0,0 @@
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
)
|
||||
|
||||
// ResponseTypesEqual compares two response_type values. If either
|
||||
// contains a space, it is treated as an unordered list. For example,
|
||||
// comparing "code id_token" and "id_token code" would evaluate to true.
|
||||
func ResponseTypesEqual(r1, r2 string) bool {
|
||||
if !strings.Contains(r1, " ") || !strings.Contains(r2, " ") {
|
||||
// fast route, no split needed
|
||||
return r1 == r2
|
||||
}
|
||||
|
||||
// split, sort, and compare
|
||||
r1Fields := strings.Fields(r1)
|
||||
r2Fields := strings.Fields(r2)
|
||||
if len(r1Fields) != len(r2Fields) {
|
||||
return false
|
||||
}
|
||||
sort.Strings(r1Fields)
|
||||
sort.Strings(r2Fields)
|
||||
for i, r1Field := range r1Fields {
|
||||
if r1Field != r2Fields[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
// OAuth2.0 response types registered by OIDC.
|
||||
//
|
||||
// See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#RegistryContents
|
||||
ResponseTypeCode = "code"
|
||||
ResponseTypeCodeIDToken = "code id_token"
|
||||
ResponseTypeCodeIDTokenToken = "code id_token token"
|
||||
ResponseTypeIDToken = "id_token"
|
||||
ResponseTypeIDTokenToken = "id_token token"
|
||||
ResponseTypeToken = "token"
|
||||
ResponseTypeNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
GrantTypeAuthCode = "authorization_code"
|
||||
GrantTypeClientCreds = "client_credentials"
|
||||
GrantTypeUserCreds = "password"
|
||||
GrantTypeImplicit = "implicit"
|
||||
GrantTypeRefreshToken = "refresh_token"
|
||||
|
||||
AuthMethodClientSecretPost = "client_secret_post"
|
||||
AuthMethodClientSecretBasic = "client_secret_basic"
|
||||
AuthMethodClientSecretJWT = "client_secret_jwt"
|
||||
AuthMethodPrivateKeyJWT = "private_key_jwt"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Credentials ClientCredentials
|
||||
Scope []string
|
||||
RedirectURL string
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
|
||||
// Must be one of the AuthMethodXXX methods above. Right now, only
|
||||
// AuthMethodClientSecretPost and AuthMethodClientSecretBasic are supported.
|
||||
AuthMethod string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
hc phttp.Client
|
||||
creds ClientCredentials
|
||||
scope []string
|
||||
authURL *url.URL
|
||||
redirectURL *url.URL
|
||||
tokenURL *url.URL
|
||||
authMethod string
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
ID string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func NewClient(hc phttp.Client, cfg Config) (c *Client, err error) {
|
||||
if len(cfg.Credentials.ID) == 0 {
|
||||
err = errors.New("missing client id")
|
||||
return
|
||||
}
|
||||
|
||||
if len(cfg.Credentials.Secret) == 0 {
|
||||
err = errors.New("missing client secret")
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.AuthMethod == "" {
|
||||
cfg.AuthMethod = AuthMethodClientSecretBasic
|
||||
} else if cfg.AuthMethod != AuthMethodClientSecretPost && cfg.AuthMethod != AuthMethodClientSecretBasic {
|
||||
err = fmt.Errorf("auth method %q is not supported", cfg.AuthMethod)
|
||||
return
|
||||
}
|
||||
|
||||
au, err := phttp.ParseNonEmptyURL(cfg.AuthURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tu, err := phttp.ParseNonEmptyURL(cfg.TokenURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Allow empty redirect URL in the case where the client
|
||||
// only needs to verify a given token.
|
||||
ru, err := url.Parse(cfg.RedirectURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c = &Client{
|
||||
creds: cfg.Credentials,
|
||||
scope: cfg.Scope,
|
||||
redirectURL: ru,
|
||||
authURL: au,
|
||||
tokenURL: tu,
|
||||
hc: hc,
|
||||
authMethod: cfg.AuthMethod,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Return the embedded HTTP client
|
||||
func (c *Client) HttpClient() phttp.Client {
|
||||
return c.hc
|
||||
}
|
||||
|
||||
// Generate the url for initial redirect to oauth provider.
|
||||
func (c *Client) AuthCodeURL(state, accessType, prompt string) string {
|
||||
v := c.commonURLValues()
|
||||
v.Set("state", state)
|
||||
if strings.ToLower(accessType) == "offline" {
|
||||
v.Set("access_type", "offline")
|
||||
}
|
||||
|
||||
if prompt != "" {
|
||||
v.Set("prompt", prompt)
|
||||
}
|
||||
v.Set("response_type", "code")
|
||||
|
||||
q := v.Encode()
|
||||
u := *c.authURL
|
||||
if u.RawQuery == "" {
|
||||
u.RawQuery = q
|
||||
} else {
|
||||
u.RawQuery += "&" + q
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c *Client) commonURLValues() url.Values {
|
||||
return url.Values{
|
||||
"redirect_uri": {c.redirectURL.String()},
|
||||
"scope": {strings.Join(c.scope, " ")},
|
||||
"client_id": {c.creds.ID},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) newAuthenticatedRequest(urlToken string, values url.Values) (*http.Request, error) {
|
||||
var req *http.Request
|
||||
var err error
|
||||
switch c.authMethod {
|
||||
case AuthMethodClientSecretPost:
|
||||
values.Set("client_secret", c.creds.Secret)
|
||||
req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case AuthMethodClientSecretBasic:
|
||||
req, err = http.NewRequest("POST", urlToken, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encodedID := url.QueryEscape(c.creds.ID)
|
||||
encodedSecret := url.QueryEscape(c.creds.Secret)
|
||||
req.SetBasicAuth(encodedID, encodedSecret)
|
||||
default:
|
||||
panic("misconfigured client: auth method not supported")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return req, nil
|
||||
|
||||
}
|
||||
|
||||
// ClientCredsToken posts the client id and secret to obtain a token scoped to the OAuth2 client via the "client_credentials" grant type.
|
||||
// May not be supported by all OAuth2 servers.
|
||||
func (c *Client) ClientCredsToken(scope []string) (result TokenResponse, err error) {
|
||||
v := url.Values{
|
||||
"scope": {strings.Join(scope, " ")},
|
||||
"grant_type": {GrantTypeClientCreds},
|
||||
}
|
||||
|
||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return parseTokenResponse(resp)
|
||||
}
|
||||
|
||||
// UserCredsToken posts the username and password to obtain a token scoped to the OAuth2 client via the "password" grant_type
|
||||
// May not be supported by all OAuth2 servers.
|
||||
func (c *Client) UserCredsToken(username, password string) (result TokenResponse, err error) {
|
||||
v := url.Values{
|
||||
"scope": {strings.Join(c.scope, " ")},
|
||||
"grant_type": {GrantTypeUserCreds},
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
}
|
||||
|
||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return parseTokenResponse(resp)
|
||||
}
|
||||
|
||||
// RequestToken requests a token from the Token Endpoint with the specified grantType.
|
||||
// If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
|
||||
// If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
|
||||
func (c *Client) RequestToken(grantType, value string) (result TokenResponse, err error) {
|
||||
v := c.commonURLValues()
|
||||
|
||||
v.Set("grant_type", grantType)
|
||||
v.Set("client_secret", c.creds.Secret)
|
||||
switch grantType {
|
||||
case GrantTypeAuthCode:
|
||||
v.Set("code", value)
|
||||
case GrantTypeRefreshToken:
|
||||
v.Set("refresh_token", value)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported grant_type: %v", grantType)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return parseTokenResponse(resp)
|
||||
}
|
||||
|
||||
func parseTokenResponse(resp *http.Response) (result TokenResponse, err error) {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
badStatusCode := resp.StatusCode < 200 || resp.StatusCode > 299
|
||||
|
||||
contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result = TokenResponse{
|
||||
RawBody: body,
|
||||
}
|
||||
|
||||
newError := func(typ, desc, state string) error {
|
||||
if typ == "" {
|
||||
return fmt.Errorf("unrecognized error %s", body)
|
||||
}
|
||||
return &Error{typ, desc, state}
|
||||
}
|
||||
|
||||
if contentType == "application/x-www-form-urlencoded" || contentType == "text/plain" {
|
||||
var vals url.Values
|
||||
vals, err = url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if error := vals.Get("error"); error != "" || badStatusCode {
|
||||
err = newError(error, vals.Get("error_description"), vals.Get("state"))
|
||||
return
|
||||
}
|
||||
e := vals.Get("expires_in")
|
||||
if e == "" {
|
||||
e = vals.Get("expires")
|
||||
}
|
||||
if e != "" {
|
||||
result.Expires, err = strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
result.AccessToken = vals.Get("access_token")
|
||||
result.TokenType = vals.Get("token_type")
|
||||
result.IDToken = vals.Get("id_token")
|
||||
result.RefreshToken = vals.Get("refresh_token")
|
||||
result.Scope = vals.Get("scope")
|
||||
} else {
|
||||
var r struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
ExpiresIn json.Number `json:"expires_in"` // Azure AD returns string
|
||||
Expires int `json:"expires"`
|
||||
Error string `json:"error"`
|
||||
Desc string `json:"error_description"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &r); err != nil {
|
||||
return
|
||||
}
|
||||
if r.Error != "" || badStatusCode {
|
||||
err = newError(r.Error, r.Desc, r.State)
|
||||
return
|
||||
}
|
||||
result.AccessToken = r.AccessToken
|
||||
result.TokenType = r.TokenType
|
||||
result.IDToken = r.IDToken
|
||||
result.RefreshToken = r.RefreshToken
|
||||
result.Scope = r.Scope
|
||||
if expiresIn, err := r.ExpiresIn.Int64(); err != nil {
|
||||
result.Expires = r.Expires
|
||||
} else {
|
||||
result.Expires = int(expiresIn)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string
|
||||
TokenType string
|
||||
Expires int
|
||||
IDToken string
|
||||
RefreshToken string // OPTIONAL.
|
||||
Scope string // OPTIONAL, if identical to the scope requested by the client, otherwise, REQUIRED.
|
||||
RawBody []byte // In case callers need some other non-standard info from the token response
|
||||
}
|
||||
|
||||
type AuthCodeRequest struct {
|
||||
ResponseType string
|
||||
ClientID string
|
||||
RedirectURL *url.URL
|
||||
Scope []string
|
||||
State string
|
||||
}
|
||||
|
||||
func ParseAuthCodeRequest(q url.Values) (AuthCodeRequest, error) {
|
||||
acr := AuthCodeRequest{
|
||||
ResponseType: q.Get("response_type"),
|
||||
ClientID: q.Get("client_id"),
|
||||
State: q.Get("state"),
|
||||
Scope: make([]string, 0),
|
||||
}
|
||||
|
||||
qs := strings.TrimSpace(q.Get("scope"))
|
||||
if qs != "" {
|
||||
acr.Scope = strings.Split(qs, " ")
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
if acr.ClientID == "" {
|
||||
return NewError(ErrorInvalidRequest)
|
||||
}
|
||||
|
||||
redirectURL := q.Get("redirect_uri")
|
||||
if redirectURL != "" {
|
||||
ru, err := url.Parse(redirectURL)
|
||||
if err != nil {
|
||||
return NewError(ErrorInvalidRequest)
|
||||
}
|
||||
acr.RedirectURL = ru
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return acr, err
|
||||
}
|
||||
-846
@@ -1,846 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// amount of time that must pass after the last key sync
|
||||
// completes before another attempt may begin
|
||||
keySyncWindow = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultScope = []string{"openid", "email", "profile"}
|
||||
|
||||
supportedAuthMethods = map[string]struct{}{
|
||||
oauth2.AuthMethodClientSecretBasic: struct{}{},
|
||||
oauth2.AuthMethodClientSecretPost: struct{}{},
|
||||
}
|
||||
)
|
||||
|
||||
type ClientCredentials oauth2.ClientCredentials
|
||||
|
||||
type ClientIdentity struct {
|
||||
Credentials ClientCredentials
|
||||
Metadata ClientMetadata
|
||||
}
|
||||
|
||||
type JWAOptions struct {
|
||||
// SigningAlg specifies an JWA alg for signing JWTs.
|
||||
//
|
||||
// Specifying this field implies different actions depending on the context. It may
|
||||
// require objects be serialized and signed as a JWT instead of plain JSON, or
|
||||
// require an existing JWT object use the specified alg.
|
||||
//
|
||||
// See: http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
|
||||
SigningAlg string
|
||||
// EncryptionAlg, if provided, specifies that the returned or sent object be stored
|
||||
// (or nested) within a JWT object and encrypted with the provided JWA alg.
|
||||
EncryptionAlg string
|
||||
// EncryptionEnc specifies the JWA enc algorithm to use with EncryptionAlg. If
|
||||
// EncryptionAlg is provided and EncryptionEnc is omitted, this field defaults
|
||||
// to A128CBC-HS256.
|
||||
//
|
||||
// If EncryptionEnc is provided EncryptionAlg must also be specified.
|
||||
EncryptionEnc string
|
||||
}
|
||||
|
||||
func (opt JWAOptions) valid() error {
|
||||
if opt.EncryptionEnc != "" && opt.EncryptionAlg == "" {
|
||||
return errors.New("encryption encoding provided with no encryption algorithm")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt JWAOptions) defaults() JWAOptions {
|
||||
if opt.EncryptionAlg != "" && opt.EncryptionEnc == "" {
|
||||
opt.EncryptionEnc = jose.EncA128CBCHS256
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
var (
|
||||
// Ensure ClientMetadata satisfies these interfaces.
|
||||
_ json.Marshaler = &ClientMetadata{}
|
||||
_ json.Unmarshaler = &ClientMetadata{}
|
||||
)
|
||||
|
||||
// ClientMetadata holds metadata that the authorization server associates
|
||||
// with a client identifier. The fields range from human-facing display
|
||||
// strings such as client name, to items that impact the security of the
|
||||
// protocol, such as the list of valid redirect URIs.
|
||||
//
|
||||
// See http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
|
||||
//
|
||||
// TODO: support language specific claim representations
|
||||
// http://openid.net/specs/openid-connect-registration-1_0.html#LanguagesAndScripts
|
||||
type ClientMetadata struct {
|
||||
RedirectURIs []url.URL // Required
|
||||
|
||||
// A list of OAuth 2.0 "response_type" values that the client wishes to restrict
|
||||
// itself to. Either "code", "token", or another registered extension.
|
||||
//
|
||||
// If omitted, only "code" will be used.
|
||||
ResponseTypes []string
|
||||
// A list of OAuth 2.0 grant types the client wishes to restrict itself to.
|
||||
// The grant type values used by OIDC are "authorization_code", "implicit",
|
||||
// and "refresh_token".
|
||||
//
|
||||
// If ommitted, only "authorization_code" will be used.
|
||||
GrantTypes []string
|
||||
// "native" or "web". If omitted, "web".
|
||||
ApplicationType string
|
||||
|
||||
// List of email addresses.
|
||||
Contacts []mail.Address
|
||||
// Name of client to be presented to the end-user.
|
||||
ClientName string
|
||||
// URL that references a logo for the Client application.
|
||||
LogoURI *url.URL
|
||||
// URL of the home page of the Client.
|
||||
ClientURI *url.URL
|
||||
// Profile data policies and terms of use to be provided to the end user.
|
||||
PolicyURI *url.URL
|
||||
TermsOfServiceURI *url.URL
|
||||
|
||||
// URL to or the value of the client's JSON Web Key Set document.
|
||||
JWKSURI *url.URL
|
||||
JWKS *jose.JWKSet
|
||||
|
||||
// URL referencing a flie with a single JSON array of redirect URIs.
|
||||
SectorIdentifierURI *url.URL
|
||||
|
||||
SubjectType string
|
||||
|
||||
// Options to restrict the JWS alg and enc values used for server responses and requests.
|
||||
IDTokenResponseOptions JWAOptions
|
||||
UserInfoResponseOptions JWAOptions
|
||||
RequestObjectOptions JWAOptions
|
||||
|
||||
// Client requested authorization method and signing options for the token endpoint.
|
||||
//
|
||||
// Defaults to "client_secret_basic"
|
||||
TokenEndpointAuthMethod string
|
||||
TokenEndpointAuthSigningAlg string
|
||||
|
||||
// DefaultMaxAge specifies the maximum amount of time in seconds before an authorized
|
||||
// user must reauthroize.
|
||||
//
|
||||
// If 0, no limitation is placed on the maximum.
|
||||
DefaultMaxAge int64
|
||||
// RequireAuthTime specifies if the auth_time claim in the ID token is required.
|
||||
RequireAuthTime bool
|
||||
|
||||
// Default Authentication Context Class Reference values for authentication requests.
|
||||
DefaultACRValues []string
|
||||
|
||||
// URI that a third party can use to initiate a login by the relaying party.
|
||||
//
|
||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin
|
||||
InitiateLoginURI *url.URL
|
||||
// Pre-registered request_uri values that may be cached by the server.
|
||||
RequestURIs []url.URL
|
||||
}
|
||||
|
||||
// Defaults returns a shallow copy of ClientMetadata with default
|
||||
// values replacing omitted fields.
|
||||
func (m ClientMetadata) Defaults() ClientMetadata {
|
||||
if len(m.ResponseTypes) == 0 {
|
||||
m.ResponseTypes = []string{oauth2.ResponseTypeCode}
|
||||
}
|
||||
if len(m.GrantTypes) == 0 {
|
||||
m.GrantTypes = []string{oauth2.GrantTypeAuthCode}
|
||||
}
|
||||
if m.ApplicationType == "" {
|
||||
m.ApplicationType = "web"
|
||||
}
|
||||
if m.TokenEndpointAuthMethod == "" {
|
||||
m.TokenEndpointAuthMethod = oauth2.AuthMethodClientSecretBasic
|
||||
}
|
||||
m.IDTokenResponseOptions = m.IDTokenResponseOptions.defaults()
|
||||
m.UserInfoResponseOptions = m.UserInfoResponseOptions.defaults()
|
||||
m.RequestObjectOptions = m.RequestObjectOptions.defaults()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *ClientMetadata) MarshalJSON() ([]byte, error) {
|
||||
e := m.toEncodableStruct()
|
||||
return json.Marshal(&e)
|
||||
}
|
||||
|
||||
func (m *ClientMetadata) UnmarshalJSON(data []byte) error {
|
||||
var e encodableClientMetadata
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
meta, err := e.toStruct()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := meta.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = meta
|
||||
return nil
|
||||
}
|
||||
|
||||
type encodableClientMetadata struct {
|
||||
RedirectURIs []string `json:"redirect_uris"` // Required
|
||||
ResponseTypes []string `json:"response_types,omitempty"`
|
||||
GrantTypes []string `json:"grant_types,omitempty"`
|
||||
ApplicationType string `json:"application_type,omitempty"`
|
||||
Contacts []string `json:"contacts,omitempty"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
PolicyURI string `json:"policy_uri,omitempty"`
|
||||
TermsOfServiceURI string `json:"tos_uri,omitempty"`
|
||||
JWKSURI string `json:"jwks_uri,omitempty"`
|
||||
JWKS *jose.JWKSet `json:"jwks,omitempty"`
|
||||
SectorIdentifierURI string `json:"sector_identifier_uri,omitempty"`
|
||||
SubjectType string `json:"subject_type,omitempty"`
|
||||
IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"`
|
||||
IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty"`
|
||||
IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty"`
|
||||
UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"`
|
||||
UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"`
|
||||
UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty"`
|
||||
RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"`
|
||||
RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty"`
|
||||
RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"`
|
||||
DefaultMaxAge int64 `json:"default_max_age,omitempty"`
|
||||
RequireAuthTime bool `json:"require_auth_time,omitempty"`
|
||||
DefaultACRValues []string `json:"default_acr_values,omitempty"`
|
||||
InitiateLoginURI string `json:"initiate_login_uri,omitempty"`
|
||||
RequestURIs []string `json:"request_uris,omitempty"`
|
||||
}
|
||||
|
||||
func (c *encodableClientMetadata) toStruct() (ClientMetadata, error) {
|
||||
p := stickyErrParser{}
|
||||
m := ClientMetadata{
|
||||
RedirectURIs: p.parseURIs(c.RedirectURIs, "redirect_uris"),
|
||||
ResponseTypes: c.ResponseTypes,
|
||||
GrantTypes: c.GrantTypes,
|
||||
ApplicationType: c.ApplicationType,
|
||||
Contacts: p.parseEmails(c.Contacts, "contacts"),
|
||||
ClientName: c.ClientName,
|
||||
LogoURI: p.parseURI(c.LogoURI, "logo_uri"),
|
||||
ClientURI: p.parseURI(c.ClientURI, "client_uri"),
|
||||
PolicyURI: p.parseURI(c.PolicyURI, "policy_uri"),
|
||||
TermsOfServiceURI: p.parseURI(c.TermsOfServiceURI, "tos_uri"),
|
||||
JWKSURI: p.parseURI(c.JWKSURI, "jwks_uri"),
|
||||
JWKS: c.JWKS,
|
||||
SectorIdentifierURI: p.parseURI(c.SectorIdentifierURI, "sector_identifier_uri"),
|
||||
SubjectType: c.SubjectType,
|
||||
TokenEndpointAuthMethod: c.TokenEndpointAuthMethod,
|
||||
TokenEndpointAuthSigningAlg: c.TokenEndpointAuthSigningAlg,
|
||||
DefaultMaxAge: c.DefaultMaxAge,
|
||||
RequireAuthTime: c.RequireAuthTime,
|
||||
DefaultACRValues: c.DefaultACRValues,
|
||||
InitiateLoginURI: p.parseURI(c.InitiateLoginURI, "initiate_login_uri"),
|
||||
RequestURIs: p.parseURIs(c.RequestURIs, "request_uris"),
|
||||
IDTokenResponseOptions: JWAOptions{
|
||||
c.IDTokenSignedResponseAlg,
|
||||
c.IDTokenEncryptedResponseAlg,
|
||||
c.IDTokenEncryptedResponseEnc,
|
||||
},
|
||||
UserInfoResponseOptions: JWAOptions{
|
||||
c.UserInfoSignedResponseAlg,
|
||||
c.UserInfoEncryptedResponseAlg,
|
||||
c.UserInfoEncryptedResponseEnc,
|
||||
},
|
||||
RequestObjectOptions: JWAOptions{
|
||||
c.RequestObjectSigningAlg,
|
||||
c.RequestObjectEncryptionAlg,
|
||||
c.RequestObjectEncryptionEnc,
|
||||
},
|
||||
}
|
||||
if p.firstErr != nil {
|
||||
return ClientMetadata{}, p.firstErr
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// stickyErrParser parses URIs and email addresses. Once it encounters
|
||||
// a parse error, subsequent calls become no-op.
|
||||
type stickyErrParser struct {
|
||||
firstErr error
|
||||
}
|
||||
|
||||
func (p *stickyErrParser) parseURI(s, field string) *url.URL {
|
||||
if p.firstErr != nil || s == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
if u.Host == "" {
|
||||
err = errors.New("no host in URI")
|
||||
} else if u.Scheme != "http" && u.Scheme != "https" {
|
||||
err = errors.New("invalid URI scheme")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
p.firstErr = fmt.Errorf("failed to parse %s: %v", field, err)
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func (p *stickyErrParser) parseURIs(s []string, field string) []url.URL {
|
||||
if p.firstErr != nil || len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
uris := make([]url.URL, len(s))
|
||||
for i, val := range s {
|
||||
if val == "" {
|
||||
p.firstErr = fmt.Errorf("invalid URI in field %s", field)
|
||||
return nil
|
||||
}
|
||||
if u := p.parseURI(val, field); u != nil {
|
||||
uris[i] = *u
|
||||
}
|
||||
}
|
||||
return uris
|
||||
}
|
||||
|
||||
func (p *stickyErrParser) parseEmails(s []string, field string) []mail.Address {
|
||||
if p.firstErr != nil || len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
addrs := make([]mail.Address, len(s))
|
||||
for i, addr := range s {
|
||||
if addr == "" {
|
||||
p.firstErr = fmt.Errorf("invalid email in field %s", field)
|
||||
return nil
|
||||
}
|
||||
a, err := mail.ParseAddress(addr)
|
||||
if err != nil {
|
||||
p.firstErr = fmt.Errorf("invalid email in field %s: %v", field, err)
|
||||
return nil
|
||||
}
|
||||
addrs[i] = *a
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (m *ClientMetadata) toEncodableStruct() encodableClientMetadata {
|
||||
return encodableClientMetadata{
|
||||
RedirectURIs: urisToStrings(m.RedirectURIs),
|
||||
ResponseTypes: m.ResponseTypes,
|
||||
GrantTypes: m.GrantTypes,
|
||||
ApplicationType: m.ApplicationType,
|
||||
Contacts: emailsToStrings(m.Contacts),
|
||||
ClientName: m.ClientName,
|
||||
LogoURI: uriToString(m.LogoURI),
|
||||
ClientURI: uriToString(m.ClientURI),
|
||||
PolicyURI: uriToString(m.PolicyURI),
|
||||
TermsOfServiceURI: uriToString(m.TermsOfServiceURI),
|
||||
JWKSURI: uriToString(m.JWKSURI),
|
||||
JWKS: m.JWKS,
|
||||
SectorIdentifierURI: uriToString(m.SectorIdentifierURI),
|
||||
SubjectType: m.SubjectType,
|
||||
IDTokenSignedResponseAlg: m.IDTokenResponseOptions.SigningAlg,
|
||||
IDTokenEncryptedResponseAlg: m.IDTokenResponseOptions.EncryptionAlg,
|
||||
IDTokenEncryptedResponseEnc: m.IDTokenResponseOptions.EncryptionEnc,
|
||||
UserInfoSignedResponseAlg: m.UserInfoResponseOptions.SigningAlg,
|
||||
UserInfoEncryptedResponseAlg: m.UserInfoResponseOptions.EncryptionAlg,
|
||||
UserInfoEncryptedResponseEnc: m.UserInfoResponseOptions.EncryptionEnc,
|
||||
RequestObjectSigningAlg: m.RequestObjectOptions.SigningAlg,
|
||||
RequestObjectEncryptionAlg: m.RequestObjectOptions.EncryptionAlg,
|
||||
RequestObjectEncryptionEnc: m.RequestObjectOptions.EncryptionEnc,
|
||||
TokenEndpointAuthMethod: m.TokenEndpointAuthMethod,
|
||||
TokenEndpointAuthSigningAlg: m.TokenEndpointAuthSigningAlg,
|
||||
DefaultMaxAge: m.DefaultMaxAge,
|
||||
RequireAuthTime: m.RequireAuthTime,
|
||||
DefaultACRValues: m.DefaultACRValues,
|
||||
InitiateLoginURI: uriToString(m.InitiateLoginURI),
|
||||
RequestURIs: urisToStrings(m.RequestURIs),
|
||||
}
|
||||
}
|
||||
|
||||
func uriToString(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func urisToStrings(urls []url.URL) []string {
|
||||
if len(urls) == 0 {
|
||||
return nil
|
||||
}
|
||||
sli := make([]string, len(urls))
|
||||
for i, u := range urls {
|
||||
sli[i] = u.String()
|
||||
}
|
||||
return sli
|
||||
}
|
||||
|
||||
func emailsToStrings(addrs []mail.Address) []string {
|
||||
if len(addrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sli := make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
sli[i] = addr.String()
|
||||
}
|
||||
return sli
|
||||
}
|
||||
|
||||
// Valid determines if a ClientMetadata conforms with the OIDC specification.
|
||||
//
|
||||
// Valid is called by UnmarshalJSON.
|
||||
//
|
||||
// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
|
||||
// URLs fields where the OIDC spec requires it. This may change in future releases
|
||||
// of this package. See: https://github.com/coreos/go-oidc/issues/34
|
||||
func (m *ClientMetadata) Valid() error {
|
||||
if len(m.RedirectURIs) == 0 {
|
||||
return errors.New("zero redirect URLs")
|
||||
}
|
||||
|
||||
validURI := func(u *url.URL, fieldName string) error {
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("no host for uri field %s", fieldName)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("uri field %s scheme is not http or https", fieldName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
uris := []struct {
|
||||
val *url.URL
|
||||
name string
|
||||
}{
|
||||
{m.LogoURI, "logo_uri"},
|
||||
{m.ClientURI, "client_uri"},
|
||||
{m.PolicyURI, "policy_uri"},
|
||||
{m.TermsOfServiceURI, "tos_uri"},
|
||||
{m.JWKSURI, "jwks_uri"},
|
||||
{m.SectorIdentifierURI, "sector_identifier_uri"},
|
||||
{m.InitiateLoginURI, "initiate_login_uri"},
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
if uri.val == nil {
|
||||
continue
|
||||
}
|
||||
if err := validURI(uri.val, uri.name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
uriLists := []struct {
|
||||
vals []url.URL
|
||||
name string
|
||||
}{
|
||||
{m.RedirectURIs, "redirect_uris"},
|
||||
{m.RequestURIs, "request_uris"},
|
||||
}
|
||||
for _, list := range uriLists {
|
||||
for _, uri := range list.vals {
|
||||
if err := validURI(&uri, list.name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options := []struct {
|
||||
option JWAOptions
|
||||
name string
|
||||
}{
|
||||
{m.IDTokenResponseOptions, "id_token response"},
|
||||
{m.UserInfoResponseOptions, "userinfo response"},
|
||||
{m.RequestObjectOptions, "request_object"},
|
||||
}
|
||||
for _, option := range options {
|
||||
if err := option.option.valid(); err != nil {
|
||||
return fmt.Errorf("invalid JWA values for %s: %v", option.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ClientRegistrationResponse struct {
|
||||
ClientID string // Required
|
||||
ClientSecret string
|
||||
RegistrationAccessToken string
|
||||
RegistrationClientURI string
|
||||
// If IsZero is true, unspecified.
|
||||
ClientIDIssuedAt time.Time
|
||||
// Time at which the client_secret will expire.
|
||||
// If IsZero is true, it will not expire.
|
||||
ClientSecretExpiresAt time.Time
|
||||
|
||||
ClientMetadata
|
||||
}
|
||||
|
||||
type encodableClientRegistrationResponse struct {
|
||||
ClientID string `json:"client_id"` // Required
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
|
||||
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
||||
// Time at which the client_secret will expire, in seconds since the epoch.
|
||||
// If 0 it will not expire.
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"` // Required
|
||||
|
||||
encodableClientMetadata
|
||||
}
|
||||
|
||||
func unixToSec(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
func (c *ClientRegistrationResponse) MarshalJSON() ([]byte, error) {
|
||||
e := encodableClientRegistrationResponse{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
RegistrationAccessToken: c.RegistrationAccessToken,
|
||||
RegistrationClientURI: c.RegistrationClientURI,
|
||||
ClientIDIssuedAt: unixToSec(c.ClientIDIssuedAt),
|
||||
ClientSecretExpiresAt: unixToSec(c.ClientSecretExpiresAt),
|
||||
encodableClientMetadata: c.ClientMetadata.toEncodableStruct(),
|
||||
}
|
||||
return json.Marshal(&e)
|
||||
}
|
||||
|
||||
func secToUnix(sec int64) time.Time {
|
||||
if sec == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(sec, 0)
|
||||
}
|
||||
|
||||
func (c *ClientRegistrationResponse) UnmarshalJSON(data []byte) error {
|
||||
var e encodableClientRegistrationResponse
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.ClientID == "" {
|
||||
return errors.New("no client_id in client registration response")
|
||||
}
|
||||
metadata, err := e.encodableClientMetadata.toStruct()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*c = ClientRegistrationResponse{
|
||||
ClientID: e.ClientID,
|
||||
ClientSecret: e.ClientSecret,
|
||||
RegistrationAccessToken: e.RegistrationAccessToken,
|
||||
RegistrationClientURI: e.RegistrationClientURI,
|
||||
ClientIDIssuedAt: secToUnix(e.ClientIDIssuedAt),
|
||||
ClientSecretExpiresAt: secToUnix(e.ClientSecretExpiresAt),
|
||||
ClientMetadata: metadata,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
HTTPClient phttp.Client
|
||||
Credentials ClientCredentials
|
||||
Scope []string
|
||||
RedirectURL string
|
||||
ProviderConfig ProviderConfig
|
||||
KeySet key.PublicKeySet
|
||||
}
|
||||
|
||||
func NewClient(cfg ClientConfig) (*Client, error) {
|
||||
// Allow empty redirect URL in the case where the client
|
||||
// only needs to verify a given token.
|
||||
ru, err := url.Parse(cfg.RedirectURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid redirect URL: %v", err)
|
||||
}
|
||||
|
||||
c := Client{
|
||||
credentials: cfg.Credentials,
|
||||
httpClient: cfg.HTTPClient,
|
||||
scope: cfg.Scope,
|
||||
redirectURL: ru.String(),
|
||||
providerConfig: newProviderConfigRepo(cfg.ProviderConfig),
|
||||
keySet: cfg.KeySet,
|
||||
}
|
||||
|
||||
if c.httpClient == nil {
|
||||
c.httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
if c.scope == nil {
|
||||
c.scope = make([]string, len(DefaultScope))
|
||||
copy(c.scope, DefaultScope)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
httpClient phttp.Client
|
||||
providerConfig *providerConfigRepo
|
||||
credentials ClientCredentials
|
||||
redirectURL string
|
||||
scope []string
|
||||
keySet key.PublicKeySet
|
||||
providerSyncer *ProviderConfigSyncer
|
||||
|
||||
keySetSyncMutex sync.RWMutex
|
||||
lastKeySetSync time.Time
|
||||
}
|
||||
|
||||
func (c *Client) Healthy() error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
cfg := c.providerConfig.Get()
|
||||
|
||||
if cfg.Empty() {
|
||||
return errors.New("oidc client provider config empty")
|
||||
}
|
||||
|
||||
if !cfg.ExpiresAt.IsZero() && cfg.ExpiresAt.Before(now) {
|
||||
return errors.New("oidc client provider config expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) OAuthClient() (*oauth2.Client, error) {
|
||||
cfg := c.providerConfig.Get()
|
||||
authMethod, err := chooseAuthMethod(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ocfg := oauth2.Config{
|
||||
Credentials: oauth2.ClientCredentials(c.credentials),
|
||||
RedirectURL: c.redirectURL,
|
||||
AuthURL: cfg.AuthEndpoint.String(),
|
||||
TokenURL: cfg.TokenEndpoint.String(),
|
||||
Scope: c.scope,
|
||||
AuthMethod: authMethod,
|
||||
}
|
||||
|
||||
return oauth2.NewClient(c.httpClient, ocfg)
|
||||
}
|
||||
|
||||
func chooseAuthMethod(cfg ProviderConfig) (string, error) {
|
||||
if len(cfg.TokenEndpointAuthMethodsSupported) == 0 {
|
||||
return oauth2.AuthMethodClientSecretBasic, nil
|
||||
}
|
||||
|
||||
for _, authMethod := range cfg.TokenEndpointAuthMethodsSupported {
|
||||
if _, ok := supportedAuthMethods[authMethod]; ok {
|
||||
return authMethod, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no supported auth methods")
|
||||
}
|
||||
|
||||
// SyncProviderConfig starts the provider config syncer
|
||||
func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} {
|
||||
r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL)
|
||||
s := NewProviderConfigSyncer(r, c.providerConfig)
|
||||
stop := s.Run()
|
||||
s.WaitUntilInitialSync()
|
||||
return stop
|
||||
}
|
||||
|
||||
func (c *Client) maybeSyncKeys() error {
|
||||
tooSoon := func() bool {
|
||||
return time.Now().UTC().Before(c.lastKeySetSync.Add(keySyncWindow))
|
||||
}
|
||||
|
||||
// ignore request to sync keys if a sync operation has been
|
||||
// attempted too recently
|
||||
if tooSoon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.keySetSyncMutex.Lock()
|
||||
defer c.keySetSyncMutex.Unlock()
|
||||
|
||||
// check again, as another goroutine may have been holding
|
||||
// the lock while updating the keys
|
||||
if tooSoon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := c.providerConfig.Get()
|
||||
r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint.String())
|
||||
w := &clientKeyRepo{client: c}
|
||||
_, err := key.Sync(r, w)
|
||||
c.lastKeySetSync = time.Now().UTC()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type clientKeyRepo struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (r *clientKeyRepo) Set(ks key.KeySet) error {
|
||||
pks, ok := ks.(*key.PublicKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PublicKey")
|
||||
}
|
||||
r.client.keySet = *pks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ClientCredsToken(scope []string) (jose.JWT, error) {
|
||||
cfg := c.providerConfig.Get()
|
||||
|
||||
if !cfg.SupportsGrantType(oauth2.GrantTypeClientCreds) {
|
||||
return jose.JWT{}, fmt.Errorf("%v grant type is not supported", oauth2.GrantTypeClientCreds)
|
||||
}
|
||||
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.ClientCredsToken(scope)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
// ExchangeAuthCode exchanges an OAuth2 auth code for an OIDC JWT ID token.
|
||||
func (c *Client) ExchangeAuthCode(code string) (jose.JWT, error) {
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
// RefreshToken uses a refresh token to exchange for a new OIDC JWT ID Token.
|
||||
func (c *Client) RefreshToken(refreshToken string) (jose.JWT, error) {
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.RequestToken(oauth2.GrantTypeRefreshToken, refreshToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
func (c *Client) VerifyJWT(jwt jose.JWT) error {
|
||||
var keysFunc func() []key.PublicKey
|
||||
if kID, ok := jwt.KeyID(); ok {
|
||||
keysFunc = c.keysFuncWithID(kID)
|
||||
} else {
|
||||
keysFunc = c.keysFuncAll()
|
||||
}
|
||||
|
||||
v := NewJWTVerifier(
|
||||
c.providerConfig.Get().Issuer.String(),
|
||||
c.credentials.ID,
|
||||
c.maybeSyncKeys, keysFunc)
|
||||
|
||||
return v.Verify(jwt)
|
||||
}
|
||||
|
||||
// keysFuncWithID returns a function that retrieves at most unexpired
|
||||
// public key from the Client that matches the provided ID
|
||||
func (c *Client) keysFuncWithID(kID string) func() []key.PublicKey {
|
||||
return func() []key.PublicKey {
|
||||
c.keySetSyncMutex.RLock()
|
||||
defer c.keySetSyncMutex.RUnlock()
|
||||
|
||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
k := c.keySet.Key(kID)
|
||||
if k == nil {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
return []key.PublicKey{*k}
|
||||
}
|
||||
}
|
||||
|
||||
// keysFuncAll returns a function that retrieves all unexpired public
|
||||
// keys from the Client
|
||||
func (c *Client) keysFuncAll() func() []key.PublicKey {
|
||||
return func() []key.PublicKey {
|
||||
c.keySetSyncMutex.RLock()
|
||||
defer c.keySetSyncMutex.RUnlock()
|
||||
|
||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
return c.keySet.Keys()
|
||||
}
|
||||
}
|
||||
|
||||
type providerConfigRepo struct {
|
||||
mu sync.RWMutex
|
||||
config ProviderConfig // do not access directly, use Get()
|
||||
}
|
||||
|
||||
func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo {
|
||||
return &providerConfigRepo{sync.RWMutex{}, pc}
|
||||
}
|
||||
|
||||
// returns an error to implement ProviderConfigSetter
|
||||
func (r *providerConfigRepo) Set(cfg ProviderConfig) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.config = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *providerConfigRepo) Get() ProviderConfig {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.config
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Package oidc is DEPRECATED. Use github.com/coreos/go-oidc instead.
|
||||
package oidc
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
ID string
|
||||
Name string
|
||||
Email string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func IdentityFromClaims(claims jose.Claims) (*Identity, error) {
|
||||
if claims == nil {
|
||||
return nil, errors.New("nil claim set")
|
||||
}
|
||||
|
||||
var ident Identity
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
if ident.ID, ok, err = claims.StringClaim("sub"); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, errors.New("missing required claim: sub")
|
||||
}
|
||||
|
||||
if ident.Email, _, err = claims.StringClaim("email"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exp, ok, err := claims.TimeClaim("exp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
ident.ExpiresAt = exp
|
||||
}
|
||||
|
||||
return &ident, nil
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
package oidc
|
||||
|
||||
type LoginFunc func(ident Identity, sessionKey string) (redirectURL string, err error)
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
)
|
||||
|
||||
// DefaultPublicKeySetTTL is the default TTL set on the PublicKeySet if no
|
||||
// Cache-Control header is provided by the JWK Set document endpoint.
|
||||
const DefaultPublicKeySetTTL = 24 * time.Hour
|
||||
|
||||
// NewRemotePublicKeyRepo is responsible for fetching the JWK Set document.
|
||||
func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo {
|
||||
return &remotePublicKeyRepo{hc: hc, ep: ep}
|
||||
}
|
||||
|
||||
type remotePublicKeyRepo struct {
|
||||
hc phttp.Client
|
||||
ep string
|
||||
}
|
||||
|
||||
// Get returns a PublicKeySet fetched from the JWK Set document endpoint. A TTL
|
||||
// is set on the Key Set to avoid it having to be re-retrieved for every
|
||||
// encryption event. This TTL is typically controlled by the endpoint returning
|
||||
// a Cache-Control header, but defaults to 24 hours if no Cache-Control header
|
||||
// is found.
|
||||
func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
||||
req, err := http.NewRequest("GET", r.ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := r.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var d struct {
|
||||
Keys []jose.JWK `json:"keys"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(d.Keys) == 0 {
|
||||
return nil, errors.New("zero keys in response")
|
||||
}
|
||||
|
||||
ttl, ok, err := phttp.Cacheable(resp.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
ttl = DefaultPublicKeySetTTL
|
||||
}
|
||||
|
||||
exp := time.Now().UTC().Add(ttl)
|
||||
ks := key.NewPublicKeySet(d.Keys, exp)
|
||||
return ks, nil
|
||||
}
|
||||
-687
@@ -1,687 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/timeutil"
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// Subject Identifier types defined by the OIDC spec. Specifies if the provider
|
||||
// should provide the same sub claim value to all clients (public) or a unique
|
||||
// value for each client (pairwise).
|
||||
//
|
||||
// See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
||||
SubjectTypePublic = "public"
|
||||
SubjectTypePairwise = "pairwise"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default values for omitted provider config fields.
|
||||
//
|
||||
// Use ProviderConfig's Defaults method to fill a provider config with these values.
|
||||
DefaultGrantTypesSupported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit}
|
||||
DefaultResponseModesSupported = []string{"query", "fragment"}
|
||||
DefaultTokenEndpointAuthMethodsSupported = []string{oauth2.AuthMethodClientSecretBasic}
|
||||
DefaultClaimTypesSupported = []string{"normal"}
|
||||
)
|
||||
|
||||
const (
|
||||
MaximumProviderConfigSyncInterval = 24 * time.Hour
|
||||
MinimumProviderConfigSyncInterval = time.Minute
|
||||
|
||||
discoveryConfigPath = "/.well-known/openid-configuration"
|
||||
)
|
||||
|
||||
// internally configurable for tests
|
||||
var minimumProviderConfigSyncInterval = MinimumProviderConfigSyncInterval
|
||||
|
||||
var (
|
||||
// Ensure ProviderConfig satisfies these interfaces.
|
||||
_ json.Marshaler = &ProviderConfig{}
|
||||
_ json.Unmarshaler = &ProviderConfig{}
|
||||
)
|
||||
|
||||
// ProviderConfig represents the OpenID Provider Metadata specifying what
|
||||
// configurations a provider supports.
|
||||
//
|
||||
// See: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type ProviderConfig struct {
|
||||
Issuer *url.URL // Required
|
||||
AuthEndpoint *url.URL // Required
|
||||
TokenEndpoint *url.URL // Required if grant types other than "implicit" are supported
|
||||
UserInfoEndpoint *url.URL
|
||||
KeysEndpoint *url.URL // Required
|
||||
RegistrationEndpoint *url.URL
|
||||
EndSessionEndpoint *url.URL
|
||||
CheckSessionIFrame *url.URL
|
||||
|
||||
// Servers MAY choose not to advertise some supported scope values even when this
|
||||
// parameter is used, although those defined in OpenID Core SHOULD be listed, if supported.
|
||||
ScopesSupported []string
|
||||
// OAuth2.0 response types supported.
|
||||
ResponseTypesSupported []string // Required
|
||||
// OAuth2.0 response modes supported.
|
||||
//
|
||||
// If omitted, defaults to DefaultResponseModesSupported.
|
||||
ResponseModesSupported []string
|
||||
// OAuth2.0 grant types supported.
|
||||
//
|
||||
// If omitted, defaults to DefaultGrantTypesSupported.
|
||||
GrantTypesSupported []string
|
||||
ACRValuesSupported []string
|
||||
// SubjectTypesSupported specifies strategies for providing values for the sub claim.
|
||||
SubjectTypesSupported []string // Required
|
||||
|
||||
// JWA signing and encryption algorith values supported for ID tokens.
|
||||
IDTokenSigningAlgValues []string // Required
|
||||
IDTokenEncryptionAlgValues []string
|
||||
IDTokenEncryptionEncValues []string
|
||||
|
||||
// JWA signing and encryption algorith values supported for user info responses.
|
||||
UserInfoSigningAlgValues []string
|
||||
UserInfoEncryptionAlgValues []string
|
||||
UserInfoEncryptionEncValues []string
|
||||
|
||||
// JWA signing and encryption algorith values supported for request objects.
|
||||
ReqObjSigningAlgValues []string
|
||||
ReqObjEncryptionAlgValues []string
|
||||
ReqObjEncryptionEncValues []string
|
||||
|
||||
TokenEndpointAuthMethodsSupported []string
|
||||
TokenEndpointAuthSigningAlgValuesSupported []string
|
||||
DisplayValuesSupported []string
|
||||
ClaimTypesSupported []string
|
||||
ClaimsSupported []string
|
||||
ServiceDocs *url.URL
|
||||
ClaimsLocalsSupported []string
|
||||
UILocalsSupported []string
|
||||
ClaimsParameterSupported bool
|
||||
RequestParameterSupported bool
|
||||
RequestURIParamaterSupported bool
|
||||
RequireRequestURIRegistration bool
|
||||
|
||||
Policy *url.URL
|
||||
TermsOfService *url.URL
|
||||
|
||||
// Not part of the OpenID Provider Metadata
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Defaults returns a shallow copy of ProviderConfig with default
|
||||
// values replacing omitted fields.
|
||||
//
|
||||
// var cfg oidc.ProviderConfig
|
||||
// // Fill provider config with default values for omitted fields.
|
||||
// cfg = cfg.Defaults()
|
||||
//
|
||||
func (p ProviderConfig) Defaults() ProviderConfig {
|
||||
setDefault := func(val *[]string, defaultVal []string) {
|
||||
if len(*val) == 0 {
|
||||
*val = defaultVal
|
||||
}
|
||||
}
|
||||
setDefault(&p.GrantTypesSupported, DefaultGrantTypesSupported)
|
||||
setDefault(&p.ResponseModesSupported, DefaultResponseModesSupported)
|
||||
setDefault(&p.TokenEndpointAuthMethodsSupported, DefaultTokenEndpointAuthMethodsSupported)
|
||||
setDefault(&p.ClaimTypesSupported, DefaultClaimTypesSupported)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ProviderConfig) MarshalJSON() ([]byte, error) {
|
||||
e := p.toEncodableStruct()
|
||||
return json.Marshal(&e)
|
||||
}
|
||||
|
||||
func (p *ProviderConfig) UnmarshalJSON(data []byte) error {
|
||||
var e encodableProviderConfig
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return err
|
||||
}
|
||||
conf, err := e.toStruct()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := conf.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
*p = conf
|
||||
return nil
|
||||
}
|
||||
|
||||
type encodableProviderConfig struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
||||
KeysEndpoint string `json:"jwks_uri"`
|
||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||
CheckSessionIFrame string `json:"check_session_iframe,omitempty"`
|
||||
|
||||
// Use 'omitempty' for all slices as per OIDC spec:
|
||||
// "Claims that return multiple values are represented as JSON arrays.
|
||||
// Claims with zero elements MUST be omitted from the response."
|
||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
|
||||
|
||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
||||
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||
|
||||
IDTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
IDTokenEncryptionAlgValues []string `json:"id_token_encryption_alg_values_supported,omitempty"`
|
||||
IDTokenEncryptionEncValues []string `json:"id_token_encryption_enc_values_supported,omitempty"`
|
||||
UserInfoSigningAlgValues []string `json:"userinfo_signing_alg_values_supported,omitempty"`
|
||||
UserInfoEncryptionAlgValues []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
|
||||
UserInfoEncryptionEncValues []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
|
||||
ReqObjSigningAlgValues []string `json:"request_object_signing_alg_values_supported,omitempty"`
|
||||
ReqObjEncryptionAlgValues []string `json:"request_object_encryption_alg_values_supported,omitempty"`
|
||||
ReqObjEncryptionEncValues []string `json:"request_object_encryption_enc_values_supported,omitempty"`
|
||||
|
||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||
|
||||
DisplayValuesSupported []string `json:"display_values_supported,omitempty"`
|
||||
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
|
||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||
ServiceDocs string `json:"service_documentation,omitempty"`
|
||||
ClaimsLocalsSupported []string `json:"claims_locales_supported,omitempty"`
|
||||
UILocalsSupported []string `json:"ui_locales_supported,omitempty"`
|
||||
ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"`
|
||||
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
|
||||
RequestURIParamaterSupported bool `json:"request_uri_parameter_supported,omitempty"`
|
||||
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
|
||||
|
||||
Policy string `json:"op_policy_uri,omitempty"`
|
||||
TermsOfService string `json:"op_tos_uri,omitempty"`
|
||||
}
|
||||
|
||||
func (cfg ProviderConfig) toEncodableStruct() encodableProviderConfig {
|
||||
return encodableProviderConfig{
|
||||
Issuer: uriToString(cfg.Issuer),
|
||||
AuthEndpoint: uriToString(cfg.AuthEndpoint),
|
||||
TokenEndpoint: uriToString(cfg.TokenEndpoint),
|
||||
UserInfoEndpoint: uriToString(cfg.UserInfoEndpoint),
|
||||
KeysEndpoint: uriToString(cfg.KeysEndpoint),
|
||||
RegistrationEndpoint: uriToString(cfg.RegistrationEndpoint),
|
||||
EndSessionEndpoint: uriToString(cfg.EndSessionEndpoint),
|
||||
CheckSessionIFrame: uriToString(cfg.CheckSessionIFrame),
|
||||
ScopesSupported: cfg.ScopesSupported,
|
||||
ResponseTypesSupported: cfg.ResponseTypesSupported,
|
||||
ResponseModesSupported: cfg.ResponseModesSupported,
|
||||
GrantTypesSupported: cfg.GrantTypesSupported,
|
||||
ACRValuesSupported: cfg.ACRValuesSupported,
|
||||
SubjectTypesSupported: cfg.SubjectTypesSupported,
|
||||
IDTokenSigningAlgValues: cfg.IDTokenSigningAlgValues,
|
||||
IDTokenEncryptionAlgValues: cfg.IDTokenEncryptionAlgValues,
|
||||
IDTokenEncryptionEncValues: cfg.IDTokenEncryptionEncValues,
|
||||
UserInfoSigningAlgValues: cfg.UserInfoSigningAlgValues,
|
||||
UserInfoEncryptionAlgValues: cfg.UserInfoEncryptionAlgValues,
|
||||
UserInfoEncryptionEncValues: cfg.UserInfoEncryptionEncValues,
|
||||
ReqObjSigningAlgValues: cfg.ReqObjSigningAlgValues,
|
||||
ReqObjEncryptionAlgValues: cfg.ReqObjEncryptionAlgValues,
|
||||
ReqObjEncryptionEncValues: cfg.ReqObjEncryptionEncValues,
|
||||
TokenEndpointAuthMethodsSupported: cfg.TokenEndpointAuthMethodsSupported,
|
||||
TokenEndpointAuthSigningAlgValuesSupported: cfg.TokenEndpointAuthSigningAlgValuesSupported,
|
||||
DisplayValuesSupported: cfg.DisplayValuesSupported,
|
||||
ClaimTypesSupported: cfg.ClaimTypesSupported,
|
||||
ClaimsSupported: cfg.ClaimsSupported,
|
||||
ServiceDocs: uriToString(cfg.ServiceDocs),
|
||||
ClaimsLocalsSupported: cfg.ClaimsLocalsSupported,
|
||||
UILocalsSupported: cfg.UILocalsSupported,
|
||||
ClaimsParameterSupported: cfg.ClaimsParameterSupported,
|
||||
RequestParameterSupported: cfg.RequestParameterSupported,
|
||||
RequestURIParamaterSupported: cfg.RequestURIParamaterSupported,
|
||||
RequireRequestURIRegistration: cfg.RequireRequestURIRegistration,
|
||||
Policy: uriToString(cfg.Policy),
|
||||
TermsOfService: uriToString(cfg.TermsOfService),
|
||||
}
|
||||
}
|
||||
|
||||
func (e encodableProviderConfig) toStruct() (ProviderConfig, error) {
|
||||
p := stickyErrParser{}
|
||||
conf := ProviderConfig{
|
||||
Issuer: p.parseURI(e.Issuer, "issuer"),
|
||||
AuthEndpoint: p.parseURI(e.AuthEndpoint, "authorization_endpoint"),
|
||||
TokenEndpoint: p.parseURI(e.TokenEndpoint, "token_endpoint"),
|
||||
UserInfoEndpoint: p.parseURI(e.UserInfoEndpoint, "userinfo_endpoint"),
|
||||
KeysEndpoint: p.parseURI(e.KeysEndpoint, "jwks_uri"),
|
||||
RegistrationEndpoint: p.parseURI(e.RegistrationEndpoint, "registration_endpoint"),
|
||||
EndSessionEndpoint: p.parseURI(e.EndSessionEndpoint, "end_session_endpoint"),
|
||||
CheckSessionIFrame: p.parseURI(e.CheckSessionIFrame, "check_session_iframe"),
|
||||
ScopesSupported: e.ScopesSupported,
|
||||
ResponseTypesSupported: e.ResponseTypesSupported,
|
||||
ResponseModesSupported: e.ResponseModesSupported,
|
||||
GrantTypesSupported: e.GrantTypesSupported,
|
||||
ACRValuesSupported: e.ACRValuesSupported,
|
||||
SubjectTypesSupported: e.SubjectTypesSupported,
|
||||
IDTokenSigningAlgValues: e.IDTokenSigningAlgValues,
|
||||
IDTokenEncryptionAlgValues: e.IDTokenEncryptionAlgValues,
|
||||
IDTokenEncryptionEncValues: e.IDTokenEncryptionEncValues,
|
||||
UserInfoSigningAlgValues: e.UserInfoSigningAlgValues,
|
||||
UserInfoEncryptionAlgValues: e.UserInfoEncryptionAlgValues,
|
||||
UserInfoEncryptionEncValues: e.UserInfoEncryptionEncValues,
|
||||
ReqObjSigningAlgValues: e.ReqObjSigningAlgValues,
|
||||
ReqObjEncryptionAlgValues: e.ReqObjEncryptionAlgValues,
|
||||
ReqObjEncryptionEncValues: e.ReqObjEncryptionEncValues,
|
||||
TokenEndpointAuthMethodsSupported: e.TokenEndpointAuthMethodsSupported,
|
||||
TokenEndpointAuthSigningAlgValuesSupported: e.TokenEndpointAuthSigningAlgValuesSupported,
|
||||
DisplayValuesSupported: e.DisplayValuesSupported,
|
||||
ClaimTypesSupported: e.ClaimTypesSupported,
|
||||
ClaimsSupported: e.ClaimsSupported,
|
||||
ServiceDocs: p.parseURI(e.ServiceDocs, "service_documentation"),
|
||||
ClaimsLocalsSupported: e.ClaimsLocalsSupported,
|
||||
UILocalsSupported: e.UILocalsSupported,
|
||||
ClaimsParameterSupported: e.ClaimsParameterSupported,
|
||||
RequestParameterSupported: e.RequestParameterSupported,
|
||||
RequestURIParamaterSupported: e.RequestURIParamaterSupported,
|
||||
RequireRequestURIRegistration: e.RequireRequestURIRegistration,
|
||||
Policy: p.parseURI(e.Policy, "op_policy-uri"),
|
||||
TermsOfService: p.parseURI(e.TermsOfService, "op_tos_uri"),
|
||||
}
|
||||
if p.firstErr != nil {
|
||||
return ProviderConfig{}, p.firstErr
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// Empty returns if a ProviderConfig holds no information.
|
||||
//
|
||||
// This case generally indicates a ProviderConfigGetter has experienced an error
|
||||
// and has nothing to report.
|
||||
func (p ProviderConfig) Empty() bool {
|
||||
return p.Issuer == nil
|
||||
}
|
||||
|
||||
func contains(sli []string, ele string) bool {
|
||||
for _, s := range sli {
|
||||
if s == ele {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Valid determines if a ProviderConfig conforms with the OIDC specification.
|
||||
// If Valid returns successfully it guarantees required field are non-nil and
|
||||
// URLs are well formed.
|
||||
//
|
||||
// Valid is called by UnmarshalJSON.
|
||||
//
|
||||
// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
|
||||
// URLs fields where the OIDC spec requires it. This may change in future releases
|
||||
// of this package. See: https://github.com/coreos/go-oidc/issues/34
|
||||
func (p ProviderConfig) Valid() error {
|
||||
grantTypes := p.GrantTypesSupported
|
||||
if len(grantTypes) == 0 {
|
||||
grantTypes = DefaultGrantTypesSupported
|
||||
}
|
||||
implicitOnly := true
|
||||
for _, grantType := range grantTypes {
|
||||
if grantType != oauth2.GrantTypeImplicit {
|
||||
implicitOnly = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.SubjectTypesSupported) == 0 {
|
||||
return errors.New("missing required field subject_types_supported")
|
||||
}
|
||||
if len(p.IDTokenSigningAlgValues) == 0 {
|
||||
return errors.New("missing required field id_token_signing_alg_values_supported")
|
||||
}
|
||||
|
||||
if len(p.ScopesSupported) != 0 && !contains(p.ScopesSupported, "openid") {
|
||||
return errors.New("scoped_supported must be unspecified or include 'openid'")
|
||||
}
|
||||
|
||||
if !contains(p.IDTokenSigningAlgValues, "RS256") {
|
||||
return errors.New("id_token_signing_alg_values_supported must include 'RS256'")
|
||||
}
|
||||
|
||||
uris := []struct {
|
||||
val *url.URL
|
||||
name string
|
||||
required bool
|
||||
}{
|
||||
{p.Issuer, "issuer", true},
|
||||
{p.AuthEndpoint, "authorization_endpoint", true},
|
||||
{p.TokenEndpoint, "token_endpoint", !implicitOnly},
|
||||
{p.UserInfoEndpoint, "userinfo_endpoint", false},
|
||||
{p.KeysEndpoint, "jwks_uri", true},
|
||||
{p.RegistrationEndpoint, "registration_endpoint", false},
|
||||
{p.EndSessionEndpoint, "end_session_endpoint", false},
|
||||
{p.CheckSessionIFrame, "check_session_iframe", false},
|
||||
{p.ServiceDocs, "service_documentation", false},
|
||||
{p.Policy, "op_policy_uri", false},
|
||||
{p.TermsOfService, "op_tos_uri", false},
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
if uri.val == nil {
|
||||
if !uri.required {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("empty value for required uri field %s", uri.name)
|
||||
}
|
||||
if uri.val.Host == "" {
|
||||
return fmt.Errorf("no host for uri field %s", uri.name)
|
||||
}
|
||||
if uri.val.Scheme != "http" && uri.val.Scheme != "https" {
|
||||
return fmt.Errorf("uri field %s schemeis not http or https", uri.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Supports determines if provider supports a client given their respective metadata.
|
||||
func (p ProviderConfig) Supports(c ClientMetadata) error {
|
||||
if err := p.Valid(); err != nil {
|
||||
return fmt.Errorf("invalid provider config: %v", err)
|
||||
}
|
||||
if err := c.Valid(); err != nil {
|
||||
return fmt.Errorf("invalid client config: %v", err)
|
||||
}
|
||||
|
||||
// Fill default values for omitted fields
|
||||
c = c.Defaults()
|
||||
p = p.Defaults()
|
||||
|
||||
// Do the supported values list the requested one?
|
||||
supports := []struct {
|
||||
supported []string
|
||||
requested string
|
||||
name string
|
||||
}{
|
||||
{p.IDTokenSigningAlgValues, c.IDTokenResponseOptions.SigningAlg, "id_token_signed_response_alg"},
|
||||
{p.IDTokenEncryptionAlgValues, c.IDTokenResponseOptions.EncryptionAlg, "id_token_encryption_response_alg"},
|
||||
{p.IDTokenEncryptionEncValues, c.IDTokenResponseOptions.EncryptionEnc, "id_token_encryption_response_enc"},
|
||||
{p.UserInfoSigningAlgValues, c.UserInfoResponseOptions.SigningAlg, "userinfo_signed_response_alg"},
|
||||
{p.UserInfoEncryptionAlgValues, c.UserInfoResponseOptions.EncryptionAlg, "userinfo_encryption_response_alg"},
|
||||
{p.UserInfoEncryptionEncValues, c.UserInfoResponseOptions.EncryptionEnc, "userinfo_encryption_response_enc"},
|
||||
{p.ReqObjSigningAlgValues, c.RequestObjectOptions.SigningAlg, "request_object_signing_alg"},
|
||||
{p.ReqObjEncryptionAlgValues, c.RequestObjectOptions.EncryptionAlg, "request_object_encryption_alg"},
|
||||
{p.ReqObjEncryptionEncValues, c.RequestObjectOptions.EncryptionEnc, "request_object_encryption_enc"},
|
||||
}
|
||||
for _, field := range supports {
|
||||
if field.requested == "" {
|
||||
continue
|
||||
}
|
||||
if !contains(field.supported, field.requested) {
|
||||
return fmt.Errorf("provider does not support requested value for field %s", field.name)
|
||||
}
|
||||
}
|
||||
|
||||
stringsEqual := func(s1, s2 string) bool { return s1 == s2 }
|
||||
|
||||
// For lists, are the list of requested values a subset of the supported ones?
|
||||
supportsAll := []struct {
|
||||
supported []string
|
||||
requested []string
|
||||
name string
|
||||
// OAuth2.0 response_type can be space separated lists where order doesn't matter.
|
||||
// For example "id_token token" is the same as "token id_token"
|
||||
// Support a custom compare method.
|
||||
comp func(s1, s2 string) bool
|
||||
}{
|
||||
{p.GrantTypesSupported, c.GrantTypes, "grant_types", stringsEqual},
|
||||
{p.ResponseTypesSupported, c.ResponseTypes, "response_type", oauth2.ResponseTypesEqual},
|
||||
}
|
||||
for _, field := range supportsAll {
|
||||
requestLoop:
|
||||
for _, req := range field.requested {
|
||||
for _, sup := range field.supported {
|
||||
if field.comp(req, sup) {
|
||||
continue requestLoop
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("provider does not support requested value for field %s", field.name)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(ericchiang): Are there more checks we feel comfortable with begin strict about?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p ProviderConfig) SupportsGrantType(grantType string) bool {
|
||||
var supported []string
|
||||
if len(p.GrantTypesSupported) == 0 {
|
||||
supported = DefaultGrantTypesSupported
|
||||
} else {
|
||||
supported = p.GrantTypesSupported
|
||||
}
|
||||
|
||||
for _, t := range supported {
|
||||
if t == grantType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ProviderConfigGetter interface {
|
||||
Get() (ProviderConfig, error)
|
||||
}
|
||||
|
||||
type ProviderConfigSetter interface {
|
||||
Set(ProviderConfig) error
|
||||
}
|
||||
|
||||
type ProviderConfigSyncer struct {
|
||||
from ProviderConfigGetter
|
||||
to ProviderConfigSetter
|
||||
clock clockwork.Clock
|
||||
|
||||
initialSyncDone bool
|
||||
initialSyncWait sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer {
|
||||
return &ProviderConfigSyncer{
|
||||
from: from,
|
||||
to: to,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProviderConfigSyncer) Run() chan struct{} {
|
||||
stop := make(chan struct{})
|
||||
|
||||
var next pcsStepper
|
||||
next = &pcsStepNext{aft: time.Duration(0)}
|
||||
|
||||
s.initialSyncWait.Add(1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.clock.After(next.after()):
|
||||
next = next.step(s.sync)
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func (s *ProviderConfigSyncer) WaitUntilInitialSync() {
|
||||
s.initialSyncWait.Wait()
|
||||
}
|
||||
|
||||
func (s *ProviderConfigSyncer) sync() (time.Duration, error) {
|
||||
cfg, err := s.from.Get()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err = s.to.Set(cfg); err != nil {
|
||||
return 0, fmt.Errorf("error setting provider config: %v", err)
|
||||
}
|
||||
|
||||
if !s.initialSyncDone {
|
||||
s.initialSyncWait.Done()
|
||||
s.initialSyncDone = true
|
||||
}
|
||||
|
||||
return nextSyncAfter(cfg.ExpiresAt, s.clock), nil
|
||||
}
|
||||
|
||||
type pcsStepFunc func() (time.Duration, error)
|
||||
|
||||
type pcsStepper interface {
|
||||
after() time.Duration
|
||||
step(pcsStepFunc) pcsStepper
|
||||
}
|
||||
|
||||
type pcsStepNext struct {
|
||||
aft time.Duration
|
||||
}
|
||||
|
||||
func (n *pcsStepNext) after() time.Duration {
|
||||
return n.aft
|
||||
}
|
||||
|
||||
func (n *pcsStepNext) step(fn pcsStepFunc) (next pcsStepper) {
|
||||
ttl, err := fn()
|
||||
if err == nil {
|
||||
next = &pcsStepNext{aft: ttl}
|
||||
} else {
|
||||
next = &pcsStepRetry{aft: time.Second}
|
||||
log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type pcsStepRetry struct {
|
||||
aft time.Duration
|
||||
}
|
||||
|
||||
func (r *pcsStepRetry) after() time.Duration {
|
||||
return r.aft
|
||||
}
|
||||
|
||||
func (r *pcsStepRetry) step(fn pcsStepFunc) (next pcsStepper) {
|
||||
ttl, err := fn()
|
||||
if err == nil {
|
||||
next = &pcsStepNext{aft: ttl}
|
||||
} else {
|
||||
next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)}
|
||||
log.Printf("go-oidc: provider config sync failed, retrying in %v: %v", next.after(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func nextSyncAfter(exp time.Time, clock clockwork.Clock) time.Duration {
|
||||
if exp.IsZero() {
|
||||
return MaximumProviderConfigSyncInterval
|
||||
}
|
||||
|
||||
t := exp.Sub(clock.Now()) / 2
|
||||
if t > MaximumProviderConfigSyncInterval {
|
||||
t = MaximumProviderConfigSyncInterval
|
||||
} else if t < minimumProviderConfigSyncInterval {
|
||||
t = minimumProviderConfigSyncInterval
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
type httpProviderConfigGetter struct {
|
||||
hc phttp.Client
|
||||
issuerURL string
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProviderConfigGetter {
|
||||
return &httpProviderConfigGetter{
|
||||
hc: hc,
|
||||
issuerURL: issuerURL,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
|
||||
// If the Issuer value contains a path component, any terminating / MUST be removed before
|
||||
// appending /.well-known/openid-configuration.
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
|
||||
discoveryURL := strings.TrimSuffix(r.issuerURL, "/") + discoveryConfigPath
|
||||
req, err := http.NewRequest("GET", discoveryURL, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := r.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err = json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var ttl time.Duration
|
||||
var ok bool
|
||||
ttl, ok, err = phttp.Cacheable(resp.Header)
|
||||
if err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
cfg.ExpiresAt = r.clock.Now().UTC().Add(ttl)
|
||||
}
|
||||
|
||||
// The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.
|
||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
|
||||
if !urlEqual(cfg.Issuer.String(), r.issuerURL) {
|
||||
err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FetchProviderConfig(hc phttp.Client, issuerURL string) (ProviderConfig, error) {
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
|
||||
g := NewHTTPProviderConfigGetter(hc, issuerURL)
|
||||
return g.Get()
|
||||
}
|
||||
|
||||
func WaitForProviderConfig(hc phttp.Client, issuerURL string) (pcfg ProviderConfig) {
|
||||
return waitForProviderConfig(hc, issuerURL, clockwork.NewRealClock())
|
||||
}
|
||||
|
||||
func waitForProviderConfig(hc phttp.Client, issuerURL string, clock clockwork.Clock) (pcfg ProviderConfig) {
|
||||
var sleep time.Duration
|
||||
var err error
|
||||
for {
|
||||
pcfg, err = FetchProviderConfig(hc, issuerURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
sleep = timeutil.ExpBackoff(sleep, time.Minute)
|
||||
fmt.Printf("Failed fetching provider config, trying again in %v: %v\n", sleep, err)
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
type TokenRefresher interface {
|
||||
// Verify checks if the provided token is currently valid or not.
|
||||
Verify(jose.JWT) error
|
||||
|
||||
// Refresh attempts to authenticate and retrieve a new token.
|
||||
Refresh() (jose.JWT, error)
|
||||
}
|
||||
|
||||
type ClientCredsTokenRefresher struct {
|
||||
Issuer string
|
||||
OIDCClient *Client
|
||||
}
|
||||
|
||||
func (c *ClientCredsTokenRefresher) Verify(jwt jose.JWT) (err error) {
|
||||
_, err = VerifyClientClaims(jwt, c.Issuer)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ClientCredsTokenRefresher) Refresh() (jwt jose.JWT, err error) {
|
||||
if err = c.OIDCClient.Healthy(); err != nil {
|
||||
err = fmt.Errorf("unable to authenticate, unhealthy OIDC client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err = c.OIDCClient.ClientCredsToken([]string{"openid"})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to verify auth code with issuer: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type AuthenticatedTransport struct {
|
||||
TokenRefresher
|
||||
http.RoundTripper
|
||||
|
||||
mu sync.Mutex
|
||||
jwt jose.JWT
|
||||
}
|
||||
|
||||
func (t *AuthenticatedTransport) verifiedJWT() (jose.JWT, error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.TokenRefresher.Verify(t.jwt) == nil {
|
||||
return t.jwt, nil
|
||||
}
|
||||
|
||||
jwt, err := t.TokenRefresher.Refresh()
|
||||
if err != nil {
|
||||
return jose.JWT{}, fmt.Errorf("unable to acquire valid JWT: %v", err)
|
||||
}
|
||||
|
||||
t.jwt = jwt
|
||||
return t.jwt, nil
|
||||
}
|
||||
|
||||
// SetJWT sets the JWT held by the Transport.
|
||||
// This is useful for cases in which you want to set an initial JWT.
|
||||
func (t *AuthenticatedTransport) SetJWT(jwt jose.JWT) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.jwt = jwt
|
||||
}
|
||||
|
||||
func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
jwt, err := t.verifiedJWT()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := phttp.CopyRequest(r)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt.Encode()))
|
||||
return t.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
// RequestTokenExtractor funcs extract a raw encoded token from a request.
|
||||
type RequestTokenExtractor func(r *http.Request) (string, error)
|
||||
|
||||
// ExtractBearerToken is a RequestTokenExtractor which extracts a bearer token from a request's
|
||||
// Authorization header.
|
||||
func ExtractBearerToken(r *http.Request) (string, error) {
|
||||
ah := r.Header.Get("Authorization")
|
||||
if ah == "" {
|
||||
return "", errors.New("missing Authorization header")
|
||||
}
|
||||
|
||||
if len(ah) <= 6 || strings.ToUpper(ah[0:6]) != "BEARER" {
|
||||
return "", errors.New("should be a bearer token")
|
||||
}
|
||||
|
||||
val := ah[7:]
|
||||
if len(val) == 0 {
|
||||
return "", errors.New("bearer token is empty")
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// CookieTokenExtractor returns a RequestTokenExtractor which extracts a token from the named cookie in a request.
|
||||
func CookieTokenExtractor(cookieName string) RequestTokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
ck, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("token cookie not found in request: %v", err)
|
||||
}
|
||||
|
||||
if ck.Value == "" {
|
||||
return "", errors.New("token cookie found but is empty")
|
||||
}
|
||||
|
||||
return ck.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewClaims(iss, sub string, aud interface{}, iat, exp time.Time) jose.Claims {
|
||||
return jose.Claims{
|
||||
// required
|
||||
"iss": iss,
|
||||
"sub": sub,
|
||||
"aud": aud,
|
||||
"iat": iat.Unix(),
|
||||
"exp": exp.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func GenClientID(hostport string) (string, error) {
|
||||
b, err := randBytes(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var host string
|
||||
if strings.Contains(hostport, ":") {
|
||||
host, _, err = net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
host = hostport
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s@%s", base64.URLEncoding.EncodeToString(b), host), nil
|
||||
}
|
||||
|
||||
func randBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
got, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if n != got {
|
||||
return nil, errors.New("unable to generate enough random data")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// urlEqual checks two urls for equality using only the host and path portions.
|
||||
func urlEqual(url1, url2 string) bool {
|
||||
u1, err := url.Parse(url1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
u2, err := url.Parse(url2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.ToLower(u1.Host+u1.Path) == strings.ToLower(u2.Host+u2.Path)
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
)
|
||||
|
||||
func VerifySignature(jwt jose.JWT, keys []key.PublicKey) (bool, error) {
|
||||
jwtBytes := []byte(jwt.Data())
|
||||
for _, k := range keys {
|
||||
v, err := k.Verifier()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if v.Verify(jwt.Signature, jwtBytes) == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// containsString returns true if the given string(needle) is found
|
||||
// in the string array(haystack).
|
||||
func containsString(needle string, haystack []string) bool {
|
||||
for _, v := range haystack {
|
||||
if v == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify claims in accordance with OIDC spec
|
||||
// http://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
|
||||
func VerifyClaims(jwt jose.JWT, issuer, clientID string) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ident, err := IdentityFromClaims(claims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ident.ExpiresAt.Before(now) {
|
||||
return errors.New("token is expired")
|
||||
}
|
||||
|
||||
// iss REQUIRED. Issuer Identifier for the Issuer of the response.
|
||||
// The iss value is a case sensitive URL using the https scheme that contains scheme,
|
||||
// host, and optionally, port number and path components and no query or fragment components.
|
||||
if iss, exists := claims["iss"].(string); exists {
|
||||
if !urlEqual(iss, issuer) {
|
||||
return fmt.Errorf("invalid claim value: 'iss'. expected=%s, found=%s.", issuer, iss)
|
||||
}
|
||||
} else {
|
||||
return errors.New("missing claim: 'iss'")
|
||||
}
|
||||
|
||||
// iat REQUIRED. Time at which the JWT was issued.
|
||||
// Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z
|
||||
// as measured in UTC until the date/time.
|
||||
if _, exists := claims["iat"].(float64); !exists {
|
||||
return errors.New("missing claim: 'iat'")
|
||||
}
|
||||
|
||||
// aud REQUIRED. Audience(s) that this ID Token is intended for.
|
||||
// It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value.
|
||||
// It MAY also contain identifiers for other audiences. In the general case, the aud
|
||||
// value is an array of case sensitive strings. In the common special case when there
|
||||
// is one audience, the aud value MAY be a single case sensitive string.
|
||||
if aud, ok, err := claims.StringClaim("aud"); err == nil && ok {
|
||||
if aud != clientID {
|
||||
return fmt.Errorf("invalid claims, 'aud' claim and 'client_id' do not match, aud=%s, client_id=%s", aud, clientID)
|
||||
}
|
||||
} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok {
|
||||
if !containsString(clientID, aud) {
|
||||
return fmt.Errorf("invalid claims, cannot find 'client_id' in 'aud' claim, aud=%v, client_id=%s", aud, clientID)
|
||||
}
|
||||
} else {
|
||||
return errors.New("invalid claim value: 'aud' is required, and should be either string or string array")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyClientClaims verifies all the required claims are valid for a "client credentials" JWT.
|
||||
// Returns the client ID if valid, or an error if invalid.
|
||||
func VerifyClientClaims(jwt jose.JWT, issuer string) (string, error) {
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse JWT claims: %v", err)
|
||||
}
|
||||
|
||||
iss, ok, err := claims.StringClaim("iss")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'iss' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'iss' claim")
|
||||
} else if !urlEqual(iss, issuer) {
|
||||
return "", fmt.Errorf("'iss' claim does not match expected issuer, iss=%s", iss)
|
||||
}
|
||||
|
||||
sub, ok, err := claims.StringClaim("sub")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'sub' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'sub' claim")
|
||||
}
|
||||
|
||||
if aud, ok, err := claims.StringClaim("aud"); err == nil && ok {
|
||||
if aud != sub {
|
||||
return "", fmt.Errorf("invalid claims, 'aud' claim and 'sub' claim do not match, aud=%s, sub=%s", aud, sub)
|
||||
}
|
||||
} else if aud, ok, err := claims.StringsClaim("aud"); err == nil && ok {
|
||||
if !containsString(sub, aud) {
|
||||
return "", fmt.Errorf("invalid claims, cannot find 'sud' in 'aud' claim, aud=%v, sub=%s", aud, sub)
|
||||
}
|
||||
} else {
|
||||
return "", errors.New("invalid claim value: 'aud' is required, and should be either string or string array")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
exp, ok, err := claims.TimeClaim("exp")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'exp' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'exp' claim")
|
||||
} else if exp.Before(now) {
|
||||
return "", fmt.Errorf("token already expired at: %v", exp)
|
||||
}
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
type JWTVerifier struct {
|
||||
issuer string
|
||||
clientID string
|
||||
syncFunc func() error
|
||||
keysFunc func() []key.PublicKey
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewJWTVerifier(issuer, clientID string, syncFunc func() error, keysFunc func() []key.PublicKey) JWTVerifier {
|
||||
return JWTVerifier{
|
||||
issuer: issuer,
|
||||
clientID: clientID,
|
||||
syncFunc: syncFunc,
|
||||
keysFunc: keysFunc,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *JWTVerifier) Verify(jwt jose.JWT) error {
|
||||
// Verify claims before verifying the signature. This is an optimization to throw out
|
||||
// tokens we know are invalid without undergoing an expensive signature check and
|
||||
// possibly a re-sync event.
|
||||
if err := VerifyClaims(jwt, v.issuer, v.clientID); err != nil {
|
||||
return fmt.Errorf("oidc: JWT claims invalid: %v", err)
|
||||
}
|
||||
|
||||
ok, err := VerifySignature(jwt, v.keysFunc())
|
||||
if err != nil {
|
||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
||||
} else if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = v.syncFunc(); err != nil {
|
||||
return fmt.Errorf("oidc: failed syncing KeySet: %v", err)
|
||||
}
|
||||
|
||||
ok, err = VerifySignature(jwt, v.keysFunc())
|
||||
if err != nil {
|
||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
||||
} else if !ok {
|
||||
return errors.New("oidc: unable to verify JWT signature: no matching keys")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
CoreOS Project
|
||||
Copyright 2014 CoreOS, Inc
|
||||
|
||||
This product includes software developed at CoreOS, Inc.
|
||||
(http://www.coreos.com/).
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
health
|
||||
====
|
||||
|
||||
A simple framework for implementing an HTTP health check endpoint on servers.
|
||||
|
||||
Users implement their `health.Checkable` types, and create a `health.Checker`, from which they can get an `http.HandlerFunc` using `health.Checker.MakeHealthHandlerFunc`.
|
||||
|
||||
### Documentation
|
||||
|
||||
For more details, visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/health)
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/pkg/httputil"
|
||||
)
|
||||
|
||||
// Checkables should return nil when the thing they are checking is healthy, and an error otherwise.
|
||||
type Checkable interface {
|
||||
Healthy() error
|
||||
}
|
||||
|
||||
// Checker provides a way to make an endpoint which can be probed for system health.
|
||||
type Checker struct {
|
||||
// Checks are the Checkables to be checked when probing.
|
||||
Checks []Checkable
|
||||
|
||||
// Unhealthyhandler is called when one or more of the checks are unhealthy.
|
||||
// If not provided DefaultUnhealthyHandler is called.
|
||||
UnhealthyHandler UnhealthyHandler
|
||||
|
||||
// HealthyHandler is called when all checks are healthy.
|
||||
// If not provided, DefaultHealthyHandler is called.
|
||||
HealthyHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
func (c Checker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
unhealthyHandler := c.UnhealthyHandler
|
||||
if unhealthyHandler == nil {
|
||||
unhealthyHandler = DefaultUnhealthyHandler
|
||||
}
|
||||
|
||||
successHandler := c.HealthyHandler
|
||||
if successHandler == nil {
|
||||
successHandler = DefaultHealthyHandler
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
w.Header().Set("Allow", "GET")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Check(c.Checks); err != nil {
|
||||
unhealthyHandler(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
successHandler(w, r)
|
||||
}
|
||||
|
||||
type UnhealthyHandler func(w http.ResponseWriter, r *http.Request, err error)
|
||||
|
||||
type StatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Details *StatusResponseDetails `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type StatusResponseDetails struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func Check(checks []Checkable) (err error) {
|
||||
errs := []error{}
|
||||
for _, c := range checks {
|
||||
if e := c.Healthy(); e != nil {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
err = nil
|
||||
case 1:
|
||||
err = errs[0]
|
||||
default:
|
||||
err = fmt.Errorf("multiple health check failure: %v", errs)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func DefaultHealthyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := httputil.WriteJSONResponse(w, http.StatusOK, StatusResponse{
|
||||
Status: "ok",
|
||||
})
|
||||
if err != nil {
|
||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
||||
// once it lands.
|
||||
log.Printf("Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultUnhealthyHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||
writeErr := httputil.WriteJSONResponse(w, http.StatusInternalServerError, StatusResponse{
|
||||
Status: "error",
|
||||
Details: &StatusResponseDetails{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
},
|
||||
})
|
||||
if writeErr != nil {
|
||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
||||
// once it lands.
|
||||
log.Printf("Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ExpvarHandler is copied from https://golang.org/src/expvar/expvar.go, where it's sadly unexported.
|
||||
func ExpvarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprintf(w, "{\n")
|
||||
first := true
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
if !first {
|
||||
fmt.Fprintf(w, ",\n")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
||||
})
|
||||
fmt.Fprintf(w, "\n}\n")
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
httputil
|
||||
====
|
||||
|
||||
Common code for dealing with HTTP.
|
||||
|
||||
Includes:
|
||||
|
||||
* Code for returning JSON responses.
|
||||
|
||||
### Documentation
|
||||
|
||||
Visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/httputil)
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeleteCookies effectively deletes all named cookies
|
||||
// by wiping all data and setting to expire immediately.
|
||||
func DeleteCookies(w http.ResponseWriter, cookieNames ...string) {
|
||||
for _, n := range cookieNames {
|
||||
c := &http.Cookie{
|
||||
Name: n,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Expires: time.Time{},
|
||||
}
|
||||
http.SetCookie(w, c)
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONContentType = "application/json"
|
||||
)
|
||||
|
||||
func WriteJSONResponse(w http.ResponseWriter, code int, resp interface{}) error {
|
||||
enc, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", JSONContentType)
|
||||
w.WriteHeader(code)
|
||||
|
||||
_, err = w.Write(enc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package timeutil
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExpBackoff(prev, max time.Duration) time.Duration {
|
||||
if prev == 0 {
|
||||
return time.Second
|
||||
}
|
||||
if prev > max/2 {
|
||||
return max
|
||||
}
|
||||
return 2 * prev
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
|
||||
*.swp
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.3
|
||||
|
||||
sudo: false
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
clockwork
|
||||
=========
|
||||
|
||||
[](https://travis-ci.org/jonboulle/clockwork)
|
||||
[](http://godoc.org/github.com/jonboulle/clockwork)
|
||||
|
||||
a simple fake clock for golang
|
||||
|
||||
# Usage
|
||||
|
||||
Replace uses of the `time` package with the `clockwork.Clock` interface instead.
|
||||
|
||||
For example, instead of using `time.Sleep` directly:
|
||||
|
||||
```
|
||||
func my_func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
do_something()
|
||||
}
|
||||
```
|
||||
|
||||
inject a clock and use its `Sleep` method instead:
|
||||
|
||||
```
|
||||
func my_func(clock clockwork.Clock) {
|
||||
clock.Sleep(3 * time.Second)
|
||||
do_something()
|
||||
}
|
||||
```
|
||||
|
||||
Now you can easily test `my_func` with a `FakeClock`:
|
||||
|
||||
```
|
||||
func TestMyFunc(t *testing.T) {
|
||||
c := clockwork.NewFakeClock()
|
||||
|
||||
// Start our sleepy function
|
||||
my_func(c)
|
||||
|
||||
// Ensure we wait until my_func is sleeping
|
||||
c.BlockUntil(1)
|
||||
|
||||
assert_state()
|
||||
|
||||
// Advance the FakeClock forward in time
|
||||
c.Advance(3)
|
||||
|
||||
assert_state()
|
||||
}
|
||||
```
|
||||
|
||||
and in production builds, simply inject the real clock instead:
|
||||
```
|
||||
my_func(clockwork.NewRealClock())
|
||||
```
|
||||
|
||||
See [example_test.go](example_test.go) for a full example.
|
||||
|
||||
# Credits
|
||||
|
||||
clockwork is inspired by @wickman's [threaded fake clock](https://gist.github.com/wickman/3840816), and the [Golang playground](http://blog.golang.org/playground#Faking time)
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
package clockwork
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock provides an interface that packages can use instead of directly
|
||||
// using the time module, so that chronology-related behavior can be tested
|
||||
type Clock interface {
|
||||
After(d time.Duration) <-chan time.Time
|
||||
Sleep(d time.Duration)
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// FakeClock provides an interface for a clock which can be
|
||||
// manually advanced through time
|
||||
type FakeClock interface {
|
||||
Clock
|
||||
// Advance advances the FakeClock to a new point in time, ensuring any existing
|
||||
// sleepers are notified appropriately before returning
|
||||
Advance(d time.Duration)
|
||||
// BlockUntil will block until the FakeClock has the given number of
|
||||
// sleepers (callers of Sleep or After)
|
||||
BlockUntil(n int)
|
||||
}
|
||||
|
||||
// NewRealClock returns a Clock which simply delegates calls to the actual time
|
||||
// package; it should be used by packages in production.
|
||||
func NewRealClock() Clock {
|
||||
return &realClock{}
|
||||
}
|
||||
|
||||
// NewFakeClock returns a FakeClock implementation which can be
|
||||
// manually advanced through time for testing. The initial time of the
|
||||
// FakeClock will be an arbitrary non-zero time.
|
||||
func NewFakeClock() FakeClock {
|
||||
// use a fixture that does not fulfill Time.IsZero()
|
||||
return NewFakeClockAt(time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC))
|
||||
}
|
||||
|
||||
// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
|
||||
func NewFakeClockAt(t time.Time) FakeClock {
|
||||
return &fakeClock{
|
||||
time: t,
|
||||
}
|
||||
}
|
||||
|
||||
type realClock struct{}
|
||||
|
||||
func (rc *realClock) After(d time.Duration) <-chan time.Time {
|
||||
return time.After(d)
|
||||
}
|
||||
|
||||
func (rc *realClock) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
func (rc *realClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
type fakeClock struct {
|
||||
sleepers []*sleeper
|
||||
blockers []*blocker
|
||||
time time.Time
|
||||
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
// sleeper represents a caller of After or Sleep
|
||||
type sleeper struct {
|
||||
until time.Time
|
||||
done chan time.Time
|
||||
}
|
||||
|
||||
// blocker represents a caller of BlockUntil
|
||||
type blocker struct {
|
||||
count int
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
// After mimics time.After; it waits for the given duration to elapse on the
|
||||
// fakeClock, then sends the current time on the returned channel.
|
||||
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
now := fc.time
|
||||
done := make(chan time.Time, 1)
|
||||
if d.Nanoseconds() == 0 {
|
||||
// special case - trigger immediately
|
||||
done <- now
|
||||
} else {
|
||||
// otherwise, add to the set of sleepers
|
||||
s := &sleeper{
|
||||
until: now.Add(d),
|
||||
done: done,
|
||||
}
|
||||
fc.sleepers = append(fc.sleepers, s)
|
||||
// and notify any blockers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
}
|
||||
return done
|
||||
}
|
||||
|
||||
// notifyBlockers notifies all the blockers waiting until the
|
||||
// given number of sleepers are waiting on the fakeClock. It
|
||||
// returns an updated slice of blockers (i.e. those still waiting)
|
||||
func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) {
|
||||
for _, b := range blockers {
|
||||
if b.count == count {
|
||||
close(b.ch)
|
||||
} else {
|
||||
newBlockers = append(newBlockers, b)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sleep blocks until the given duration has passed on the fakeClock
|
||||
func (fc *fakeClock) Sleep(d time.Duration) {
|
||||
<-fc.After(d)
|
||||
}
|
||||
|
||||
// Time returns the current time of the fakeClock
|
||||
func (fc *fakeClock) Now() time.Time {
|
||||
fc.l.RLock()
|
||||
t := fc.time
|
||||
fc.l.RUnlock()
|
||||
return t
|
||||
}
|
||||
|
||||
// Advance advances fakeClock to a new point in time, ensuring channels from any
|
||||
// previous invocations of After are notified appropriately before returning
|
||||
func (fc *fakeClock) Advance(d time.Duration) {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
end := fc.time.Add(d)
|
||||
var newSleepers []*sleeper
|
||||
for _, s := range fc.sleepers {
|
||||
if end.Sub(s.until) >= 0 {
|
||||
s.done <- end
|
||||
} else {
|
||||
newSleepers = append(newSleepers, s)
|
||||
}
|
||||
}
|
||||
fc.sleepers = newSleepers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
fc.time = end
|
||||
}
|
||||
|
||||
// BlockUntil will block until the fakeClock has the given number of sleepers
|
||||
// (callers of Sleep or After)
|
||||
func (fc *fakeClock) BlockUntil(n int) {
|
||||
fc.l.Lock()
|
||||
// Fast path: current number of sleepers is what we're looking for
|
||||
if len(fc.sleepers) == n {
|
||||
fc.l.Unlock()
|
||||
return
|
||||
}
|
||||
// Otherwise, set up a new blocker
|
||||
b := &blocker{
|
||||
count: n,
|
||||
ch: make(chan struct{}),
|
||||
}
|
||||
fc.blockers = append(fc.blockers, b)
|
||||
fc.l.Unlock()
|
||||
<-b.ch
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2017 marvin + konsorten GmbH (open-source@konsorten.de)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Windows Terminal Sequences
|
||||
|
||||
This library allow for enabling Windows terminal color support for Go.
|
||||
|
||||
See [Console Virtual Terminal Sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) for details.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
sequences "github.com/konsorten/go-windows-terminal-sequences"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sequences.EnableVirtualTerminalProcessing(syscall.Stdout, true)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de).
|
||||
|
||||
We thank all the authors who provided code to this library:
|
||||
|
||||
* Felix Kollmann
|
||||
* Nicolas Perraut
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+1
@@ -0,0 +1 @@
|
||||
module github.com/konsorten/go-windows-terminal-sequences
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// +build windows
|
||||
|
||||
package sequences
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32Dll *syscall.LazyDLL = syscall.NewLazyDLL("Kernel32.dll")
|
||||
setConsoleMode *syscall.LazyProc = kernel32Dll.NewProc("SetConsoleMode")
|
||||
)
|
||||
|
||||
func EnableVirtualTerminalProcessing(stream syscall.Handle, enable bool) error {
|
||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
|
||||
|
||||
var mode uint32
|
||||
err := syscall.GetConsoleMode(syscall.Stdout, &mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enable {
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
} else {
|
||||
mode &^= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
}
|
||||
|
||||
ret, _, err := setConsoleMode.Call(uintptr(unsafe.Pointer(stream)), uintptr(mode))
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// +build linux darwin
|
||||
|
||||
package sequences
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func EnableVirtualTerminalProcessing(stream uintptr, enable bool) error {
|
||||
return fmt.Errorf("windows only package")
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.11.x"
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
## 1.1.2
|
||||
|
||||
* Fix error when decode hook decodes interface implementation into interface
|
||||
type. [GH-140]
|
||||
|
||||
## 1.1.1
|
||||
|
||||
* Fix panic that can happen in `decodePtr`
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133]
|
||||
* Support struct to struct decoding [GH-137]
|
||||
* If source map value is nil, then destination map value is nil (instead of empty)
|
||||
* If source slice value is nil, then destination slice value is nil (instead of empty)
|
||||
* If source pointer is nil, then destination pointer is set to nil (instead of
|
||||
allocated zero value of type)
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial tagged stable release.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Mitchell Hashimoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# mapstructure [](https://godoc.org/github.com/mitchellh/mapstructure)
|
||||
|
||||
mapstructure is a Go library for decoding generic map values to structures
|
||||
and vice versa, while providing helpful error handling.
|
||||
|
||||
This library is most useful when decoding values from some data stream (JSON,
|
||||
Gob, etc.) where you don't _quite_ know the structure of the underlying data
|
||||
until you read a part of it. You can therefore read a `map[string]interface{}`
|
||||
and use this library to decode it into the proper underlying native Go
|
||||
structure.
|
||||
|
||||
## Installation
|
||||
|
||||
Standard `go get`:
|
||||
|
||||
```
|
||||
$ go get github.com/mitchellh/mapstructure
|
||||
```
|
||||
|
||||
## Usage & Example
|
||||
|
||||
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure).
|
||||
|
||||
The `Decode` function has examples associated with it there.
|
||||
|
||||
## But Why?!
|
||||
|
||||
Go offers fantastic standard libraries for decoding formats such as JSON.
|
||||
The standard method is to have a struct pre-created, and populate that struct
|
||||
from the bytes of the encoded format. This is great, but the problem is if
|
||||
you have configuration or an encoding that changes slightly depending on
|
||||
specific fields. For example, consider this JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "person",
|
||||
"name": "Mitchell"
|
||||
}
|
||||
```
|
||||
|
||||
Perhaps we can't populate a specific structure without first reading
|
||||
the "type" field from the JSON. We could always do two passes over the
|
||||
decoding of the JSON (reading the "type" first, and the rest later).
|
||||
However, it is much simpler to just decode this into a `map[string]interface{}`
|
||||
structure, read the "type" key, then use something like this library
|
||||
to decode it into the proper structure.
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
|
||||
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
|
||||
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
|
||||
// Create variables here so we can reference them with the reflect pkg
|
||||
var f1 DecodeHookFuncType
|
||||
var f2 DecodeHookFuncKind
|
||||
|
||||
// Fill in the variables into this interface and the rest is done
|
||||
// automatically using the reflect package.
|
||||
potential := []interface{}{f1, f2}
|
||||
|
||||
v := reflect.ValueOf(h)
|
||||
vt := v.Type()
|
||||
for _, raw := range potential {
|
||||
pt := reflect.ValueOf(raw).Type()
|
||||
if vt.ConvertibleTo(pt) {
|
||||
return v.Convert(pt).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeHookExec executes the given decode hook. This should be used
|
||||
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc
|
||||
// that took reflect.Kind instead of reflect.Type.
|
||||
func DecodeHookExec(
|
||||
raw DecodeHookFunc,
|
||||
from reflect.Type, to reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
switch f := typedDecodeHook(raw).(type) {
|
||||
case DecodeHookFuncType:
|
||||
return f(from, to, data)
|
||||
case DecodeHookFuncKind:
|
||||
return f(from.Kind(), to.Kind(), data)
|
||||
default:
|
||||
return nil, errors.New("invalid decode hook signature")
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeDecodeHookFunc creates a single DecodeHookFunc that
|
||||
// automatically composes multiple DecodeHookFuncs.
|
||||
//
|
||||
// The composed funcs are called in order, with the result of the
|
||||
// previous transformation.
|
||||
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
var err error
|
||||
for _, f1 := range fs {
|
||||
data, err = DecodeHookExec(f1, f, t, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Modify the from kind to be correct with the new data
|
||||
f = nil
|
||||
if val := reflect.ValueOf(data); val.IsValid() {
|
||||
f = val.Type()
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToSliceHookFunc returns a DecodeHookFunc that converts
|
||||
// string to []string by splitting on the given sep.
|
||||
func StringToSliceHookFunc(sep string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f != reflect.String || t != reflect.Slice {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
raw := data.(string)
|
||||
if raw == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(raw, sep), nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeDurationHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Duration.
|
||||
func StringToTimeDurationHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Duration(5)) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.ParseDuration(data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IP
|
||||
func StringToIPHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IP{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
ip := net.ParseIP(data.(string))
|
||||
if ip == nil {
|
||||
return net.IP{}, fmt.Errorf("failed parsing ip %v", data)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StringToIPNetHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to net.IPNet
|
||||
func StringToIPNetHookFunc() DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(net.IPNet{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
_, net, err := net.ParseCIDR(data.(string))
|
||||
return net, err
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimeHookFunc returns a DecodeHookFunc that converts
|
||||
// strings to time.Time.
|
||||
func StringToTimeHookFunc(layout string) DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if f.Kind() != reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
if t != reflect.TypeOf(time.Time{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Convert it by parsing
|
||||
return time.Parse(layout, data.(string))
|
||||
}
|
||||
}
|
||||
|
||||
// WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to
|
||||
// the decoder.
|
||||
//
|
||||
// Note that this is significantly different from the WeaklyTypedInput option
|
||||
// of the DecoderConfig.
|
||||
func WeaklyTypedHook(
|
||||
f reflect.Kind,
|
||||
t reflect.Kind,
|
||||
data interface{}) (interface{}, error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
switch t {
|
||||
case reflect.String:
|
||||
switch f {
|
||||
case reflect.Bool:
|
||||
if dataVal.Bool() {
|
||||
return "1", nil
|
||||
}
|
||||
return "0", nil
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil
|
||||
case reflect.Int:
|
||||
return strconv.FormatInt(dataVal.Int(), 10), nil
|
||||
case reflect.Slice:
|
||||
dataType := dataVal.Type()
|
||||
elemKind := dataType.Elem().Kind()
|
||||
if elemKind == reflect.Uint8 {
|
||||
return string(dataVal.Interface().([]uint8)), nil
|
||||
}
|
||||
case reflect.Uint:
|
||||
return strconv.FormatUint(dataVal.Uint(), 10), nil
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package mapstructure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error implements the error interface and can represents multiple
|
||||
// errors that occur in the course of a single decode.
|
||||
type Error struct {
|
||||
Errors []string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
points := make([]string, len(e.Errors))
|
||||
for i, err := range e.Errors {
|
||||
points[i] = fmt.Sprintf("* %s", err)
|
||||
}
|
||||
|
||||
sort.Strings(points)
|
||||
return fmt.Sprintf(
|
||||
"%d error(s) decoding:\n\n%s",
|
||||
len(e.Errors), strings.Join(points, "\n"))
|
||||
}
|
||||
|
||||
// WrappedErrors implements the errwrap.Wrapper interface to make this
|
||||
// return value more useful with the errwrap and go-multierror libraries.
|
||||
func (e *Error) WrappedErrors() []error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]error, len(e.Errors))
|
||||
for i, e := range e.Errors {
|
||||
result[i] = errors.New(e)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func appendErrors(errors []string, err error) []string {
|
||||
switch e := err.(type) {
|
||||
case *Error:
|
||||
return append(errors, e.Errors...)
|
||||
default:
|
||||
return append(errors, e.Error())
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
module github.com/mitchellh/mapstructure
|
||||
+1149
File diff suppressed because it is too large
Load Diff
+1
@@ -1 +1,2 @@
|
||||
logrus
|
||||
vendor
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user