From ff67bd23f2bba47554c97dc452f376d5c34a2980 Mon Sep 17 00:00:00 2001 From: Chris Branch Date: Tue, 7 Nov 2017 15:17:19 +0000 Subject: [PATCH] Release 2017.11.1 --- cmd/cloudflare-warp/cloudflareca.go | 58 +++++++- cmd/cloudflare-warp/hello.go | 60 ++++---- cmd/cloudflare-warp/linux_service.go | 13 +- cmd/cloudflare-warp/login.go | 172 ++++++++++++++++++++- cmd/cloudflare-warp/main.go | 115 ++++++++------ cmd/cloudflare-warp/service_template.go | 102 ++++++++++++- origin/tunnel.go | 19 +-- tunnelrpc/pogs/tunnelrpc.go | 21 +-- tunnelrpc/tunnelrpc.capnp | 10 +- tunnelrpc/tunnelrpc.capnp.go | 190 ++++++++++-------------- 10 files changed, 537 insertions(+), 223 deletions(-) diff --git a/cmd/cloudflare-warp/cloudflareca.go b/cmd/cloudflare-warp/cloudflareca.go index ee88666a..458a2212 100644 --- a/cmd/cloudflare-warp/cloudflareca.go +++ b/cmd/cloudflare-warp/cloudflareca.go @@ -4,7 +4,27 @@ import ( "crypto/x509" ) -const cloudflareRootCA = `-----BEGIN CERTIFICATE----- +// TODO: remove the Origin CA root certs when migrated to Authenticated Origin Pull certs +const cloudflareRootCA = ` +Issuer: C=US, ST=California, L=San Francisco, O=CloudFlare, Inc., OU=CloudFlare Origin SSL ECC Certificate Authority +-----BEGIN CERTIFICATE----- +MIICiDCCAi6gAwIBAgIUXZP3MWb8MKwBE1Qbawsp1sfA/Y4wCgYIKoZIzj0EAwIw +gY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T +YW4gRnJhbmNpc2NvMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYDVQQL +Ey9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0 +eTAeFw0xNjAyMjIxODI0MDBaFw0yMTAyMjIwMDI0MDBaMIGPMQswCQYDVQQGEwJV +UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEZ +MBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE4MDYGA1UECxMvQ2xvdWRGbGFyZSBP +cmlnaW4gU1NMIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAASR+sGALuaGshnUbcxKry+0LEXZ4NY6JUAtSeA6g87K3jaA +xpIg9G50PokpfWkhbarLfpcZu0UAoYy2su0EhN7wo2YwZDAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUhTBdOypw1O3VkmcH/es5 +tBoOOKcwHwYDVR0jBBgwFoAUhTBdOypw1O3VkmcH/es5tBoOOKcwCgYIKoZIzj0E +AwIDSAAwRQIgEiIEHQr5UKma50D1WRMJBUSgjg24U8n8E2mfw/8UPz0CIQCr5V/e +mcifak4CQsr+DH4pn5SJD7JxtCG3YGswW8QZsw== +-----END CERTIFICATE----- +Issuer: C=US, O=CloudFlare, Inc., OU=CloudFlare Origin SSL Certificate Authority, L=San Francisco, ST=California +-----BEGIN CERTIFICATE----- MIID/DCCAuagAwIBAgIID+rOSdTGfGcwCwYJKoZIhvcNAQELMIGLMQswCQYDVQQG EwJVUzEZMBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE0MDIGA1UECxMrQ2xvdWRG bGFyZSBPcmlnaW4gU1NMIENlcnRpZmljYXRlIEF1dGhvcml0eTEWMBQGA1UEBxMN @@ -27,6 +47,42 @@ QGgDl6gRmb8aDwk7Q92BPvek5nMzaWlP82ixavvYI+okoSY8pwdcVKobx6rWzMWz ZEC9M6H3F0dDYE23XcCFIdgNSAmmGyXPBstOe0aAJXwJTxOEPn36VWr0PKIQJy5Y 4o1wpMpqCOIwWc8J9REV/REzN6Z1LXImdUgXIXOwrz56gKUJzPejtBQyIGj0mveX Fu6q54beR89jDc+oABmOgg== +-----END CERTIFICATE----- +Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net +-----BEGIN CERTIFICATE----- +MIIGBjCCA/CgAwIBAgIIV5G6lVbCLmEwCwYJKoZIhvcNAQENMIGQMQswCQYDVQQG +EwJVUzEZMBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjEUMBIGA1UECxMLT3JpZ2lu +IFB1bGwxFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzARBgNVBAgTCkNhbGlmb3Ju +aWExIzAhBgNVBAMTGm9yaWdpbi1wdWxsLmNsb3VkZmxhcmUubmV0MB4XDTE1MDEx +MzAyNDc1M1oXDTIwMDExMjAyNTI1M1owgZAxCzAJBgNVBAYTAlVTMRkwFwYDVQQK +ExBDbG91ZEZsYXJlLCBJbmMuMRQwEgYDVQQLEwtPcmlnaW4gUHVsbDEWMBQGA1UE +BxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5pYTEjMCEGA1UEAxMa +b3JpZ2luLXB1bGwuY2xvdWRmbGFyZS5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDdsts6I2H5dGyn4adACQRXlfo0KmwsN7B5rxD8C5qgy6spyONr +WV0ecvdeGQfWa8Gy/yuTuOnsXfy7oyZ1dm93c3Mea7YkM7KNMc5Y6m520E9tHooc +f1qxeDpGSsnWc7HWibFgD7qZQx+T+yfNqt63vPI0HYBOYao6hWd3JQhu5caAcIS2 +ms5tzSSZVH83ZPe6Lkb5xRgLl3eXEFcfI2DjnlOtLFqpjHuEB3Tr6agfdWyaGEEi +lRY1IB3k6TfLTaSiX2/SyJ96bp92wvTSjR7USjDV9ypf7AD6u6vwJZ3bwNisNw5L +ptph0FBnc1R6nDoHmvQRoyytoe0rl/d801i9Nru/fXa+l5K2nf1koR3IX440Z2i9 ++Z4iVA69NmCbT4MVjm7K3zlOtwfI7i1KYVv+ATo4ycgBuZfY9f/2lBhIv7BHuZal +b9D+/EK8aMUfjDF4icEGm+RQfExv2nOpkR4BfQppF/dLmkYfjgtO1403X0ihkT6T +PYQdmYS6Jf53/KpqC3aA+R7zg2birtvprinlR14MNvwOsDOzsK4p8WYsgZOR4Qr2 +gAx+z2aVOs/87+TVOR0r14irQsxbg7uP2X4t+EXx13glHxwG+CnzUVycDLMVGvuG +aUgF9hukZxlOZnrl6VOf1fg0Caf3uvV8smOkVw6DMsGhBZSJVwao0UQNqQIDAQAB +o2YwZDAOBgNVHQ8BAf8EBAMCAAYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4E +FgQUQ1lLK2mLgOERM2pXzVc42p59xeswHwYDVR0jBBgwFoAUQ1lLK2mLgOERM2pX +zVc42p59xeswCwYJKoZIhvcNAQENA4ICAQDKDQM1qPRVP/4Gltz0D6OU6xezFBKr +LWtDoA1qW2F7pkiYawCP9MrDPDJsHy7dx+xw3bBZxOsK5PA/T7p1dqpEl6i8F692 +g//EuYOifLYw3ySPe3LRNhvPl/1f6Sn862VhPvLa8aQAAwR9e/CZvlY3fj+6G5ik +3it7fikmKUsVnugNOkjmwI3hZqXfJNc7AtHDFw0mEOV0dSeAPTo95N9cxBbm9PKv +qAEmTEXp2trQ/RjJ/AomJyfA1BQjsD0j++DI3a9/BbDwWmr1lJciKxiNKaa0BRLB +dKMrYQD+PkPNCgEuojT+paLKRrMyFUzHSG1doYm46NE9/WARTh3sFUp1B7HZSBqA +kHleoB/vQ/mDuW9C3/8Jk2uRUdZxR+LoNZItuOjU8oTy6zpN1+GgSj7bHjiy9rfA +F+ehdrz+IOh80WIiqs763PGoaYUyzxLvVowLWNoxVVoc9G+PqFKqD988XlipHVB6 +Bz+1CD4D/bWrs3cC9+kk/jFmrrAymZlkFX8tDb5aXASSLJjUjcptci9SKqtI2h0J +wUGkD7+bQAr+7vr8/R+CBmNMe7csE8NeEX6lVMF7Dh0a1YKQa6hUN18bBuYgTMuT +QzMmZpRpIBB321ZBlcnlxiTJvWxvbCPHKHj20VwwAz7LONF59s84ZsOqfoBv8gKM +s0s5dsq5zpLeaw== -----END CERTIFICATE-----` func GetCloudflareRootCA() *x509.CertPool { diff --git a/cmd/cloudflare-warp/hello.go b/cmd/cloudflare-warp/hello.go index 87d10868..8bb823db 100644 --- a/cmd/cloudflare-warp/hello.go +++ b/cmd/cloudflare-warp/hello.go @@ -4,23 +4,21 @@ import ( "bytes" "fmt" "html/template" + "io/ioutil" "net" "net/http" "os" - "strings" "github.com/pkg/errors" log "github.com/Sirupsen/logrus" - "github.com/cloudflare/cloudflare-warp/origin" - tunnelpogs "github.com/cloudflare/cloudflare-warp/tunnelrpc/pogs" cli "gopkg.in/urfave/cli.v2" ) type templateData struct { ServerName string Request *http.Request - Tags []tunnelpogs.Tag + Body string } const defaultServerName = "the Cloudflare Warp test server" @@ -42,7 +40,7 @@ const indexTemplate = ` -
+
@@ -56,21 +54,30 @@ const indexTemplate = ` running an encrypted, virtual tunnel from your laptop or server to Cloudflare's edge network.

-

Ready for the next step?

- Ready for the next step?

+
Get started here -{{if .Tags}}
-

Connection

+
+

Request

-{{range .Tags}}
{{.Name}}
-
{{.Value}}
-{{end}}
+
Method: {{.Request.Method}}
+
Protocol: {{.Request.Proto}}
+
Request URL: {{.Request.URL}}
+
Transfer encoding: {{.Request.TransferEncoding}}
+
Host: {{.Request.Host}}
+
Remote address: {{.Request.RemoteAddr}}
+
Request URI: {{.Request.RequestURI}}
+{{range $key, $value := .Request.Header}} +
Header: {{$key}}, Value: {{$value}}
+{{end}} +
Body: {{.Body}}
+
-{{end}}
+
@@ -127,10 +134,17 @@ func (s *HelloWorldServer) ListenAndServe(address string) error { func (s *HelloWorldServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.WithField("client", r.RemoteAddr).Infof("%s %s %s", r.Method, r.URL, r.Proto) var buffer bytes.Buffer - err := s.responseTemplate.Execute(&buffer, &templateData{ + var body string + rawBody, err := ioutil.ReadAll(r.Body) + if err == nil { + body = string(rawBody) + } else { + body = "" + } + err = s.responseTemplate.Execute(&buffer, &templateData{ ServerName: s.serverName, Request: r, - Tags: tagsFromHeaders(r.Header), + Body: body, }) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -139,17 +153,3 @@ func (s *HelloWorldServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { buffer.WriteTo(w) } } - -func tagsFromHeaders(header http.Header) []tunnelpogs.Tag { - var tags []tunnelpogs.Tag - for headerName, headerValues := range header { - trimmed := strings.TrimPrefix(headerName, origin.TagHeaderNamePrefix) - if trimmed == headerName { - continue - } - for _, value := range headerValues { - tags = append(tags, tunnelpogs.Tag{Name: trimmed, Value: value}) - } - } - return tags -} diff --git a/cmd/cloudflare-warp/linux_service.go b/cmd/cloudflare-warp/linux_service.go index 99a1686c..35c7347e 100644 --- a/cmd/cloudflare-warp/linux_service.go +++ b/cmd/cloudflare-warp/linux_service.go @@ -29,6 +29,8 @@ func runApp(app *cli.App) { app.Run(os.Args) } +const serviceConfigDir = "/etc/cloudflare-warp" + var systemdTemplates = []ServiceTemplate{ { Path: "/etc/systemd/system/cloudflare-warp.service", @@ -39,8 +41,7 @@ After=network.target [Service] TimeoutStartSec=0 Type=notify -ExecStart={{ .Path }} --config /etc/cloudflare-warp.yml --autoupdate 0s -User=nobody +ExecStart={{ .Path }} --config /etc/cloudflare-warp/config.yml --origincert /etc/cloudflare-warp/cert.pem --autoupdate 0s [Install] WantedBy=multi-user.target @@ -86,7 +87,7 @@ var sysvTemplate = ServiceTemplate{ # Short-Description: Cloudflare Warp # Description: Cloudflare Warp agent ### END INIT INFO -cmd="{{.Path}} --config /etc/cloudflare-warp.yml --pidfile /var/run/$name.pid" +cmd="{{.Path}} --config /etc/cloudflare-warp/config.yml --origincert /etc/cloudflare-warp/cert.pem --pidfile /var/run/$name.pid" name=$(basename $(readlink -f $0)) pid_file="/var/run/$name.pid" stdout_log="/var/log/$name.log" @@ -177,6 +178,12 @@ func installLinuxService(c *cli.Context) error { } templateArgs := ServiceTemplateArgs{Path: etPath} + if err = copyCredentials(serviceConfigDir); err != nil { + fmt.Fprintf(os.Stderr, "Failed to copy user configuration: %v\n", err) + fmt.Fprintf(os.Stderr, "Before running the service, ensure that %s contains two files, %s and %s", + serviceConfigDir, credentialFile, configFile) + } + switch { case isSystemd(): return installSystemd(&templateArgs) diff --git a/cmd/cloudflare-warp/login.go b/cmd/cloudflare-warp/login.go index dbb452f4..32537275 100644 --- a/cmd/cloudflare-warp/login.go +++ b/cmd/cloudflare-warp/login.go @@ -1,31 +1,195 @@ package main import ( + "crypto/rand" + "encoding/base32" "fmt" + "io" + "net/http" + "net/url" "os" + "os/exec" + "path/filepath" + "runtime" "syscall" + "time" + log "github.com/Sirupsen/logrus" homedir "github.com/mitchellh/go-homedir" cli "gopkg.in/urfave/cli.v2" ) +const baseLoginURL = "https://www.cloudflare.com/a/warp" +const baseCertStoreURL = "https://login.cloudflarewarp.com" +const clientTimeout = time.Minute * 20 + func login(c *cli.Context) error { - path, err := homedir.Expand(defaultConfigPath) + configPath, err := homedir.Expand(defaultConfigDir) if err != nil { return err } + ok, err := fileExists(configPath) + if !ok && err == nil { + // create config directory if doesn't already exist + err = os.Mkdir(configPath, 0700) + } + if err != nil { + return err + } + path := filepath.Join(configPath, credentialFile) fileInfo, err := os.Stat(path) if err == nil && fileInfo.Size() > 0 { - fmt.Fprintf(os.Stderr, `You have an existing config file at %s which login would overwrite. + fmt.Fprintf(os.Stderr, `You have an existing certificate at %s which login would overwrite. If this is intentional, please move or delete that file then run this command again. -`, defaultConfigPath) +`, path) return nil } if err != nil && err.(*os.PathError).Err != syscall.ENOENT { return err } - fmt.Fprintln(os.Stderr, "Please visit https://www.cloudflare.com/a/warp to obtain a certificate.") + // for local debugging + baseURL := baseCertStoreURL + if c.IsSet("url") { + baseURL = c.String("url") + } + // Generate a random post URL + certURL := baseURL + generateRandomPath() + loginURL, err := url.Parse(baseLoginURL) + if err != nil { + // shouldn't happen, URL is hardcoded + return err + } + loginURL.RawQuery = "callback=" + url.QueryEscape(certURL) + err = open(loginURL.String()) + if err != nil { + fmt.Fprintf(os.Stderr, `Please open the following URL and log in with your Cloudflare account: + +%s + +Leave cloudflare-warp running to install the certificate automatically. +`, loginURL.String()) + } else { + fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL: + +%s + +If the browser failed to open, open it yourself and visit the URL above. + +`, loginURL.String()) + } + + if download(certURL, path) { + fmt.Fprintf(os.Stderr, `You have successfully logged in. +If you wish to copy your credentials to a server, they have been saved to: +%s +`, path) + } else { + fmt.Fprintf(os.Stderr, `Failed to write the certificate due to the following error: +%v + +Your browser will download the certificate instead. You will have to manually +copy it to the following path: + +%s + +`, err, path) + } return nil } + +// generateRandomPath generates a random URL to associate with the certificate. +func generateRandomPath() string { + randomBytes := make([]byte, 40) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + return "/" + base32.StdEncoding.EncodeToString(randomBytes) +} + +// open opens the specified URL in the default browser of the user. +func open(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} + +func download(certURL, filePath string) bool { + client := &http.Client{Timeout: clientTimeout} + // attempt a (long-running) certificate get + for i := 0; i < 20; i++ { + ok, err := tryDownload(client, certURL, filePath) + if ok { + putSuccess(client, certURL) + return true + } + if err != nil { + log.WithError(err).Error("Error fetching certificate") + return false + } + } + return false +} + +func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) { + resp, err := client.Get(certURL) + if err != nil { + return false, err + } + defer resp.Body.Close() + if resp.StatusCode == 404 { + return false, nil + } + if resp.StatusCode != 200 { + return false, fmt.Errorf("Unexpected HTTP error code %d", resp.StatusCode) + } + if resp.Header.Get("Content-Type") != "application/x-pem-file" { + return false, fmt.Errorf("Unexpected content type %s", resp.Header.Get("Content-Type")) + } + // write response + file, err := os.Create(filePath) + if err != nil { + return false, err + } + defer file.Close() + written, err := io.Copy(file, resp.Body) + switch { + case err != nil: + return false, err + case resp.ContentLength != written && resp.ContentLength != -1: + return false, fmt.Errorf("Short read (%d bytes) from server while writing certificate", written) + default: + return true, nil + } +} + +func putSuccess(client *http.Client, certURL string) { + // indicate success to the relay server + req, err := http.NewRequest("PUT", certURL+"/ok", nil) + if err != nil { + log.WithError(err).Error("HTTP request error") + return + } + resp, err := client.Do(req) + if err != nil { + log.WithError(err).Error("HTTP error") + return + } + resp.Body.Close() + if resp.StatusCode != 200 { + log.Errorf("Unexpected HTTP error code %d", resp.StatusCode) + } +} diff --git a/cmd/cloudflare-warp/main.go b/cmd/cloudflare-warp/main.go index 8b7fda3c..3ab553c0 100644 --- a/cmd/cloudflare-warp/main.go +++ b/cmd/cloudflare-warp/main.go @@ -4,10 +4,12 @@ import ( "crypto/tls" "encoding/hex" "fmt" + "io/ioutil" "math/rand" "net" "os" "os/signal" + "path/filepath" "sync" "syscall" "time" @@ -31,7 +33,9 @@ import ( ) const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878" -const defaultConfigPath = "~/.cloudflare-warp.yml" +const defaultConfigDir = "~/.cloudflare-warp" +const credentialFile = "cert.pem" +const configFile = "config.yml" var listeners = gracenet.Net{} var Version = "DEV" @@ -71,10 +75,15 @@ WARNING: Usage: "Specifies a config file in YAML format.", }, altsrc.NewDurationFlag(&cli.DurationFlag{ - Name: "autoupdate", - Usage: "Periodically check for updates, restarting the server with the new version.", + Name: "autoupdate-freq", + Usage: "Autoupdate frequency. Default is 24h.", Value: time.Hour * 24, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "no-autoupdate", + Usage: "Disable periodic check for updates, restarting the server with the new version.", + Value: false, + }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "edge", Value: "cftunnel.com:7844", @@ -88,6 +97,12 @@ WARNING: EnvVars: []string{"TUNNEL_CACERT"}, Hidden: true, }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "origincert", + Usage: "Path to the certificate generated for your origin when you run cloudflare-warp login.", + EnvVars: []string{"ORIGIN_CERT"}, + Value: filepath.Join(defaultConfigDir, credentialFile), + }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "url", Value: "http://localhost:8080", @@ -112,18 +127,21 @@ WARNING: }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-key", - Usage: "A Cloudflare API key. Required(can be in the config file) unless you are only running the hello command or login command.", + Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_KEY"}, + Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-email", - Usage: "The Cloudflare user's email address associated with the API key. Required(can be in the config file) unless you are only running the hello command or login command.", + Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_EMAIL"}, + Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "api-ca-key", - Usage: "The Origin CA service key associated with the user. Required(can be in the config file) unless you are only running the hello command or login command.", + Usage: "This parameter has been deprecated since version 2017.10.1.", EnvVars: []string{"TUNNEL_API_CA_KEY"}, + Hidden: true, }), altsrc.NewStringFlag(&cli.StringFlag{ Name: "metrics", @@ -160,12 +178,6 @@ WARNING: Usage: "Maximum number of retries for connection/protocol errors.", EnvVars: []string{"TUNNEL_RETRIES"}, }), - altsrc.NewBoolFlag(&cli.BoolFlag{ - Name: "debug", - Value: false, - Usage: "Enable HTTP requests to the autogenerated cftunnel.com domain.", - EnvVars: []string{"TUNNEL_DEBUG"}, - }), altsrc.NewBoolFlag(&cli.BoolFlag{ Name: "hello-world", Usage: "Run Hello World Server", @@ -207,6 +219,12 @@ WARNING: Action: login, Usage: "Generate a configuration file with your login details", ArgsUsage: " ", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Hidden: true, + }, + }, }, &cli.Command{ Name: "hello", @@ -239,6 +257,7 @@ func startServer(c *cli.Context) { if err != nil { log.WithError(err).Fatal("Unknown logging level specified") } + log.SetLevel(logLevel) hostname, err := validation.ValidateHostname(c.String("hostname")) if err != nil { @@ -260,7 +279,6 @@ func startServer(c *cli.Context) { if c.IsSet("hello-world") { wg.Add(1) listener, err := findAvailablePort() - if err != nil { listener.Close() log.WithError(err).Fatal("Cannot start Hello World Server") @@ -268,32 +286,50 @@ func startServer(c *cli.Context) { go func() { startHelloWorldServer(listener, shutdownC) wg.Done() + listener.Close() }() c.Set("url", "http://"+listener.Addr().String()) log.Infof("Starting Hello World Server at %s", c.String("url")) } + url, err := validateUrl(c) if err != nil { log.WithError(err).Fatal("Error validating url") } - // User must have api-key, api-email and api-ca-key - if !c.IsSet("api-key") { - log.Fatal("You need to give us your api-key either via the --api-key option or put it in the configuration file. You will also need to give us your api-email and api-ca-key.") - } - if !c.IsSet("api-email") { - log.Fatal("You need to give us your api-email either via the --api-email option or put it in the configuration file. You will also need to give us your api-ca-key.") - } - if !c.IsSet("api-ca-key") { - log.Fatal("You need to give us your api-ca-key either via the --api-ca-key option or put it in the configuration file.") - } log.Infof("Proxying tunnel requests to %s", url) + + // Fail if the user provided an old authentication method + if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") { + log.Fatal("You don't need to give us your api-key anymore. Please use the new log in method. Just run cloudflare-warp login") + } + + // Check that the user has acquired a certificate using the log in command + originCertPath, err := homedir.Expand(c.String("origincert")) + if err != nil { + log.WithError(err).Fatalf("Cannot resolve path %s", c.String("origincert")) + } + ok, err := fileExists(originCertPath) + if !ok { + log.Fatalf(`Cannot find a valid certificate for your origin at the path: + + %s + +If the path above is wrong, specify the path with the -origincert option. +If you don't have a certificate signed by Cloudflare, run the command: + + %s login +`, originCertPath, os.Args[0]) + } + // Easier to send the certificate as []byte via RPC than decoding it at this point + originCert, err := ioutil.ReadFile(originCertPath) + if err != nil { + log.WithError(err).Fatalf("Cannot read %s to load origin certificate", originCertPath) + } tunnelConfig := &origin.TunnelConfig{ EdgeAddr: c.String("edge"), OriginUrl: url, Hostname: hostname, - APIKey: c.String("api-key"), - APIEmail: c.String("api-email"), - APICAKey: c.String("api-ca-key"), + OriginCert: originCert, TlsConfig: &tls.Config{}, Retries: c.Uint("retries"), HeartbeatInterval: c.Duration("heartbeat-interval"), @@ -302,7 +338,6 @@ func startServer(c *cli.Context) { ReportedVersion: Version, LBPool: c.String("lb-pool"), Tags: tags, - AccessInternalIP: c.Bool("debug"), ConnectedSignal: h2mux.NewSignal(), } @@ -329,7 +364,10 @@ func startServer(c *cli.Context) { wg.Done() }() - go autoupdate(c.Duration("autoupdate"), shutdownC) + if !c.Bool("no-autoupdate") { + log.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq")) + go autoupdate(c.Duration("autoupdate-period"), shutdownC) + } err = WaitForSignal(errC, shutdownC) if err != nil { @@ -413,21 +451,14 @@ func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, er if context.IsSet("config") { return altsrc.NewYamlSourceFromFile(context.String("config")) } - for _, tryPath := range []string{ - defaultConfigPath, - "~/.cloudflare-warp.yaml", - "~/cloudflare-warp.yaml", - "~/cloudflare-warp.yml", - "~/.et.yaml", - "~/et.yml", - "~/et.yaml", - "~/.cftunnel.yaml", // for existing users - "~/cftunnel.yaml", + dirPath, err := homedir.Expand(defaultConfigDir) + if err != nil { + return nil, nil + } + for _, path := range []string{ + filepath.Join(dirPath, "/config.yml"), + filepath.Join(dirPath, "/config.yaml"), } { - path, err := homedir.Expand(tryPath) - if err != nil { - continue - } ok, err := fileExists(path) if ok { return altsrc.NewYamlSourceFromFile(path) diff --git a/cmd/cloudflare-warp/service_template.go b/cmd/cloudflare-warp/service_template.go index 28b5df30..871b01ff 100644 --- a/cmd/cloudflare-warp/service_template.go +++ b/cmd/cloudflare-warp/service_template.go @@ -1,11 +1,14 @@ package main import ( + "bufio" "bytes" "fmt" + "io" "io/ioutil" "os" "os/exec" + "path/filepath" "text/template" homedir "github.com/mitchellh/go-homedir" @@ -78,7 +81,7 @@ func runCommand(command string, args ...string) error { } commandErr, _ := ioutil.ReadAll(stderr) if len(commandErr) > 0 { - return fmt.Errorf("%s error: %s", command, commandErr) + fmt.Fprintf(os.Stderr, "%s: %s", command, commandErr) } err = cmd.Wait() if err != nil { @@ -86,3 +89,100 @@ func runCommand(command string, args ...string) error { } return nil } + +func ensureConfigDirExists(configDir string) error { + ok, err := fileExists(configDir) + if !ok && err == nil { + err = os.Mkdir(configDir, 0700) + } + return err +} + +// openFile opens the file at path. If create is set and the file exists, returns nil, true, nil +func openFile(path string, create bool) (file *os.File, exists bool, err error) { + expandedPath, err := homedir.Expand(path) + if err != nil { + return nil, false, err + } + if create { + fileInfo, err := os.Stat(expandedPath) + if err == nil && fileInfo.Size() > 0 { + return nil, true, nil + } + file, err = os.OpenFile(expandedPath, os.O_RDWR|os.O_CREATE, 0600) + } else { + file, err = os.Open(expandedPath) + } + return file, false, err +} + +func copyCertificate(configDir string) error { + // Copy certificate + destCredentialPath := filepath.Join(configDir, credentialFile) + destFile, exists, err := openFile(destCredentialPath, true) + if err != nil { + return err + } else if exists { + // credentials already exist, do nothing + return nil + } + defer destFile.Close() + + srcCredentialPath := filepath.Join(defaultConfigDir, credentialFile) + srcFile, _, err := openFile(srcCredentialPath, false) + if err != nil { + return err + } + defer srcFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return fmt.Errorf("unable to copy %s to %s: %v", srcCredentialPath, destCredentialPath, err) + } + + return nil +} + +func copyCredentials(configDir string) error { + if err := ensureConfigDirExists(configDir); err != nil { + return err + } + + if err := copyCertificate(configDir); err != nil { + return err + } + + // Copy or create config + destConfigPath := filepath.Join(configDir, configFile) + destFile, exists, err := openFile(destConfigPath, true) + if err != nil { + return err + } else if exists { + // config already exists, do nothing + return nil + } + defer destFile.Close() + + srcConfigPath := filepath.Join(defaultConfigDir, configFile) + srcFile, _, err := openFile(srcConfigPath, false) + if err != nil { + fmt.Println("Your service needs a config file that at least specifies the hostname option.") + fmt.Println("Type in a hostname now, or leave it blank and create the config file later.") + fmt.Print("Hostname: ") + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + if input == "" { + return err + } + fmt.Fprintf(destFile, "hostname: %s\n", input) + } else { + defer srcFile.Close() + _, err = io.Copy(destFile, srcFile) + if err != nil { + return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err) + } + fmt.Printf("Copied %s to %s", srcConfigPath, destConfigPath) + } + + return nil +} diff --git a/origin/tunnel.go b/origin/tunnel.go index 975cd5fc..358019ca 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -34,9 +34,7 @@ type TunnelConfig struct { EdgeAddr string OriginUrl string Hostname string - APIKey string - APIEmail string - APICAKey string + OriginCert []byte TlsConfig *tls.Config Retries uint HeartbeatInterval time.Duration @@ -45,7 +43,6 @@ type TunnelConfig struct { ReportedVersion string LBPool string Tags []tunnelpogs.Tag - AccessInternalIP bool ConnectedSignal h2mux.Signal } @@ -78,7 +75,6 @@ func (c *TunnelConfig) RegistrationOptions() *tunnelpogs.RegistrationOptions { ExistingTunnelPolicy: policy, PoolID: c.LBPool, Tags: c.Tags, - ExposeInternalHostname: c.AccessInternalIP, } } @@ -146,15 +142,16 @@ func ServeTunnel( return err, true } if registerErr != nil { - raven.CaptureError(registerErr, nil) // Don't retry on errors like entitlement failure or version too old if e, ok := registerErr.(printableRegisterTunnelError); ok { - log.WithError(e).Error("Cannot register") + log.Error(e) if e.permanent { return nil, false } return e.cause, true } + // Only log errors to Sentry that may have been caused by the client side, to reduce dupes + raven.CaptureError(registerErr, nil) log.Error("Cannot register") return err, true } @@ -202,7 +199,7 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi }) registration, err := ts.RegisterTunnel( ctx, - &tunnelpogs.Authentication{Key: config.APIKey, Email: config.APIEmail, OriginCAKey: config.APICAKey}, + config.OriginCert, config.Hostname, config.RegistrationOptions(), ) @@ -220,9 +217,9 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi permanent: registration.PermanentFailure, } } - for _, url := range registration.Urls { - log.Infof("Registered at %s", url) - } + + log.Infof("Registered at %s", registration.Url) + for _, logLine := range registration.LogLines { log.Infof(logLine) } diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index da3d7836..df359685 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -27,7 +27,7 @@ func UnmarshalAuthentication(s tunnelrpc.Authentication) (*Authentication, error type TunnelRegistration struct { Err string - Urls []string + Url string LogLines []string PermanentFailure bool } @@ -48,7 +48,6 @@ type RegistrationOptions struct { OS string `capnp:"os"` ExistingTunnelPolicy tunnelrpc.ExistingTunnelPolicy PoolID string `capnp:"poolId"` - ExposeInternalHostname bool Tags []Tag } @@ -82,7 +81,7 @@ func UnmarshalServerInfo(s tunnelrpc.ServerInfo) (*ServerInfo, error) { } type TunnelServer interface { - RegisterTunnel(ctx context.Context, auth *Authentication, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) + RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) GetServerInfo(ctx context.Context) (*ServerInfo, error) } @@ -95,11 +94,7 @@ type TunnelServer_PogsImpl struct { } func (i TunnelServer_PogsImpl) RegisterTunnel(p tunnelrpc.TunnelServer_registerTunnel) error { - authentication, err := p.Params.Auth() - if err != nil { - return err - } - pogsAuthentication, err := UnmarshalAuthentication(authentication) + originCert, err := p.Params.OriginCert() if err != nil { return err } @@ -116,7 +111,7 @@ func (i TunnelServer_PogsImpl) RegisterTunnel(p tunnelrpc.TunnelServer_registerT return err } server.Ack(p.Options) - registration, err := i.impl.RegisterTunnel(p.Ctx, pogsAuthentication, hostname, pogsOptions) + registration, err := i.impl.RegisterTunnel(p.Ctx, originCert, hostname, pogsOptions) if err != nil { return err } @@ -149,14 +144,10 @@ func (c TunnelServer_PogsClient) Close() error { return c.Conn.Close() } -func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, auth *Authentication, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) { +func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) { client := tunnelrpc.TunnelServer{Client: c.Client} promise := client.RegisterTunnel(ctx, func(p tunnelrpc.TunnelServer_registerTunnel_Params) error { - authentication, err := p.NewAuth() - if err != nil { - return err - } - err = MarshalAuthentication(authentication, auth) + err := p.SetOriginCert(originCert) if err != nil { return err } diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 8c98c188..ef89a837 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -11,8 +11,8 @@ struct Authentication { struct TunnelRegistration { err @0 :Text; - # A list of URLs that the tunnel is accessible from. - urls @1 :List(Text); + # the url to access the tunnel + url @1 :Text; # Used to inform the client of actions taken. logLines @2 :List(Text); # In case of error, whether the client should attempt to reconnect. @@ -29,10 +29,8 @@ struct RegistrationOptions { existingTunnelPolicy @3 :ExistingTunnelPolicy; # If using the balancing policy, identifies the LB pool to use. poolId @4 :Text; - # Prevents the tunnel from being accessed at .cftunnel.com - exposeInternalHostname @5 :Bool; # Client-defined tags to associate with the tunnel - tags @6 :List(Tag); + tags @5 :List(Tag); } struct Tag { @@ -51,6 +49,6 @@ struct ServerInfo { } interface TunnelServer { - registerTunnel @0 (auth :Authentication, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); + registerTunnel @0 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); getServerInfo @1 () -> (result :ServerInfo); } diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index 1352ece1..00bc7212 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -157,29 +157,23 @@ func (s TunnelRegistration) SetErr(v string) error { return s.Struct.SetText(0, v) } -func (s TunnelRegistration) Urls() (capnp.TextList, error) { +func (s TunnelRegistration) Url() (string, error) { p, err := s.Struct.Ptr(1) - return capnp.TextList{List: p.List()}, err + return p.Text(), err } -func (s TunnelRegistration) HasUrls() bool { +func (s TunnelRegistration) HasUrl() bool { p, err := s.Struct.Ptr(1) return p.IsValid() || err != nil } -func (s TunnelRegistration) SetUrls(v capnp.TextList) error { - return s.Struct.SetPtr(1, v.List.ToPtr()) +func (s TunnelRegistration) UrlBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err } -// NewUrls sets the urls field to a newly -// allocated capnp.TextList, preferring placement in s's segment. -func (s TunnelRegistration) NewUrls(n int32) (capnp.TextList, error) { - l, err := capnp.NewTextList(s.Struct.Segment(), n) - if err != nil { - return capnp.TextList{}, err - } - err = s.Struct.SetPtr(1, l.List.ToPtr()) - return l, err +func (s TunnelRegistration) SetUrl(v string) error { + return s.Struct.SetText(1, v) } func (s TunnelRegistration) LogLines() (capnp.TextList, error) { @@ -349,14 +343,6 @@ func (s RegistrationOptions) SetPoolId(v string) error { return s.Struct.SetText(3, v) } -func (s RegistrationOptions) ExposeInternalHostname() bool { - return s.Struct.Bit(16) -} - -func (s RegistrationOptions) SetExposeInternalHostname(v bool) { - s.Struct.SetBit(16, v) -} - func (s RegistrationOptions) Tags() (Tag_List, error) { p, err := s.Struct.Ptr(4) return Tag_List{List: p.List()}, err @@ -750,29 +736,18 @@ func (s TunnelServer_registerTunnel_Params) String() string { return str } -func (s TunnelServer_registerTunnel_Params) Auth() (Authentication, error) { +func (s TunnelServer_registerTunnel_Params) OriginCert() ([]byte, error) { p, err := s.Struct.Ptr(0) - return Authentication{Struct: p.Struct()}, err + return []byte(p.Data()), err } -func (s TunnelServer_registerTunnel_Params) HasAuth() bool { +func (s TunnelServer_registerTunnel_Params) HasOriginCert() bool { p, err := s.Struct.Ptr(0) return p.IsValid() || err != nil } -func (s TunnelServer_registerTunnel_Params) SetAuth(v Authentication) error { - return s.Struct.SetPtr(0, v.Struct.ToPtr()) -} - -// NewAuth sets the auth field to a newly -// allocated Authentication struct, preferring placement in s's segment. -func (s TunnelServer_registerTunnel_Params) NewAuth() (Authentication, error) { - ss, err := NewAuthentication(s.Struct.Segment()) - if err != nil { - return Authentication{}, err - } - err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) - return ss, err +func (s TunnelServer_registerTunnel_Params) SetOriginCert(v []byte) error { + return s.Struct.SetData(0, v) } func (s TunnelServer_registerTunnel_Params) Hostname() (string, error) { @@ -844,10 +819,6 @@ func (p TunnelServer_registerTunnel_Params_Promise) Struct() (TunnelServer_regis return TunnelServer_registerTunnel_Params{s}, err } -func (p TunnelServer_registerTunnel_Params_Promise) Auth() Authentication_Promise { - return Authentication_Promise{Pipeline: p.Pipeline.GetPipeline(0)} -} - func (p TunnelServer_registerTunnel_Params_Promise) Options() RegistrationOptions_Promise { return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} } @@ -1060,74 +1031,73 @@ func (p TunnelServer_getServerInfo_Results_Promise) Result() ServerInfo_Promise return ServerInfo_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\x94To\x88\x14e\x18\x7f~\xef\xbb3\xabp" + - "\xb6;\xcc\x0aut\x08b\x90\x82\xe6e\x86\x99\xb4\xe7" + - "\xa5\xe6^\xa7\xb7\xaf]a\xe9\x07\xc7\xbd\xf7\xf6\xc6f" + - "g\xb6\x99\xd9\xcb\x8b\xd4\x92 \x0c2R\xfb\xd2\x87\xc8" + - "\xfbf`%\x14E\x18\x9c\xd0\x1f\xc1\"\x02\x0b\xad\xeb" + - "C\x98\x04RH\xd2\x87\x0cb\xe2\x9d\xbd\xd9\x99\xee\x94" + - "\xf0\xdb\xfb\xe7y\x7f\xef\xef\xf9=\xcf\xf3[9\xc1\xfa" + - "X\xafv\x8fF$\xd6iz\xb4\xae\xf1\xcd\xe4\xfdo" + - "\x9c{\x89\x8c\"\x8b\xf6\x9f\x1e(]\x0f\x0f\xfeH\x84" + - "U\xfb\xd82\x98\xaf\xb2<\x91y\x88\x0d\x11\xa2\x85\x15" + - "LO\xf5\xe6>\"\xa3\x07D\x1a\xcf\x13\xad:\xce\xde" + - "\x04\xc1<\xc5\xde#D=\xbf\xf7/p\xaf\x1e\x9c\"" + - "\xa3\x88\x14*\x0e4\xb7\xf0\xbf\xcd'\xe3\xd5\xe3\\\xc5" + - "\x0e\xec8zD\xbb|\xf4K\x12Ed\x835\x85\xfa" + - "\x07_\x0c\x139\xb5\xfc\x87\xbf\x06B\xd4\xfd\xfe\x83\xef" + - "\xf6\x8f\\<7\x0b:fwB\x9b4O\xa9w\xe6" + - "I\xedYB\xc4.[w\xbc\xf0\xfdC\xd3m\x9e1" + - "\xca|\xfd\x08(\x17m}b\xc7\x9e\xf9\xfb.]\x9a" + - "\xc9\x00\xea\xea\xba\x16g0_/\x13\xa2\xd5\xbb\xd6\xcb" + - "\x9dk\xb6_!\xa3\xc8\xb3b\x98K\xf5+\xe6j]" + - "\xfd\xd1\xab\xbfl\x1eR\xab\xe8\xf0\xfe\x0dC\x0f,>" + - "s-\x8b\xf6\x8c>\xa9\xd0^\x8c\xd1F\xd7\xfc\xf6\xc8" + - "]\x87\xbf\xb86\x8b\xb4\x0a4\x8f\xeb?\x98'c\xc0" + - "\x13*\xf6\xea\xa6\xb7\xcew\x17\xba\xff\x9c\xa5F\xac\xf1" + - "\xd7z7\xcc\x9f\xe2\xd8\x8b\xfa\xaf\xb4<\x0a[\xae+" + - "\x1d\xbf\xc9k+jV\xd3m\xae\xdd\xb8\xd7\x0eB\xdb" + - "\xad\x0f\xc7\x17U\xaf\xe0\xd8\xb5\x89* \xba\xc0\x88\x8c" + - "\x9e\xb5D\x80\xb1\xf0)\"0\xc3\xe8'*\xdbu\xd7" + - "\xf3e4b\x075\xcfu%\xf1Zx`\xb7\xe5X" + - "nMv\xe0\xb5\x04\xbe\x0d\xfb\x98\xf4\xc7\xa5\xbf\xc2\x97" + - "u;\x08\xa5\xdf>\\R\xb5|\x8b7\x02\xd1\xc5s" + - "D9\x10\x19\x1b\x97\x11\x89>\x0e1\xc8`\x00%\xa8" + - "\xc3\xca\x00\x91\xd8\xcc!\x86\x19\x0c\xc6J1/\xd1O" + - "$\x069\xc4v\x86\x82\xd5\x0a\xc7PL{\x88\x80\"" + - "!\x1a\xf3\x82\xd0\xb5\x1a\x92\x88\xd0E\x0c]\x84\x03^" + - "3\xb4=7@1\xed\xa2\x99\xe8\x84:K\xa8\xafo" + - "\x85c\xd2\x0d\xedr\xcdRobMR\xa6\x8bo\xc4" + - "\xf4^\"\xb1\x81CT3L\xb7\xecN\x99\xe6\x9f\x96" + - "\x13\x09\x95E\xb2a\xd9N\xb2\x8b<\xdf\xae\xdb\xee\xc3" + - "\xeb)\xffh\x1a3\xb7\\\xdbb\x09\xfd\x98\xd1P3" + - "\xb4\xf3\x9e\x1b(fwv\x98}\xa8\xe4\xfa\x80CL" + - "e\x98}\xaa\xe4\xfa\x98C|\x96av\xa6\x9bH\x9c" + - "\xe6\x10g\x19\xc0K\xe0D\xc6\xe7\xef\x10\x89\xb3\x1c\xe2" + - "<\x83\x91\xe3%\xe4\x88\x8co\xd7\x12\x89\xaf8\xc4\x05" + - "\x06C+\x96\xa0\x11\x19\xdf}B$.p\x88_\x18" + - "\x0c=W\x82Nd\xfc\xac\x0a8\xcd!\xfeb\x88j" + - "\x8e-\xdd\xb02\x92\xd5\x7f\\\xfa\x81\xed\xb9\xc9\x9e{" + - "A'W9\xd3\x89\xf8O+\xa2\x90\xda\x0c\x01\x05B" + - "\xb9\xe9yNe$\xf3\xae\xe9\x05\xb2\xe2\"\x94\xbek" + - "9\x9b\xbdr\xbb\xec\x001\x80P\x08\xadz\x80\xdb\x08" + - "U\x0e\x14S; \xa8\xc3\x8e\xc4H$\xce\x0f[u" + - "%\xe9\xbc\x8e\xa4KUVK8\xc4\xca\x8c\xa4\xcbU" + - "\xb1\xef\xe6\x10\xf71\x14\xe2\xff\x92\xc2\x8e[NK\xce" + - ")\xe1\x8dG\xa2.\xc3\xf6\xaa\xe2\x8ez\xf1D4\x10" + - "\xdc\xd2\x9bm2h9<\x0cD\xae\xc3w\x81\xaa\xd7" + - "<\x0eQb(\xfb\xea>D1\xb5\x94\x9b5|\xf2" + - "IAa\xb7\x15\xd0\x88:\xde\x8d\xc4\xb4\x8c\xde\xe7\x88" + - "\x19K\xf3H\xfd\x12\x89=\x1a=>1ca>J" + - "f\x9d\xcam\xd8>D\x09oZ\x143\xefC\x15\xb8" + - "5\xc7P\xb9\xe6\x9d\xff\xcf5\xb1\xc4\x9be\x9a\xc8\xc7" + - "G=\x95g\x06m\x0f\x91\xe8\xe2\x10\xb73D\x8e\xd7" + - "\x9e|*l\xcd\x94w\xeeL\xb6\xc9\xa5\x93\xc9\xdbf" + - "Q\xec\xa0Z\xca,vr\x88\xb1L\xffH\xd5T\xbb" + - "8\xc4\xf3\x99\x91\x9cP\xc3\xbb\x97C\x1cKG\xf2\xf5" + - "W\x88\xc41\x0e\xf16C^\xfa~B\xa4\xd0\xf2\x9d" + - "N_\xab3\xd5\xcd\x8eW\x1f\xb4]\x19\xa8\x99\x9bu" + - "\xd5\x94~\xc3r\xa5\x8bp\x93e;-_\xaaFh" + - "\x8f\xc8\xbf\x01\x00\x00\xff\xff\x80\xf4\x060" +const schema_db8274f9144abc7e = "x\xda\x9cT_h\x1c\xd5\x1b\xfd\xce\xbd3\xbb-$" + + "\xbf\xcd0\x1bh\x17B\xa0\xe4\x87\xb6\xd0?\xb1*5" + + "\x167\x89m%1m\xf6\xc6Vk\x9b\x87N7\xb7" + + "\x9b\x89\xb33\xeb\xccl\xb4\x85\xb4Z\"R\xc1\xa2\x96" + + "\x82\x05\x11\x15\x0b*E\xfb\xa0h\xa0\x0f\xf5E\x91\"" + + "\xfa\xa0\x82\xd8\x17-E,J!\xf8\xe2\xd3\xc8\x9d\xcd" + + "\xec\x8c)\xa6\xd2\xb7;w\xbe?\xe7\x9c\xfb}g\xcb" + + "\xd3l\x90\xf5\xeb\x9bu\"\xb1]\xcfE\xdb\xeb\xdf\xbc" + + "s\xff\xd9+\xf3d\x94Xt\xfc\xd2h\xf1\xaf\xf0\xe4" + + "OD\xd8:\xc7\x8e\xc1|\x95\xe5\x89\xcc\x97\xd98!" + + "\xea\x1e\xc1\xd5\xcb\xfd\xda\xa7d\xdc\x05\"\x9d\xe7\x89\xb6" + + "\x9eg7@0\x17\xd8G\x84\xa8\xe7\x8f\xe1N\xf7\xe6" + + "\xc9\xcbd\x94\x90\x96j\x05>\xc9Ga\xd6\xd5\xd1\xb4" + + "\xb9\x0a\x1e=x\xe65\xfd\xfa\x99/I\x94\x90\x8d\xd6" + + "U\xb4\xae\xf90\xd7j\xea\xd8\xad=\x01BT\xba\xf8" + + "\xe0\x87\xc3S?^YV;\x867\xa7/\x9a\xa7T" + + "\x9e\xf9\x82\xfe\x0c!b\xd7\xad\xb5\xcf\xfd\xf0\xd0\xd5\x16" + + "\xd0\xb8\xca\xcf\xfa/ -\xda\xf3\xf8\xc1\x99\xd5s\xd7" + + "\xae-Q\x80\xfa\xf5\xbd\x1eS\xf8M/\x13\xa2\xfb\x0e" + + "\x0d\xc9\xc9m\xfbo\x90Q\xe2\xffPcun\x00\xe6" + + "\xda\x9cj\xd2\x9d{\xd1\xac\xabSt\xfa\xf8\x8e\xf1\x07" + + "\xd6}\xbe\x98-\xb7/\xb7\xa8\xca\xd99U\xee\xc8\xb6" + + "\xdf\x1f\xf9\xff\xe9/\x16\x97\xa1\x8e\x03O\xe56\xc0<" + + "\x17W<\xab\x82o\xeez\xf3\xbbR\xa1\xf4\xe72=" + + "b\xf5\x16r30\xbf\x8ec\xbf\xca\xfdJ\x1b\xa3\xb0" + + "\xe9\xba\xd2\xf1\x1bZusr\xacn\xaaZ\x0d\xb71" + + "\xb0\xf3Y;\x08m\xb7\xb67\xbe/W<\xc7\xae\x1e" + + "\xad\x00\xa2\x03\x8c\xc8\xe8\x19 \x02\x8c\xee\x03D`\x86" + + "1LT\xb6k\xae\xe7\xcbh\xca\x0e\xaa\x9e\xebJ\xe2" + + "\xd5\xf0\xc4a\xcb\xb1\xdc\xaal7\xca\xdd\xda\xa8\xd5\xe0" + + "1\xe9\xcfJ\x7f\x93/kv\x10J\xbfu\xd9W\xb1" + + "\x0a\xbeU\x0fD\x07\xd7\x884\x10\x19;\x0f\x10\x89\x1d" + + "\x1c\xa2\xc2`\x00E\xa8\xcb\xdd\xa3Db\x8cC\xecg" + + "0\x18+\xc6\x08\xf7\x0d\x13\x89\x0a\x87\x98d\x88<\xdf" + + "\xae\xd9\xee\xc3\x92\xb8\x1f\xa2\x93\x18:\x09\xd1\xb4\x17\x84" + + "\xaeU\x97D\x84\x0eb\xe8 \x9c\xf0\x1a\xa1\xed\xb9\x01" + + "\xba\xd2\xc9\"\xa0\x8b\xb0\x92VC\xcdpZ\xba\xa1]" + + "\xb5T2Q,S\x0ay\x1d\x91\x18\xe4\x10c\x19\xc8" + + "#\xf7dx$\x90w\x1fNy\xe4\x9f\x92G\x13T" + + "\xbd\xb2n\xd9N\xf2\x95\x90\x19\xa2\xfc\xa3i\xccJ\xf8" + + "&bU\xfd\x18\xddx\xa37f\xa80\xaeic<" + + "\xa7\x14|\x9dC\xbc\x9b\xc1\xf8\xb6R\xf0\x0d\x0e\xf1^" + + "\x06\xe3\xf9\x12\x91x\x8bC\\`\x00/\x82\x13\x19\xef" + + "\x7f@$.p\x88\xcf\x18\x0c\x8d\x17\xa1\x11\x19\x9f\x0c" + + "\x10\x89\x8b\x1c\xe2\x12\x83\xa1kE\xe8D\xc6\xc2\x06\"" + + "\xf11\x87\xf8\x96!\xaa:\xb6t\xc3\x91\xa9\xac\xfe\xb3" + + "\xd2\x0fl\xcfM\xbe\xb9\x17\xb4\x09\xca\xa5\x89Dk8" + + "*^A\x8d$\x0a\xa9\xf7\x10P \x94\x1b\x9e\xe7\x8c" + + "L%y\x85\xd0\xaa\x05\xf8\x1f\xa1\xc2\x81\xae\xd4\x01\x08" + + "\xea\xb2-\x1b[.[oc`\xafUS2\xadj" + + "\xcb\xb4^\xc1\xef\xe3\x10[22mTOy7\x87" + + "\xb8\x97\xa1\xa0\xe6\xa9\xfdl\xb3\x96\xd3\x94\xb7<\xd0\xed" + + "v\xa0&\xc3\xd6i\xc4=\xe2\xf5U,?o\xd5\x83" + + ";\xcc\x9e\x90A\xa1\xe9\x84\x81\xd0\xda\x1c:\xd5\xbb\xac" + + "\xe2\x10E\x86\xb2/\x83\xa6\x13\xa2+\xb5\x98e\xd3\xce" + + "\xff\xad]\xb9\xd5\xa5\xa5\x8fN\xd4\xf6u$vf\xf4" + + "\x1f#f\xac\xcf#\xb5R$\xcei\xf4\xf8\xc4\x8c\xee" + + "|\x94,<\x95[e\x07\x11%\x0c\xa87\xe60\x88" + + "\x0ap\xa7\x062!{\x83\xff\xc2?q\xcd\xdb\xb3o" + + "\xf5)(d\x8a{\xa6\xee\x0c\x91\xe8\xe0\x10k\x18\"" + + "\xc7[\xf2\x82\xc2\x9e\xcc@\xac\xb4\xa3-\xc0\xc9\xa6\x16" + + "T\xb2\xaa\xdf\xd5\xaeo)\x1b\x99\xe4\x10\xd3\x99\xd9\x93" + + "\xea\xf2\x10\x87p2+j\xabe\x9e\xe6\x10\xf3\xe9\x8a" + + ">\xff\x12\x91\x98\xe7\x10\xaf0\xe4\xa5\xef'\x90\xf2M" + + "?5\x16\xc7\xab\x8d\xd9\xae\x0c\xd4B.-\x8c\xfa\xa5" + + "\xd6\xa4!\xfd\xba\xe5J\x17\xe1.\xcbv\x9a\xbeT\x83" + + "B\x0c \xfc\x1d\x00\x00\xff\xff\xfa.\x1fg" func init() { schemas.Register(schema_db8274f9144abc7e,