Release 2017.11.1

This commit is contained in:
Chris Branch 2017-11-07 15:17:19 +00:00
parent 82cb539fbe
commit ff67bd23f2
10 changed files with 537 additions and 223 deletions

View File

@ -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 {

View File

@ -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 = `
</style>
</head>
<body class="sans-serif black">
<div class="bt bw2 b--orange bg-white pb6">
<div class="bt bw2 b--orange bg-white pb6">
<div class="mw7 center ph4 pt3">
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 109 40.5" class="mw4">
<path class="st0" d="M98.6 14.2L93 12.9l-1-.4-25.7.2v12.4l32.3.1z"/>
@ -56,21 +54,30 @@ const indexTemplate = `
running an encrypted, virtual tunnel from your laptop or server to
Cloudflare's edge network.
</p>
<p class="b f5 mt5 fw6">Ready for the next step?</p>
<a
class="fw6 link white bg-blue ph4 pv2 br1 dib f5 link-hover"
<p class="b f5 mt5 fw6">Ready for the next step?</p>
<a
class="fw6 link white bg-blue ph4 pv2 br1 dib f5 link-hover"
style="border-bottom: 1px solid #1f679e"
href="https://warp.cloudflare.com">
Get started here
</a>
{{if .Tags}} <section>
<h4 class="f6 fw4 pt5 mb2">Connection</h4>
<section>
<h4 class="f6 fw4 pt5 mb2">Request</h4>
<dl class="bl bw2 b--orange ph3 pt3 pb2 bg-light-gray f7 code overflow-x-auto mw-100">
{{range .Tags}} <dt class="ttu mb1">{{.Name}}</dt>
<dd class="ml0 mb3 f5">{{.Value}}</dd>
{{end}} </dl>
<dd class="ml0 mb3 f5">Method: {{.Request.Method}}</dd>
<dd class="ml0 mb3 f5">Protocol: {{.Request.Proto}}</dd>
<dd class="ml0 mb3 f5">Request URL: {{.Request.URL}}</dd>
<dd class="ml0 mb3 f5">Transfer encoding: {{.Request.TransferEncoding}}</dd>
<dd class="ml0 mb3 f5">Host: {{.Request.Host}}</dd>
<dd class="ml0 mb3 f5">Remote address: {{.Request.RemoteAddr}}</dd>
<dd class="ml0 mb3 f5">Request URI: {{.Request.RequestURI}}</dd>
{{range $key, $value := .Request.Header}}
<dd class="ml0 mb3 f5">Header: {{$key}}, Value: {{$value}}</dd>
{{end}}
<dd class="ml0 mb3 f5">Body: {{.Body}}</dd>
</dl>
</section>
{{end}} </div>
</div>
</div>
</body>
</html>
@ -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
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 <subdomain>.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);
}

View File

@ -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,