Merge branch 'master' of ssh://stash.cfops.it:7999/tun/cloudflared

This commit is contained in:
Areg Harutyunyan 2019-07-10 11:46:13 -05:00
commit 583bad4972
63 changed files with 3415 additions and 1539 deletions

14
Gopkg.lock generated
View File

@ -220,6 +220,14 @@
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09" revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
version = "v0.1.0" version = "v0.1.0"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]] [[projects]]
digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b" digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b"
name = "github.com/lib/pq" name = "github.com/lib/pq"
@ -359,12 +367,12 @@
version = "v2.4" version = "v2.4"
[[projects]] [[projects]]
digest = "1:5f2aaa360f48d1711795bd88c7e45a38f86cf81e4bc01453d20983baa67e2d51" digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e" revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
version = "v1.0.3" version = "v1.4.2"
[[projects]] [[projects]]
digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c" digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c"

View File

@ -26,7 +26,7 @@
[[constraint]] [[constraint]]
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"
version = "=1.0.3" version = "=1.4.2"
[[constraint]] [[constraint]]
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"

View File

@ -78,6 +78,6 @@ tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
.PHONY: vet .PHONY: vet
vet: vet:
go vet ./... go vet -composites=false ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype which go-sumtype # go get github.com/BurntSushi/go-sumtype
go-sumtype $$(go list ./...) go-sumtype $$(go list ./...)

View File

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

View File

@ -1,6 +1,7 @@
package tunnel package tunnel
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -11,9 +12,17 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/cloudflare/cloudflared/h2mux"
"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" "github.com/getsentry/raven-go"
"golang.org/x/crypto/ssh/terminal" "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/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/cmd/sqlgateway" "github.com/cloudflare/cloudflared/cmd/sqlgateway"
@ -235,9 +244,8 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
return err return err
} }
buildInfo := origin.GetBuildInfo() buildInfo := buildinfo.GetBuildInfo(version)
logger.Infof("Build info: %+v", *buildInfo) buildInfo.Log(logger)
logger.Infof("Version %s", version)
logClientOptions(c) logClientOptions(c)
if c.IsSet("proxy-dns") { if c.IsSet("proxy-dns") {
@ -253,16 +261,6 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
// Wait for proxy-dns to come up (if used) // Wait for proxy-dns to come up (if used)
<-dnsReadySignal <-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")) metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil { if err != nil {
logger.WithError(err).Error("Error opening metrics server listener") logger.WithError(err).Error("Error opening metrics server listener")
@ -280,6 +278,33 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
go writePidFile(connectedSignal, c.String("pidfile")) 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 // Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) { if dnsProxyStandAlone(c) {
connectedSignal.Notify() connectedSignal.Notify()
@ -288,6 +313,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
} }
if c.IsSet("hello-world") { if c.IsSet("hello-world") {
logger.Infof("hello-world set")
helloListener, err := hello.CreateTLSListener("127.0.0.1:") helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil { if err != nil {
logger.WithError(err).Error("Cannot start Hello World Server") logger.WithError(err).Error("Cannot start Hello World Server")
@ -324,7 +350,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() 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")) return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
@ -349,6 +375,110 @@ func Before(c *cli.Context) error {
return nil 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
}
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{
{
TunnelHostname: h2mux.TunnelHostname(c.String("hostname")),
Origin: reverseProxyOrigin,
},
},
}
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
}
cloudflaredConfig := &connection.CloudflaredConfig{
CloudflaredID: cloudflaredID,
Tags: tags,
BuildInfo: buildInfo,
}
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, func waitToShutdown(wg *sync.WaitGroup,
errC chan error, errC chan error,
shutdownC, graceShutdownC chan struct{}, shutdownC, graceShutdownC chan struct{},
@ -422,8 +552,8 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}, },
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq", Name: "autoupdate-freq",
Usage: "Autoupdate frequency. Default is 24h.", Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq),
Value: time.Hour * 24, Value: updater.DefaultCheckUpdateFreq,
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
@ -637,6 +767,18 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Value: time.Second * 90, Value: time.Second * 90,
Hidden: shouldHide, 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{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns", Name: "proxy-dns",
Usage: "Run a DNS over HTTPS proxy server.", Usage: "Run a DNS over HTTPS proxy server.",
@ -696,5 +838,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"}, EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
Hidden: true, 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,
}),
} }
} }

View File

@ -12,7 +12,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
@ -145,7 +147,7 @@ If you don't have a certificate signed by Cloudflare, run the command:
func prepareTunnelConfig( func prepareTunnelConfig(
c *cli.Context, c *cli.Context,
buildInfo *origin.BuildInfo, buildInfo *buildinfo.BuildInfo,
version string, logger, version string, logger,
transportLogger *logrus.Logger, transportLogger *logrus.Logger,
) (*origin.TunnelConfig, error) { ) (*origin.TunnelConfig, error) {
@ -272,6 +274,15 @@ func prepareTunnelConfig(
}, nil }, nil
} }
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"))
}
// Otherwise lookup edge server addresses through service discovery
return connection.NewEdgeAddrResolver(logger)
}
func isRunningFromTerminal() bool { func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd())) return terminal.IsTerminal(int(os.Stdout.Fd()))
} }

View File

@ -1,6 +1,7 @@
package updater package updater
import ( import (
"context"
"os" "os"
"runtime" "runtime"
"time" "time"
@ -14,6 +15,7 @@ import (
) )
const ( const (
DefaultCheckUpdateFreq = time.Hour * 24
appID = "app_idCzgxYerVD" 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/" 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." noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
@ -75,30 +77,6 @@ func Update(_ *cli.Context) error {
return updateOutcome.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 // Checks for an update and applies it if one is available
func loggedUpdate() UpdateOutcome { func loggedUpdate() UpdateOutcome {
updateOutcome := checkForUpdateAndApply() updateOutcome := checkForUpdateAndApply()
@ -112,7 +90,88 @@ func loggedUpdate() UpdateOutcome {
return 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 { 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" { if runtime.GOOS == "windows" {
logger.Info(noUpdateOnWindowsMessage) logger.Info(noUpdateOnWindowsMessage)
return false return false
@ -122,8 +181,7 @@ func IsAutoupdateEnabled(c *cli.Context) bool {
logger.Info(noUpdateInShellMessage) logger.Info(noUpdateInShellMessage)
return false return false
} }
return true
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
} }
func isRunningFromTerminal() bool { func isRunningFromTerminal() bool {

View File

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

View File

@ -2,14 +2,14 @@ package connection
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"sync"
"time" "time"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -17,7 +17,6 @@ import (
) )
const ( const (
dialTimeout = 5 * time.Second
openStreamTimeout = 30 * time.Second openStreamTimeout = 30 * time.Second
) )
@ -29,134 +28,54 @@ func (e dialError) Error() string {
return e.cause.Error() return e.cause.Error()
} }
type muxerShutdownError struct{} type Connection struct {
id uuid.UUID
func (e muxerShutdownError) Error() string { muxer *h2mux.Muxer
return "muxer shutdown"
} }
type ConnectionConfig struct { func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) {
TLSConfig *tls.Config id, err := uuid.NewRandom()
HeartbeatInterval time.Duration if err != nil {
MaxHeartbeats uint64 return nil, err
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
} }
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 // 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) openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
defer cancel() defer cancel()
conn, err := h.newRPConn(openStreamCtx)
rpcConn, err := c.newRPConn(openStreamCtx, logger)
if err != nil { 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() defer rpcConn.Close()
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)}
return tsClient.Connect(ctx, parameters) return tsClient.Connect(ctx, parameters)
} }
func (h *h2muxHandler) shutdown() { func (c *Connection) Shutdown() {
h.muxer.Shutdown() c.muxer.Shutdown()
} }
func (h *h2muxHandler) newRPConn(ctx context.Context) (*rpc.Conn, error) { func (c *Connection) newRPConn(ctx context.Context, logger *logrus.Entry) (*rpc.Conn, error) {
stream, err := h.muxer.OpenStream(ctx, []h2mux.Header{ stream, err := c.muxer.OpenRPCStream(ctx)
{Name: ":method", Value: "RPC"},
{Name: ":scheme", Value: "capnp"},
{Name: ":path", Value: "*"},
}, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return rpc.NewConn( return rpc.NewConn(
tunnelrpc.NewTransportLogger(h.logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)), tunnelrpc.NewTransportLogger(logger.WithField("rpc", "connect"), rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(h.logger.WithField("subsystem", "rpc-transport")), tunnelrpc.ConnLog(logger.WithField("rpc", "connect")),
), nil ), 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()
}
}

View File

@ -5,10 +5,11 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net" "net"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const (
@ -22,6 +23,9 @@ const (
dotServerName = "cloudflare-dns.com" dotServerName = "cloudflare-dns.com"
dotServerAddr = "1.1.1.1:853" dotServerAddr = "1.1.1.1:853"
dotTimeout = time.Duration(15 * time.Second) dotTimeout = time.Duration(15 * time.Second)
// SRV record resolution TTL
resolveEdgeAddrTTL = 1 * time.Hour
) )
var friendlyDNSErrorLines = []string{ var friendlyDNSErrorLines = []string{
@ -34,20 +38,65 @@ var friendlyDNSErrorLines = []string{
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`, ` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
} }
func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, error) { // EdgeServiceDiscoverer is an interface for looking up Cloudflare's edge network addresses
if len(addresses) > 0 { type EdgeServiceDiscoverer interface {
var tcpAddrs []*net.TCPAddr // Addr returns an address to connect to cloudflare's edge network
for _, address := range addresses { Addr() *net.TCPAddr
// Addresses specified (for testing, usually) // AvailableAddrs returns the number of unique addresses
tcpAddr, err := net.ResolveTCPAddr("tcp", address) AvailableAddrs() uint8
if err != nil { // Refresh rediscover Cloudflare's edge network addresses
return nil, err Refresh() error
} }
tcpAddrs = append(tcpAddrs, tcpAddr)
} // EdgeAddrResolver discovers the addresses of Cloudflare's edge network through SRV record.
return tcpAddrs, nil // 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) _, addrs, err := net.LookupSRV(srvService, srvProto, srvName)
if err != nil { if err != nil {
// Try to fall back to DoT from Cloudflare directly. // 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 resolvedIPsPerCNAME [][]*net.TCPAddr
var lookupErr error var lookupErr error
for _, addr := range addrs { for _, addr := range addrs {
ips, err := ResolveSRVToTCP(addr) ips, err := resolveSRVToTCP(addr)
if err != nil || len(ips) == 0 { if err != nil || len(ips) == 0 {
// don't return early, we might be able to resolve other addresses // don't return early, we might be able to resolve other addresses
lookupErr = err lookupErr = err
@ -86,14 +135,14 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err
} }
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips) resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
} }
ips := FlattenServiceIPs(resolvedIPsPerCNAME) ips := flattenServiceIPs(resolvedIPsPerCNAME)
if lookupErr == nil && len(ips) == 0 { if lookupErr == nil && len(ips) == 0 {
return nil, fmt.Errorf("Unknown service discovery error") return nil, fmt.Errorf("Unknown service discovery error")
} }
return ips, lookupErr 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) ips, err := net.LookupIP(srv.Target)
if err != nil { if err != nil {
return nil, err 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 // 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. // 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 var result []*net.TCPAddr
for len(ipsByService) > 0 { for len(ipsByService) > 0 {
filtered := 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
}

View File

@ -7,8 +7,26 @@ import (
"github.com/stretchr/testify/assert" "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) { func TestFlattenServiceIPs(t *testing.T) {
result := FlattenServiceIPs([][]*net.TCPAddr{ result := flattenServiceIPs([][]*net.TCPAddr{
[]*net.TCPAddr{ []*net.TCPAddr{
&net.TCPAddr{Port: 1}, &net.TCPAddr{Port: 1},
&net.TCPAddr{Port: 2}, &net.TCPAddr{Port: 2},

281
connection/manager.go Normal file
View File

@ -0,0 +1,281 @@
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
}
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, "handshake with edge error")
}
h2muxConn, err := newConnection(muxer, edgeIP)
if err != nil {
return errors.Wrap(err, "create h2mux connection error")
}
go em.serveConn(ctx, h2muxConn)
connResult, err := h2muxConn.Connect(ctx, &pogs.ConnectParameters{
OriginCert: em.state.getUserCredential(),
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
NumPreviousAttempts: 0,
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
}, em.logger)
if err != nil {
h2muxConn.Shutdown()
return errors.Wrap(err, "connect with edge error")
}
if connErr := connResult.Err; connErr != nil {
if !connErr.ShouldRetry {
return errors.Wrap(connErr, em.noRetryMessage())
}
return errors.Wrapf(connErr, "server respond with retry at %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
}

View File

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

View File

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

View File

@ -94,6 +94,14 @@ type Header struct {
Name, Value string 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. // Handshake establishes a muxed connection with the peer.
// After the handshake completes, it is possible to open and accept streams. // After the handshake completes, it is possible to open and accept streams.
func Handshake( 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 { func (m *Muxer) Metrics() *MuxerMetrics {
return m.muxMetricsUpdater.metrics() return m.muxMetricsUpdater.metrics()
} }

View File

@ -68,7 +68,8 @@ type MuxedStream struct {
sentEOF bool sentEOF bool
// true if the peer sent us an EOF // true if the peer sent us an EOF
receivedEOF bool receivedEOF bool
// If valid, tunnelHostname is used to identify which origin service is the intended recipient of the request
tunnelHostname TunnelHostname
// Compression-related fields // Compression-related fields
receivedUseDict bool receivedUseDict bool
method string method string
@ -195,6 +196,25 @@ func (s *MuxedStream) WriteHeaders(headers []Header) error {
return nil 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 { func (s *MuxedStream) getReceiveWindow() uint32 {
s.writeLock.Lock() s.writeLock.Lock()
defer s.writeLock.Unlock() defer s.writeLock.Unlock()

View File

@ -98,3 +98,30 @@ func TestMuxedStreamEOF(t *testing.T) {
assert.Equal(t, 0, n) 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())
}
}

View File

@ -11,6 +11,10 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
const (
CloudflaredProxyTunnelHostnameHeader = "cf-cloudflared-proxy-tunnel-hostname"
)
type MuxReader struct { type MuxReader struct {
// f is used to read HTTP2 frames. // f is used to read HTTP2 frames.
f *http2.Framer f *http2.Framer
@ -235,6 +239,8 @@ func (r *MuxReader) receiveHeaderData(frame *http2.MetaHeadersFrame) error {
if r.dictionaries.write != nil { if r.dictionaries.write != nil {
continue continue
} }
case CloudflaredProxyTunnelHostnameHeader:
stream.tunnelHostname = TunnelHostname(header.Value)
} }
headers = append(headers, Header{Name: header.Name, Value: header.Value}) headers = append(headers, Header{Name: header.Name, Value: header.Value})
} }

107
h2mux/muxreader_test.go Normal file
View File

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

View File

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

View File

@ -6,6 +6,8 @@ import (
"net" "net"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
@ -34,6 +36,8 @@ type Supervisor struct {
// currently-connecting tunnels to finish connecting so we can reset backoff timer // currently-connecting tunnels to finish connecting so we can reset backoff timer
nextConnectedIndex int nextConnectedIndex int
nextConnectedSignal chan struct{} nextConnectedSignal chan struct{}
logger *logrus.Entry
} }
type resolveResult struct { type resolveResult struct {
@ -51,6 +55,7 @@ func NewSupervisor(config *TunnelConfig) *Supervisor {
config: config, config: config,
tunnelErrors: make(chan tunnelError), tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{}, 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 { func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
logger := s.config.Logger logger := s.logger
edgeIPs, err := connection.ResolveEdgeIPs(logger, s.config.EdgeAddrs)
edgeIPs, err := s.resolveEdgeIPs()
if err != nil { if err != nil {
logger.Infof("ResolveEdgeIPs err") logger.Infof("ResolveEdgeIPs err")
return err return err
@ -215,6 +222,15 @@ func (s *Supervisor) getEdgeIP(index int) *net.TCPAddr {
return s.edgeIPs[index%len(s.edgeIPs)] 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() { func (s *Supervisor) refreshEdgeIPs() {
if s.resolverC != nil { if s.resolverC != nil {
return return
@ -224,7 +240,7 @@ func (s *Supervisor) refreshEdgeIPs() {
} }
s.resolverC = make(chan resolveResult) s.resolverC = make(chan resolveResult)
go func() { go func() {
edgeIPs, err := connection.ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs) edgeIPs, err := s.resolveEdgeIPs()
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err} s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
}() }()
} }

View File

@ -14,9 +14,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/streamhandler"
"github.com/cloudflare/cloudflared/tunnelrpc" "github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
@ -41,7 +42,7 @@ const (
) )
type TunnelConfig struct { type TunnelConfig struct {
BuildInfo *BuildInfo BuildInfo *buildinfo.BuildInfo
ClientID string ClientID string
ClientTlsConfig *tls.Config ClientTlsConfig *tls.Config
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once 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 { func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
ctx, cancel := context.WithCancel(context.Background()) return NewSupervisor(config).Run(ctx, connectedSignal, cloudflaredID)
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 ServeTunnelLoop(ctx context.Context, func ServeTunnelLoop(ctx context.Context,
@ -471,39 +436,6 @@ func LogServerInfo(
metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName) 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) { func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}} h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
for headerName, headerValues := range h1.Header { for headerName, headerValues := range h1.Header {
@ -514,10 +446,6 @@ func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
return return
} }
func FindCfRayHeader(h1 *http.Request) string {
return h1.Header.Get("Cf-Ray")
}
type TunnelHandler struct { type TunnelHandler struct {
originUrl string originUrl string
muxer *h2mux.Muxer muxer *h2mux.Muxer
@ -605,8 +533,8 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
return reqErr return reqErr
} }
cfRay := FindCfRayHeader(req) cfRay := streamhandler.FindCfRayHeader(req)
lbProbe := isLBProbeRequest(req) lbProbe := streamhandler.IsLBProbeRequest(req)
h.logRequest(req, cfRay, lbProbe) h.logRequest(req, cfRay, lbProbe)
var resp *http.Response var resp *http.Response
@ -629,7 +557,7 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request,
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest") return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
} }
err = H2RequestHeadersToH1Request(stream.Headers, req) err = streamhandler.H2RequestHeadersToH1Request(stream.Headers, req)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "invalid request received") return nil, errors.Wrap(err, "invalid request received")
} }
@ -759,10 +687,6 @@ func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10) 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. // Print out the given lines in a nice ASCII box.
func asciiBox(lines []string, padding int) (box []string) { func asciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines) maxLen := maxLen(lines)

View File

@ -8,6 +8,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
@ -22,20 +23,22 @@ import (
// OriginService is an interface to proxy requests to different type of origins // OriginService is an interface to proxy requests to different type of origins
type OriginService interface { type OriginService interface {
Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error) Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error)
URL() *url.URL
Summary() string
Shutdown() Shutdown()
} }
// HTTPService talks to origin using HTTP/HTTPS // HTTPService talks to origin using HTTP/HTTPS
type HTTPService struct { type HTTPService struct {
client http.RoundTripper client http.RoundTripper
originAddr string originURL *url.URL
chunkedEncoding bool chunkedEncoding bool
} }
func NewHTTPService(transport http.RoundTripper, originAddr string, chunkedEncoding bool) OriginService { func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
return &HTTPService{ return &HTTPService{
client: transport, client: transport,
originAddr: originAddr, originURL: url,
chunkedEncoding: chunkedEncoding, chunkedEncoding: chunkedEncoding,
} }
} }
@ -55,13 +58,13 @@ func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*htt
resp, err := hc.client.RoundTrip(req) resp, err := hc.client.RoundTrip(req)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Error proxying request to HTTP origin") return nil, errors.Wrap(err, "error proxying request to HTTP origin")
} }
defer resp.Body.Close() defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp)) err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Error writing response header to HTTP origin") return nil, errors.Wrap(err, "error writing response header to HTTP origin")
} }
if isEventStream(resp) { if isEventStream(resp) {
writeEventStream(stream, resp.Body) writeEventStream(stream, resp.Body)
@ -73,30 +76,43 @@ func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*htt
return resp, nil 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() {} func (hc *HTTPService) Shutdown() {}
// WebsocketService talks to origin using WS/WSS // WebsocketService talks to origin using WS/WSS
type WebsocketService struct { type WebsocketService struct {
tlsConfig *tls.Config tlsConfig *tls.Config
originURL *url.URL
shutdownC chan struct{} shutdownC chan struct{}
} }
func NewWebSocketService(tlsConfig *tls.Config, url string) (OriginService, error) { func NewWebSocketService(tlsConfig *tls.Config, url *url.URL) (OriginService, error) {
listener, err := net.Listen("tcp", "127.0.0.1:") listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Cannot start Websocket Proxy Server") return nil, errors.Wrap(err, "cannot start Websocket Proxy Server")
} }
shutdownC := make(chan struct{}) shutdownC := make(chan struct{})
go func() { go func() {
websocket.StartProxyServer(log.CreateLogger(), listener, url, shutdownC) websocket.StartProxyServer(log.CreateLogger(), listener, url.String(), shutdownC)
}() }()
return &WebsocketService{ return &WebsocketService{
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
originURL: url,
shutdownC: shutdownC, shutdownC: shutdownC,
}, nil }, nil
} }
func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (response *http.Response, err error) { 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) conn, response, err := websocket.ClientConnect(req, wsc.tlsConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -104,7 +120,7 @@ func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request)
defer conn.Close() defer conn.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(response)) err = stream.WriteHeaders(h1ResponseToH2Response(response))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Error writing response header to websocket origin") return nil, errors.Wrap(err, "error writing response header to websocket origin")
} }
// Copy to/from stream to the undelying connection. Use the underlying // Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves // connection because cloudflared doesn't operate on the message themselves
@ -112,6 +128,14 @@ func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request)
return response, nil 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() { func (wsc *WebsocketService) Shutdown() {
close(wsc.shutdownC) close(wsc.shutdownC)
} }
@ -120,21 +144,26 @@ func (wsc *WebsocketService) Shutdown() {
type HelloWorldService struct { type HelloWorldService struct {
client http.RoundTripper client http.RoundTripper
listener net.Listener listener net.Listener
originURL *url.URL
shutdownC chan struct{} shutdownC chan struct{}
} }
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) { func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
listener, err := hello.CreateTLSListener("127.0.0.1:") listener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Cannot start Hello World Server") return nil, errors.Wrap(err, "cannot start Hello World Server")
} }
shutdownC := make(chan struct{}) shutdownC := make(chan struct{})
go func() { go func() {
hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC) hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC)
}() }()
return &HelloWorldService{ return &HelloWorldService{
client: transport, client: transport,
listener: listener, listener: listener,
originURL: &url.URL{
Scheme: "https",
Host: listener.Addr().String(),
},
shutdownC: shutdownC, shutdownC: shutdownC,
}, nil }, nil
} }
@ -142,16 +171,15 @@ func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) { func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Request origin to keep connection alive to improve performance // Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive") req.Header.Set("Connection", "keep-alive")
resp, err := hwc.client.RoundTrip(req) resp, err := hwc.client.RoundTrip(req)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Error proxying request to Hello World origin") return nil, errors.Wrap(err, "error proxying request to Hello World origin")
} }
defer resp.Body.Close() defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp)) err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Error writing response header to Hello World origin") 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 // Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
@ -161,6 +189,14 @@ func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request
return resp, nil 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() { func (hwc *HelloWorldService) Shutdown() {
hwc.listener.Close() hwc.listener.Close()
} }

69
streamhandler/request.go Normal file
View File

@ -0,0 +1,69 @@
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":
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
}

View File

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

View File

@ -0,0 +1,223 @@
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,
Origin: &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,
Origin: &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)
}

179
supervisor/supervisor.go Normal file
View File

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

View File

@ -89,16 +89,35 @@ func LoadOriginCA(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error)
return originCertPool, nil return originCertPool, nil
} }
func LoadCustomCertPool(customCertFilename string) (*x509.CertPool, error) { func LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) {
pool := x509.NewCertPool() // First, obtain the system certificate pool
customCAPoolPEM, err := ioutil.ReadFile(customCertFilename) certPool, err := x509.SystemCertPool()
if err != nil { if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s", customCertFilename)) certPool = x509.NewCertPool()
} }
if !pool.AppendCertsFromPEM(customCAPoolPEM) {
// 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 nil, fmt.Errorf("error appending custom CA to cert pool")
} }
return pool, nil return certPool, nil
} }
func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) { func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) {

View File

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

View File

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

View File

@ -72,6 +72,7 @@ type EdgeConnectionConfig struct {
HeartbeatInterval time.Duration HeartbeatInterval time.Duration
Timeout time.Duration Timeout time.Duration
MaxFailedHeartbeats uint64 MaxFailedHeartbeats uint64
UserCredentialPath string
} }
// FailReason impelents FallibleConfig interface for EdgeConnectionConfig // FailReason impelents FallibleConfig interface for EdgeConnectionConfig
@ -133,58 +134,28 @@ type OriginConfig interface {
} }
type HTTPOriginConfig struct { type HTTPOriginConfig struct {
URL OriginAddr `capnp:"url"` URLString string `capnp:"urlString"`
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"` TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"`
DialDualStack bool DialDualStack bool
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"` TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"`
TLSVerify bool `capnp:"tlsVerify"` TLSVerify bool `capnp:"tlsVerify"`
OriginCAPool string OriginCAPool string
OriginServerName string OriginServerName string
MaxIdleConnections uint64 MaxIdleConnections uint64
IdleConnectionTimeout time.Duration IdleConnectionTimeout time.Duration
ProxyConnectTimeout time.Duration ProxyConnectionTimeout time.Duration
ExpectContinueTimeout time.Duration ExpectContinueTimeout time.Duration
ChunkedEncoding bool ChunkedEncoding bool
}
type OriginAddr interface {
Addr() string
}
type HTTPURL struct {
URL *url.URL
}
func (ha *HTTPURL) Addr() string {
return ha.URL.String()
}
func (ha *HTTPURL) capnpHTTPURL() *CapnpHTTPURL {
return &CapnpHTTPURL{
URL: ha.URL.String(),
}
}
// URL for a HTTP origin, capnp doesn't have native support for URL, so represent it as string
type CapnpHTTPURL struct {
URL string `capnp:"url"`
}
type UnixPath struct {
Path string
}
func (up *UnixPath) Addr() string {
return up.Path
} }
func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) { func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
rootCAs, err := tlsconfig.LoadCustomCertPool(hc.OriginCAPool) rootCAs, err := tlsconfig.LoadCustomOriginCA(hc.OriginCAPool)
if err != nil { if err != nil {
return nil, err return nil, err
} }
dialContext := (&net.Dialer{ dialContext := (&net.Dialer{
Timeout: hc.ProxyConnectTimeout, Timeout: hc.ProxyConnectionTimeout,
KeepAlive: hc.TCPKeepAlive, KeepAlive: hc.TCPKeepAlive,
DualStack: hc.DialDualStack, DualStack: hc.DialDualStack,
}).DialContext }).DialContext
@ -201,25 +172,29 @@ func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
IdleConnTimeout: hc.IdleConnectionTimeout, IdleConnTimeout: hc.IdleConnectionTimeout,
ExpectContinueTimeout: hc.ExpectContinueTimeout, ExpectContinueTimeout: hc.ExpectContinueTimeout,
} }
if unixPath, ok := hc.URL.(*UnixPath); ok { 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) { transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialContext(ctx, "unix", unixPath.Addr()) return dialContext(ctx, "unix", url.Host)
} }
} }
return originservice.NewHTTPService(transport, hc.URL.Addr(), hc.ChunkedEncoding), nil return originservice.NewHTTPService(transport, url, hc.ChunkedEncoding), nil
} }
func (_ *HTTPOriginConfig) isOriginConfig() {} func (_ *HTTPOriginConfig) isOriginConfig() {}
type WebSocketOriginConfig struct { type WebSocketOriginConfig struct {
URL string `capnp:"url"` URLString string `capnp:"urlString"`
TLSVerify bool `capnp:"tlsVerify"` TLSVerify bool `capnp:"tlsVerify"`
OriginCAPool string OriginCAPool string
OriginServerName string OriginServerName string
} }
func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error) { func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error) {
rootCAs, err := tlsconfig.LoadCustomCertPool(wsc.OriginCAPool) rootCAs, err := tlsconfig.LoadCustomOriginCA(wsc.OriginCAPool)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -228,7 +203,12 @@ func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error)
ServerName: wsc.OriginServerName, ServerName: wsc.OriginServerName,
InsecureSkipVerify: wsc.TLSVerify, InsecureSkipVerify: wsc.TLSVerify,
} }
return originservice.NewWebSocketService(tlsConfig, wsc.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() {} func (_ *WebSocketOriginConfig) isOriginConfig() {}
@ -549,115 +529,12 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
} }
func MarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig, p *HTTPOriginConfig) error { func MarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig, p *HTTPOriginConfig) error {
switch originAddr := p.URL.(type) { return pogs.Insert(tunnelrpc.HTTPOriginConfig_TypeID, s.Struct, p)
case *HTTPURL:
ss, err := s.OriginAddr().NewHttp()
if err != nil {
return err
}
if err := MarshalHTTPURL(ss, originAddr); err != nil {
return err
}
case *UnixPath:
ss, err := s.OriginAddr().NewUnix()
if err != nil {
return err
}
if err := MarshalUnixPath(ss, originAddr); err != nil {
return err
}
default:
return fmt.Errorf("Unknown type for OriginAddr: %T", originAddr)
}
s.SetTcpKeepAlive(p.TCPKeepAlive.Nanoseconds())
s.SetDialDualStack(p.DialDualStack)
s.SetTlsHandshakeTimeout(p.TLSHandshakeTimeout.Nanoseconds())
s.SetTlsVerify(p.TLSVerify)
s.SetOriginCAPool(p.OriginCAPool)
s.SetOriginServerName(p.OriginServerName)
s.SetMaxIdleConnections(p.MaxIdleConnections)
s.SetIdleConnectionTimeout(p.IdleConnectionTimeout.Nanoseconds())
s.SetProxyConnectionTimeout(p.ProxyConnectTimeout.Nanoseconds())
s.SetExpectContinueTimeout(p.ExpectContinueTimeout.Nanoseconds())
s.SetChunkedEncoding(p.ChunkedEncoding)
return nil
} }
func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig, error) { func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig, error) {
p := new(HTTPOriginConfig) p := new(HTTPOriginConfig)
switch s.OriginAddr().Which() { err := pogs.Extract(p, tunnelrpc.HTTPOriginConfig_TypeID, s.Struct)
case tunnelrpc.HTTPOriginConfig_originAddr_Which_http:
ss, err := s.OriginAddr().Http()
if err != nil {
return nil, err
}
originAddr, err := UnmarshalCapnpHTTPURL(ss)
if err != nil {
return nil, err
}
p.URL = originAddr
case tunnelrpc.HTTPOriginConfig_originAddr_Which_unix:
ss, err := s.OriginAddr().Unix()
if err != nil {
return nil, err
}
originAddr, err := UnmarshalUnixPath(ss)
if err != nil {
return nil, err
}
p.URL = originAddr
default:
return nil, fmt.Errorf("Unknown type for OriginAddr: %T", s.OriginAddr().Which())
}
p.TCPKeepAlive = time.Duration(s.TcpKeepAlive())
p.DialDualStack = s.DialDualStack()
p.TLSHandshakeTimeout = time.Duration(s.TlsHandshakeTimeout())
p.TLSVerify = s.TlsVerify()
originCAPool, err := s.OriginCAPool()
if err != nil {
return nil, err
}
p.OriginCAPool = originCAPool
originServerName, err := s.OriginServerName()
if err != nil {
return nil, err
}
p.OriginServerName = originServerName
p.MaxIdleConnections = s.MaxIdleConnections()
p.IdleConnectionTimeout = time.Duration(s.IdleConnectionTimeout())
p.ProxyConnectTimeout = time.Duration(s.ProxyConnectionTimeout())
p.ExpectContinueTimeout = time.Duration(s.ExpectContinueTimeout())
p.ChunkedEncoding = s.ChunkedEncoding()
return p, nil
}
func MarshalHTTPURL(s tunnelrpc.CapnpHTTPURL, p *HTTPURL) error {
return pogs.Insert(tunnelrpc.CapnpHTTPURL_TypeID, s.Struct, p.capnpHTTPURL())
}
func UnmarshalCapnpHTTPURL(s tunnelrpc.CapnpHTTPURL) (*HTTPURL, error) {
p := new(CapnpHTTPURL)
err := pogs.Extract(p, tunnelrpc.CapnpHTTPURL_TypeID, s.Struct)
if err != nil {
return nil, err
}
url, err := url.Parse(p.URL)
if err != nil {
return nil, err
}
return &HTTPURL{
URL: url,
}, nil
}
func MarshalUnixPath(s tunnelrpc.UnixPath, p *UnixPath) error {
err := pogs.Insert(tunnelrpc.UnixPath_TypeID, s.Struct, p)
return err
}
func UnmarshalUnixPath(s tunnelrpc.UnixPath) (*UnixPath, error) {
p := new(UnixPath)
err := pogs.Extract(p, tunnelrpc.UnixPath_TypeID, s.Struct)
return p, err return p, err
} }

View File

@ -2,7 +2,6 @@ package pogs
import ( import (
"fmt" "fmt"
"net/url"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -205,6 +204,24 @@ 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 // Functions to generate sample data for ease of testing
@ -260,23 +277,18 @@ func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReversePr
func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig { func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
sample := &HTTPOriginConfig{ sample := &HTTPOriginConfig{
URL: &HTTPURL{ URLString: "https.example.com",
URL: &url.URL{ TCPKeepAlive: 7 * time.Second,
Scheme: "https", DialDualStack: true,
Host: "example.com", TLSHandshakeTimeout: 11 * time.Second,
}, TLSVerify: true,
}, OriginCAPool: "/etc/cert.pem",
TCPKeepAlive: 7 * time.Second, OriginServerName: "secure.example.com",
DialDualStack: true, MaxIdleConnections: 19,
TLSHandshakeTimeout: 11 * time.Second, IdleConnectionTimeout: 17 * time.Second,
TLSVerify: true, ProxyConnectionTimeout: 15 * time.Second,
OriginCAPool: "/etc/cert.pem", ExpectContinueTimeout: 21 * time.Second,
OriginServerName: "secure.example.com", ChunkedEncoding: true,
MaxIdleConnections: 19,
IdleConnectionTimeout: 17 * time.Second,
ProxyConnectTimeout: 15 * time.Second,
ExpectContinueTimeout: 21 * time.Second,
ChunkedEncoding: true,
} }
sample.ensureNoZeroFields() sample.ensureNoZeroFields()
for _, f := range overrides { for _, f := range overrides {
@ -287,20 +299,18 @@ func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginCon
func sampleHTTPOriginUnixPathConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig { func sampleHTTPOriginUnixPathConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
sample := &HTTPOriginConfig{ sample := &HTTPOriginConfig{
URL: &UnixPath{ URLString: "unix:/var/lib/file.sock",
Path: "/var/lib/file.sock", TCPKeepAlive: 7 * time.Second,
}, DialDualStack: true,
TCPKeepAlive: 7 * time.Second, TLSHandshakeTimeout: 11 * time.Second,
DialDualStack: true, TLSVerify: true,
TLSHandshakeTimeout: 11 * time.Second, OriginCAPool: "/etc/cert.pem",
TLSVerify: true, OriginServerName: "secure.example.com",
OriginCAPool: "/etc/cert.pem", MaxIdleConnections: 19,
OriginServerName: "secure.example.com", IdleConnectionTimeout: 17 * time.Second,
MaxIdleConnections: 19, ProxyConnectionTimeout: 15 * time.Second,
IdleConnectionTimeout: 17 * time.Second, ExpectContinueTimeout: 21 * time.Second,
ProxyConnectTimeout: 15 * time.Second, ChunkedEncoding: true,
ExpectContinueTimeout: 21 * time.Second,
ChunkedEncoding: true,
} }
sample.ensureNoZeroFields() sample.ensureNoZeroFields()
for _, f := range overrides { for _, f := range overrides {
@ -311,7 +321,7 @@ func sampleHTTPOriginUnixPathConfig(overrides ...func(*HTTPOriginConfig)) *HTTPO
func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig { func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig {
sample := &WebSocketOriginConfig{ sample := &WebSocketOriginConfig{
URL: "ssh://example.com", URLString: "ssh://example.com",
TLSVerify: true, TLSVerify: true,
OriginCAPool: "/etc/cert.pem", OriginCAPool: "/etc/cert.pem",
OriginServerName: "secure.example.com", OriginServerName: "secure.example.com",

View File

@ -117,6 +117,8 @@ struct EdgeConnectionConfig {
# closing the connection to the edge. # closing the connection to the edge.
# cloudflared CLI option: `heartbeat-count` # cloudflared CLI option: `heartbeat-count`
maxFailedHeartbeats @3 :UInt64; maxFailedHeartbeats @3 :UInt64;
# Absolute path of the file containing certificate and token to connect with the edge
userCredentialPath @4 :Text;
} }
struct ReverseProxyConfig { struct ReverseProxyConfig {
@ -145,7 +147,7 @@ struct WebSocketOriginConfig {
# cloudflared will start a websocket server that forwards data to this URI # cloudflared will start a websocket server that forwards data to this URI
# cloudflared CLI option: `url` # cloudflared CLI option: `url`
# cloudflared logic: https://github.com/cloudflare/cloudflared/blob/2019.3.2/cmd/cloudflared/tunnel/cmd.go#L304 # 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. # Whether cloudflared should verify TLS connections to the origin.
# negation of cloudflared CLI option: `no-tls-verify` # negation of cloudflared CLI option: `no-tls-verify`
tlsVerify @1 :Bool; tlsVerify @1 :Bool;
@ -166,25 +168,22 @@ struct WebSocketOriginConfig {
struct HTTPOriginConfig { struct HTTPOriginConfig {
# HTTP(S) URL of the origin service. # HTTP(S) URL of the origin service.
# cloudflared CLI option: `url` # cloudflared CLI option: `url`
originAddr :union { urlString @0 :Text;
http @0 :CapnpHTTPURL;
unix @1 :UnixPath;
}
# the TCP keep-alive period (in ns) for an active network connection. # the TCP keep-alive period (in ns) for an active network connection.
# Zero means keep-alives are not enabled. # Zero means keep-alives are not enabled.
# cloudflared CLI option: `proxy-tcp-keepalive` # cloudflared CLI option: `proxy-tcp-keepalive`
tcpKeepAlive @2 :Int64; tcpKeepAlive @1 :Int64;
# whether cloudflared should use a "happy eyeballs"-compliant procedure # whether cloudflared should use a "happy eyeballs"-compliant procedure
# to connect to origins that resolve to both IPv4 and IPv6 addresses # to connect to origins that resolve to both IPv4 and IPv6 addresses
# negation of cloudflared CLI option: `proxy-no-happy-eyeballs` # negation of cloudflared CLI option: `proxy-no-happy-eyeballs`
dialDualStack @3 :Bool; dialDualStack @2 :Bool;
# maximum time (in ns) for cloudflared to wait for a TLS handshake # maximum time (in ns) for cloudflared to wait for a TLS handshake
# with the origin. Zero means no timeout. # with the origin. Zero means no timeout.
# cloudflared CLI option: `proxy-tls-timeout` # cloudflared CLI option: `proxy-tls-timeout`
tlsHandshakeTimeout @4 :Int64; tlsHandshakeTimeout @3 :Int64;
# Whether cloudflared should verify TLS connections to the origin. # Whether cloudflared should verify TLS connections to the origin.
# negation of cloudflared CLI option: `no-tls-verify` # negation of cloudflared CLI option: `no-tls-verify`
tlsVerify @5 :Bool; tlsVerify @4 :Bool;
# originCAPool specifies the root CA that cloudflared should use when # originCAPool specifies the root CA that cloudflared should use when
# verifying TLS connections to the origin. # verifying TLS connections to the origin.
# - if tlsVerify is false, originCAPool will be ignored. # - if tlsVerify is false, originCAPool will be ignored.
@ -193,39 +192,29 @@ struct HTTPOriginConfig {
# - if tlsVerify is true and originCAPool is non-empty, cloudflared will # - if tlsVerify is true and originCAPool is non-empty, cloudflared will
# treat it as the filepath to the root CA. # treat it as the filepath to the root CA.
# cloudflared CLI option: `origin-ca-pool` # cloudflared CLI option: `origin-ca-pool`
originCAPool @6 :Text; originCAPool @5 :Text;
# Hostname to use when verifying TLS connections to the origin. # Hostname to use when verifying TLS connections to the origin.
# cloudflared CLI option: `origin-server-name` # cloudflared CLI option: `origin-server-name`
originServerName @7 :Text; originServerName @6 :Text;
# maximum number of idle (keep-alive) connections for cloudflared to # maximum number of idle (keep-alive) connections for cloudflared to
# keep open with the origin. Zero means no limit. # keep open with the origin. Zero means no limit.
# cloudflared CLI option: `proxy-keepalive-connections` # cloudflared CLI option: `proxy-keepalive-connections`
maxIdleConnections @8 :UInt64; maxIdleConnections @7 :UInt64;
# maximum time (in ns) for an idle (keep-alive) connection to remain # maximum time (in ns) for an idle (keep-alive) connection to remain
# idle before closing itself. Zero means no timeout. # idle before closing itself. Zero means no timeout.
# cloudflared CLI option: `proxy-keepalive-timeout` # cloudflared CLI option: `proxy-keepalive-timeout`
idleConnectionTimeout @9 :Int64; idleConnectionTimeout @8 :Int64;
# maximum amount of time a dial will wait for a connect to complete. # maximum amount of time a dial will wait for a connect to complete.
proxyConnectionTimeout @10 :Int64; proxyConnectionTimeout @9 :Int64;
# The amount of time to wait for origin's first response headers after fully # 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. # 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 # Zero means no timeout and causes the body to be sent immediately, without
# waiting for the server to approve. # waiting for the server to approve.
expectContinueTimeout @11 :Int64; expectContinueTimeout @10 :Int64;
# Whether cloudflared should allow chunked transfer encoding to the # Whether cloudflared should allow chunked transfer encoding to the
# origin. (This should be disabled for WSGI origins, for example.) # origin. (This should be disabled for WSGI origins, for example.)
# negation of cloudflared CLI option: `no-chunked-encoding` # negation of cloudflared CLI option: `no-chunked-encoding`
chunkedEncoding @12 :Bool; chunkedEncoding @11 :Bool;
}
# URL for a HTTP origin, capnp doesn't have native support for URL, so represent it as Text
struct CapnpHTTPURL {
url @0: Text;
}
# Path to a unix socket
struct UnixPath {
path @0: Text;
} }
# configuration for cloudflared to provide a DNS over HTTPS proxy server # configuration for cloudflared to provide a DNS over HTTPS proxy server

View File

@ -1078,12 +1078,12 @@ type EdgeConnectionConfig struct{ capnp.Struct }
const EdgeConnectionConfig_TypeID = 0xc744e349009087aa const EdgeConnectionConfig_TypeID = 0xc744e349009087aa
func NewEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) { func NewEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}) st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1})
return EdgeConnectionConfig{st}, err return EdgeConnectionConfig{st}, err
} }
func NewRootEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) { func NewRootEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}) st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1})
return EdgeConnectionConfig{st}, err return EdgeConnectionConfig{st}, err
} }
@ -1129,12 +1129,31 @@ func (s EdgeConnectionConfig) SetMaxFailedHeartbeats(v uint64) {
s.Struct.SetUint64(24, v) s.Struct.SetUint64(24, v)
} }
func (s EdgeConnectionConfig) UserCredentialPath() (string, error) {
p, err := s.Struct.Ptr(0)
return p.Text(), err
}
func (s EdgeConnectionConfig) HasUserCredentialPath() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s EdgeConnectionConfig) UserCredentialPathBytes() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return p.TextBytes(), err
}
func (s EdgeConnectionConfig) SetUserCredentialPath(v string) error {
return s.Struct.SetText(0, v)
}
// EdgeConnectionConfig_List is a list of EdgeConnectionConfig. // EdgeConnectionConfig_List is a list of EdgeConnectionConfig.
type EdgeConnectionConfig_List struct{ capnp.List } type EdgeConnectionConfig_List struct{ capnp.List }
// NewEdgeConnectionConfig creates a new list of EdgeConnectionConfig. // NewEdgeConnectionConfig creates a new list of EdgeConnectionConfig.
func NewEdgeConnectionConfig_List(s *capnp.Segment, sz int32) (EdgeConnectionConfig_List, error) { func NewEdgeConnectionConfig_List(s *capnp.Segment, sz int32) (EdgeConnectionConfig_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}, sz) l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1}, sz)
return EdgeConnectionConfig_List{l}, err return EdgeConnectionConfig_List{l}, err
} }
@ -1432,22 +1451,22 @@ func (s WebSocketOriginConfig) String() string {
return str return str
} }
func (s WebSocketOriginConfig) Url() (string, error) { func (s WebSocketOriginConfig) UrlString() (string, error) {
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return p.Text(), err return p.Text(), err
} }
func (s WebSocketOriginConfig) HasUrl() bool { func (s WebSocketOriginConfig) HasUrlString() bool {
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil return p.IsValid() || err != nil
} }
func (s WebSocketOriginConfig) UrlBytes() ([]byte, error) { func (s WebSocketOriginConfig) UrlStringBytes() ([]byte, error) {
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return p.TextBytes(), err return p.TextBytes(), err
} }
func (s WebSocketOriginConfig) SetUrl(v string) error { func (s WebSocketOriginConfig) SetUrlString(v string) error {
return s.Struct.SetText(0, v) return s.Struct.SetText(0, v)
} }
@ -1528,25 +1547,6 @@ func (p WebSocketOriginConfig_Promise) Struct() (WebSocketOriginConfig, error) {
} }
type HTTPOriginConfig struct{ capnp.Struct } type HTTPOriginConfig struct{ capnp.Struct }
type HTTPOriginConfig_originAddr HTTPOriginConfig
type HTTPOriginConfig_originAddr_Which uint16
const (
HTTPOriginConfig_originAddr_Which_http HTTPOriginConfig_originAddr_Which = 0
HTTPOriginConfig_originAddr_Which_unix HTTPOriginConfig_originAddr_Which = 1
)
func (w HTTPOriginConfig_originAddr_Which) String() string {
const s = "httpunix"
switch w {
case HTTPOriginConfig_originAddr_Which_http:
return s[0:4]
case HTTPOriginConfig_originAddr_Which_unix:
return s[4:8]
}
return "HTTPOriginConfig_originAddr_Which(" + strconv.FormatUint(uint64(w), 10) + ")"
}
// HTTPOriginConfig_TypeID is the unique identifier for the type HTTPOriginConfig. // HTTPOriginConfig_TypeID is the unique identifier for the type HTTPOriginConfig.
const HTTPOriginConfig_TypeID = 0xe4a6a1bc139211b4 const HTTPOriginConfig_TypeID = 0xe4a6a1bc139211b4
@ -1571,93 +1571,39 @@ func (s HTTPOriginConfig) String() string {
return str return str
} }
func (s HTTPOriginConfig) OriginAddr() HTTPOriginConfig_originAddr { func (s HTTPOriginConfig) UrlString() (string, error) {
return HTTPOriginConfig_originAddr(s)
}
func (s HTTPOriginConfig_originAddr) Which() HTTPOriginConfig_originAddr_Which {
return HTTPOriginConfig_originAddr_Which(s.Struct.Uint16(0))
}
func (s HTTPOriginConfig_originAddr) Http() (CapnpHTTPURL, error) {
if s.Struct.Uint16(0) != 0 {
panic("Which() != http")
}
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return CapnpHTTPURL{Struct: p.Struct()}, err return p.Text(), err
} }
func (s HTTPOriginConfig_originAddr) HasHttp() bool { func (s HTTPOriginConfig) HasUrlString() bool {
if s.Struct.Uint16(0) != 0 {
return false
}
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil return p.IsValid() || err != nil
} }
func (s HTTPOriginConfig_originAddr) SetHttp(v CapnpHTTPURL) error { func (s HTTPOriginConfig) UrlStringBytes() ([]byte, error) {
s.Struct.SetUint16(0, 0)
return s.Struct.SetPtr(0, v.Struct.ToPtr())
}
// NewHttp sets the http field to a newly
// allocated CapnpHTTPURL struct, preferring placement in s's segment.
func (s HTTPOriginConfig_originAddr) NewHttp() (CapnpHTTPURL, error) {
s.Struct.SetUint16(0, 0)
ss, err := NewCapnpHTTPURL(s.Struct.Segment())
if err != nil {
return CapnpHTTPURL{}, err
}
err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
return ss, err
}
func (s HTTPOriginConfig_originAddr) Unix() (UnixPath, error) {
if s.Struct.Uint16(0) != 1 {
panic("Which() != unix")
}
p, err := s.Struct.Ptr(0) p, err := s.Struct.Ptr(0)
return UnixPath{Struct: p.Struct()}, err return p.TextBytes(), err
} }
func (s HTTPOriginConfig_originAddr) HasUnix() bool { func (s HTTPOriginConfig) SetUrlString(v string) error {
if s.Struct.Uint16(0) != 1 { return s.Struct.SetText(0, v)
return false
}
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s HTTPOriginConfig_originAddr) SetUnix(v UnixPath) error {
s.Struct.SetUint16(0, 1)
return s.Struct.SetPtr(0, v.Struct.ToPtr())
}
// NewUnix sets the unix field to a newly
// allocated UnixPath struct, preferring placement in s's segment.
func (s HTTPOriginConfig_originAddr) NewUnix() (UnixPath, error) {
s.Struct.SetUint16(0, 1)
ss, err := NewUnixPath(s.Struct.Segment())
if err != nil {
return UnixPath{}, err
}
err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
return ss, err
} }
func (s HTTPOriginConfig) TcpKeepAlive() int64 { func (s HTTPOriginConfig) TcpKeepAlive() int64 {
return int64(s.Struct.Uint64(8)) return int64(s.Struct.Uint64(0))
} }
func (s HTTPOriginConfig) SetTcpKeepAlive(v int64) { func (s HTTPOriginConfig) SetTcpKeepAlive(v int64) {
s.Struct.SetUint64(8, uint64(v)) s.Struct.SetUint64(0, uint64(v))
} }
func (s HTTPOriginConfig) DialDualStack() bool { func (s HTTPOriginConfig) DialDualStack() bool {
return s.Struct.Bit(16) return s.Struct.Bit(64)
} }
func (s HTTPOriginConfig) SetDialDualStack(v bool) { func (s HTTPOriginConfig) SetDialDualStack(v bool) {
s.Struct.SetBit(16, v) s.Struct.SetBit(64, v)
} }
func (s HTTPOriginConfig) TlsHandshakeTimeout() int64 { func (s HTTPOriginConfig) TlsHandshakeTimeout() int64 {
@ -1669,11 +1615,11 @@ func (s HTTPOriginConfig) SetTlsHandshakeTimeout(v int64) {
} }
func (s HTTPOriginConfig) TlsVerify() bool { func (s HTTPOriginConfig) TlsVerify() bool {
return s.Struct.Bit(17) return s.Struct.Bit(65)
} }
func (s HTTPOriginConfig) SetTlsVerify(v bool) { func (s HTTPOriginConfig) SetTlsVerify(v bool) {
s.Struct.SetBit(17, v) s.Struct.SetBit(65, v)
} }
func (s HTTPOriginConfig) OriginCAPool() (string, error) { func (s HTTPOriginConfig) OriginCAPool() (string, error) {
@ -1747,11 +1693,11 @@ func (s HTTPOriginConfig) SetExpectContinueTimeout(v int64) {
} }
func (s HTTPOriginConfig) ChunkedEncoding() bool { func (s HTTPOriginConfig) ChunkedEncoding() bool {
return s.Struct.Bit(18) return s.Struct.Bit(66)
} }
func (s HTTPOriginConfig) SetChunkedEncoding(v bool) { func (s HTTPOriginConfig) SetChunkedEncoding(v bool) {
s.Struct.SetBit(18, v) s.Struct.SetBit(66, v)
} }
// HTTPOriginConfig_List is a list of HTTPOriginConfig. // HTTPOriginConfig_List is a list of HTTPOriginConfig.
@ -1782,166 +1728,6 @@ func (p HTTPOriginConfig_Promise) Struct() (HTTPOriginConfig, error) {
return HTTPOriginConfig{s}, err return HTTPOriginConfig{s}, err
} }
func (p HTTPOriginConfig_Promise) OriginAddr() HTTPOriginConfig_originAddr_Promise {
return HTTPOriginConfig_originAddr_Promise{p.Pipeline}
}
// HTTPOriginConfig_originAddr_Promise is a wrapper for a HTTPOriginConfig_originAddr promised by a client call.
type HTTPOriginConfig_originAddr_Promise struct{ *capnp.Pipeline }
func (p HTTPOriginConfig_originAddr_Promise) Struct() (HTTPOriginConfig_originAddr, error) {
s, err := p.Pipeline.Struct()
return HTTPOriginConfig_originAddr{s}, err
}
func (p HTTPOriginConfig_originAddr_Promise) Http() CapnpHTTPURL_Promise {
return CapnpHTTPURL_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
func (p HTTPOriginConfig_originAddr_Promise) Unix() UnixPath_Promise {
return UnixPath_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
type CapnpHTTPURL struct{ capnp.Struct }
// CapnpHTTPURL_TypeID is the unique identifier for the type CapnpHTTPURL.
const CapnpHTTPURL_TypeID = 0xa160eb416f17c28e
func NewCapnpHTTPURL(s *capnp.Segment) (CapnpHTTPURL, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return CapnpHTTPURL{st}, err
}
func NewRootCapnpHTTPURL(s *capnp.Segment) (CapnpHTTPURL, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return CapnpHTTPURL{st}, err
}
func ReadRootCapnpHTTPURL(msg *capnp.Message) (CapnpHTTPURL, error) {
root, err := msg.RootPtr()
return CapnpHTTPURL{root.Struct()}, err
}
func (s CapnpHTTPURL) String() string {
str, _ := text.Marshal(0xa160eb416f17c28e, s.Struct)
return str
}
func (s CapnpHTTPURL) Url() (string, error) {
p, err := s.Struct.Ptr(0)
return p.Text(), err
}
func (s CapnpHTTPURL) HasUrl() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s CapnpHTTPURL) UrlBytes() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return p.TextBytes(), err
}
func (s CapnpHTTPURL) SetUrl(v string) error {
return s.Struct.SetText(0, v)
}
// CapnpHTTPURL_List is a list of CapnpHTTPURL.
type CapnpHTTPURL_List struct{ capnp.List }
// NewCapnpHTTPURL creates a new list of CapnpHTTPURL.
func NewCapnpHTTPURL_List(s *capnp.Segment, sz int32) (CapnpHTTPURL_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return CapnpHTTPURL_List{l}, err
}
func (s CapnpHTTPURL_List) At(i int) CapnpHTTPURL { return CapnpHTTPURL{s.List.Struct(i)} }
func (s CapnpHTTPURL_List) Set(i int, v CapnpHTTPURL) error { return s.List.SetStruct(i, v.Struct) }
func (s CapnpHTTPURL_List) String() string {
str, _ := text.MarshalList(0xa160eb416f17c28e, s.List)
return str
}
// CapnpHTTPURL_Promise is a wrapper for a CapnpHTTPURL promised by a client call.
type CapnpHTTPURL_Promise struct{ *capnp.Pipeline }
func (p CapnpHTTPURL_Promise) Struct() (CapnpHTTPURL, error) {
s, err := p.Pipeline.Struct()
return CapnpHTTPURL{s}, err
}
type UnixPath struct{ capnp.Struct }
// UnixPath_TypeID is the unique identifier for the type UnixPath.
const UnixPath_TypeID = 0xf7e406af6bd5236c
func NewUnixPath(s *capnp.Segment) (UnixPath, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return UnixPath{st}, err
}
func NewRootUnixPath(s *capnp.Segment) (UnixPath, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return UnixPath{st}, err
}
func ReadRootUnixPath(msg *capnp.Message) (UnixPath, error) {
root, err := msg.RootPtr()
return UnixPath{root.Struct()}, err
}
func (s UnixPath) String() string {
str, _ := text.Marshal(0xf7e406af6bd5236c, s.Struct)
return str
}
func (s UnixPath) Path() (string, error) {
p, err := s.Struct.Ptr(0)
return p.Text(), err
}
func (s UnixPath) HasPath() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s UnixPath) PathBytes() ([]byte, error) {
p, err := s.Struct.Ptr(0)
return p.TextBytes(), err
}
func (s UnixPath) SetPath(v string) error {
return s.Struct.SetText(0, v)
}
// UnixPath_List is a list of UnixPath.
type UnixPath_List struct{ capnp.List }
// NewUnixPath creates a new list of UnixPath.
func NewUnixPath_List(s *capnp.Segment, sz int32) (UnixPath_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return UnixPath_List{l}, err
}
func (s UnixPath_List) At(i int) UnixPath { return UnixPath{s.List.Struct(i)} }
func (s UnixPath_List) Set(i int, v UnixPath) error { return s.List.SetStruct(i, v.Struct) }
func (s UnixPath_List) String() string {
str, _ := text.MarshalList(0xf7e406af6bd5236c, s.List)
return str
}
// UnixPath_Promise is a wrapper for a UnixPath promised by a client call.
type UnixPath_Promise struct{ *capnp.Pipeline }
func (p UnixPath_Promise) Struct() (UnixPath, error) {
s, err := p.Pipeline.Struct()
return UnixPath{s}, err
}
type DoHProxyConfig struct{ capnp.Struct } type DoHProxyConfig struct{ capnp.Struct }
// DoHProxyConfig_TypeID is the unique identifier for the type DoHProxyConfig. // DoHProxyConfig_TypeID is the unique identifier for the type DoHProxyConfig.
@ -3723,227 +3509,218 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio
return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
} }
const schema_db8274f9144abc7e = "x\xda\xacY{p\\\xe5u?\xe7\xde\x95\xaedK" + const schema_db8274f9144abc7e = "x\xda\xacY}\x8c\x1c\xe5y\x7f\x9ey\xf7v|\xe6" +
"\xde\xbd\xbe\x02#\x81f[\x97L\x82\xc1\x14\xe2\xd0\x82" + "\xce{\xe3\xb9\x80}\xd8\xba\xd6\x02%\x10Lq\\Z" +
"\xdaf\xf5\xb0\x1c\xad\xe3\xc7^=\x0c1f\xc6\xd7\xbb" + "\xb8\xb6Y\xdf\x9d\xcf\xb9\xbd\xf8c\xe7\xf6\xce\x80\xb1%" +
"\x9f\xb4\xd7\xbe{\xef\xfa>l\xc95\xb1q\xa1\x80\xca" + "\x8fw\xdf\xdb\x1b{vf=\x1f\xf6\x9d\xe5\xc4`\xd9" +
"\xc3&x\x06;\x90\xdan)\x81\xe2\x82\x09L\xc7\x14" + "\x05\xae\x10l\x82%\xec\x90\x08\xdc\xba|\x08\x1aC@" +
"gB\xfa 4\x93!LC\xa7\xb4\xe9?\x01\xa63" + "\x15\xd4\xa4\xa1jK\xda\xa8\"U\x93\xaai\xf3O\x03" +
"\xb4\x0c\x85$\xc3\xd0\xc1\xdc\xce\xf9\xeesW\x8bl:" + "VU\xd4\x88\x9a\xa4B\xa9\x80\xa9\x9ew>oo9" +
"\xf5\x1f\xd6\xce\xd9\xefq\xbe\xdf9\xe7w\x1e{\x9d\xdf" + "\xdbU\xf8\x03\x8f\x9e}\xde\xf7}>\x7f\xcf\xc7\xddv" +
"1(\\\xdf\xf6J7\x80z\xa2\xad\xdd\xff\xfd\xdak" + "n\xc9\x06i]\xc7_v\x01h\x8fw\xe4\x83\xdfo" +
"\xa7~\xe7\xe8\x8f\xef\x04\xb9O\xf0\xbf\xf9\xd2\xfa\x9e\x8f" + "\xbc}\xf6wN\xfd\xe0\x18(}R\xf0\xd5\x0bc\xbd" +
"\xddC\xff\x06\x80k^m\xdf\x87\xca\xbf\xb7K\x00\xca" + "\xbf\xf2\x8e\xfe\x1b\x00\xae\x1f\xc9\x1fB\xf5\x9e\xbc\x0c\xa0" +
"\x9b\xed\x9b\x01\xfd\x7f\xban\xff\xdb\xdb\x7fy\xe4\x1e\x90" + "N\xe6\xb7\x01\x06\xfft\xdb\xe1ww\xff\xe2\xe4\x83\xa0" +
"\xfb0Y\x99\x91\x00\xd6|\xd0>\x8fJ\xa7$\x81\xe8" + "\xf4a\xca\x99\x93\x01\xd67\xf2s\xa8\x1e\xcf\xcb\xc0\x82" +
"?vk\xcf?\xe2\x89\x8f\x8e\x80\xfc%\x04hC\xfa" + "o\xde\xdb\xfb\xf7\xf8\xd4\x87'A\xf9\x1c\x02t \xfd" +
"\xfa\x9d\xf6%\x02\xa0r\xbe\xbd\x00\xe8\xbfv\xcdK/" + "\xac\xe7\x97J\x80\xeal\xbe\x08\x18\xbc}\xcb\x85\xd7O" +
"\x1e\xfe\xde\xdd\xdf\x06\xf5\x8b\x88\x10\xec\xef\x97\xfe\x07\x01" + "|\xe7\x81o\x80\xf6YD\x08\xcf\x9f\xce\xff/\x02\xaa" +
"\x95\xeb%Z\xf0\xc1\x9f_\x9d9\xfd\xda\xf2\xef\xf0\x05" + "/\x0a\x86K\x7f\xf2\xf9\xdc\x8bo/\xff\x96`\x08\xce" +
"\xfe\xe3\xaf\xdf\xfc\xdc\xe1\xef\xfd\xc6\xbb0%H\x98\x01" + "\xfd\xe3]/\x9f\xf8\xceo\xbc\x07\x93\x92\x8c9\x80\xf5" +
"X\xf3\x0d\xc9\xa6\xb5L\xfa\x0f@\xff\x81\x1f\xae\xb0\x86" + "?\xce;\xc4\xfb\xef\xf9\xff\x00\x0c\x1e\xff\x977\xb66" +
"\xfes\xfb\xc9F\x9d\x82[G;\x06P\x99\xea\xa0\x07" + "N\x9e9\x0b\xcag\xe3\xbb\xde\x94%\x09r\xc1\xed\xff" +
"\xa8\x1dt\xf0\xc3\xffrnS\xed\xc8\xf1S \x7f1" + "zq\xdb\x96\x97\xa7\x9e\x09\x7f\x09\xe5xU~\x99\x8e" +
"\xbaxw\x87 @\xc6\xbf\xe1_\xdf\xd9\xbc\xf1\xb9\xe9" + "\xfe\x8dL\xcf|\xff`\xcf\xc3\x83\xbf\xfb\xe83\xa0\xf5" +
"'\x82o\x82\xed\xac\xe39\xba\xc7\xe3[\x7f\xb47w" + "aF\x9f\x0eq\xc9\x7f\xcas\xa8\xe2\x12\xfa\xfcX\xee" +
"\xdf\xd0\xef>\xf8\x04\xa8}\x98\xbe\x88\x1fr\xacc\x1e" + "G\xc0`\xee\xce7\xb6\xff\xe2\x0f\xdd\xe7A[\x8b\xb9" +
"\x953t\xd1\x9a\xd3\x1dy\x04\xf4\xe7o:\xb7\xe5\x97" + "\xe0\xaf\x1fz\xe7\xc0M\xcfM\xbd%\xa4b\x00\xebo" +
"\x7f\xec<\x05\xeaj\xcc\xf8\x7fw\xef[{\xaezr" + "\xef<KW\x8ft~\x1b0\xe8\xfe\x8b\x9b\xb7>\xfa" +
"\xfa\x15\xfe\x04\x91\xf0\xe8<EG\xff\xba\xf3\x19@\xbf" + "\xee\xe6\x97\xe8\xea\x8cQC!.v\x0e\xa0\xfa?\x9d" +
"\xfboVmz\xf0\xed\x0dg\xe8h\xa1\xf9\x0dG\x97" + "d\xd7K\x82\xfb\x87\xb7l\xff\xeew\xcf\xd7_j\x15" +
"\x0c\xa0\xf2\xf8\x12z\xc3\xc9%\xb4\xfa\xa7\xd7l\xf9\xfe" + "D\"\xee\x93K\xc7P=\xb7\x94\xb8\x9f^J\xdc\x9f" +
"\xf7\x9f\x9d9\xd3\xac\x88@\xab\x87\x96\xaeGej)" + ")\xe1O\xbf\xb7.\xf7\xe7\x91^\x8c\x98&\xafy\x8f" +
"\x7f\xf1RZ}I\x11\x7f\xfe\x83\xeb3\x7f\x1d\xbeK" + "\x1e7\xae!\x86{?z\xf5\xafF\xde\xff\xd1kY" +
"\xa4Em]\xef\xd2\xe5\xbd]\xb4\xe0\xd6O^\xf8\xe1" + "\x07tvI\xe4\x80\xd5]\xa4\xf8\xea\x9f\x0fu[\xef" +
"\xe8\xfb?;\x9b\xb6\xd6\xd9.\x81\xac\xf5\x93.zx" + "\x1f\xfd\xde|?\x867\x8dt\x8d\xa1zO\x97pz" +
"\xff{\xc3\xdd\xe6\xfb\x87~\xd0\x040?\xe9\xd7]\xeb" + "\xd7\xb7\x01?|\xfe\x81\x13\xa5w6\xbe\xa5\xf5a\xae" +
"Q\xe9\xec\xa6\xeb\xda\xba\x9f\x01\xfc\xe8\xa9\xbb\x0f\x17\xdf" + "U\x91K]\x87P\xed\xe8\xa6O\xec\x166J\xac\xd2" +
"Z\xfb\x8a\xda\x87\x99&\x079\xd9\xbd\x0f\x95\x17\xf8\xd2" + "\xc2.4Y\xb7l/\xaa#\xcb\xe8sp\x99`\x1f" +
"3\xddd\xb8\x18\x93\xc6\xc5\xc1;\x1e[\xb6\x13\x953" + "\xbb\xf7\xeb\x8fu\\\xfc\xfa[\xadf\x92\x89\xa7Tp" +
"\xcb8\xa0\xcb8\xa0\xebo\xfd\xd6Cm\xef|\xeb\x95" + "P\xddU\xa0\xcf{\x0a\xcfH\x80A\xdf\xf9\xdf\xfb\xb3" +
"f\x90$Z\xf3B\xd6F\xe5\xd5,}\xfc\xfb\xec\x13" + "\xa1\xdaO~\xd0\"7]\xae\x8e,\xff@\xd5\x96\xd3" +
"\x02\xa0\xdf\xf7\xec\xef\xfd\xd5p\xe5\xcd\x1f7i-\xf0" + "\xd7\x96\xe5\x07\x01\x83\x07>?{h\xeb\x8ds?n" +
"\xfb\x97\x7f\xa8\x9c[N\x9f\xce.\xdf\x0b\xe8\xdf}\xf5" + "\xb5\xa9\x10\xfc\xb9\xe5s\xa8\xbe)\xb8\xdf\x10\xdc\xd2E" +
"\xdc\xbeM_\x98\x7f\xa3\x19Q\x8e\xc5%\xca<*\xab" + "}\xe5}\xff\xfc\xc5\x9ff\xa2h\xad\xfa3\x84\\\xb0" +
"\x15Z}\x95B\xab\x85w\xb4\xde\x83\xff\xfc\xd5\x9f\xa7" + "u\xfb\xbd{;\xbf\xf2\xce;\xd9(\xfaMUX\xfb" +
"|\xe8\x98\xf2\x0b\x84\x8c\xbfi\xcb\xad;;o\x7f\xeb" + "v\x95\x8c\xf9\x8a\xf2\x98z\xe1\xe9?}\x97\x1e\x92[" +
"\xad\xb4\x0f\xdd\xafp\xacO*\x04\xe5\xf3\xf2C\xcaK" + "\xad9\xa9\xee@\xd5P\xe9\x93\xabB\x87$\x9a\xdb\xf9" +
"'\xff\xe2m\xbaHj\xc6\xf2ee+*o\xd0E" + "Z\xbfv\x00\xd5\xfd\xd7\x92\\\x8dkI\xae\xdbw\x0f" +
"k^W\xf8\x1bb\xc7oe\xe93\x97\x0e\xa0\xf2\xf2" + "\xf2\x9dw\xdc\xfd\x1e(}l^n\xbeH\x9co\x10" +
"\xa5\xa4\xd7\xb9KI\xaf\x1b\xb6\x0f\xb1m7\xde\xf2." + "\xe7\xfa\xd7\xae\x95Q5\xae\x93\x01\x82\xaf\xd5w\xfc\xdd" +
"\xc8}bC\x18\x9f\xa7\x95\xdd+hS\xe7\x0a\x09\x95" + "\xa5\xe1\xa7\xff\xbbmDk\xd7\x0d\xa0\xaa\x13\xdf\xfa]" +
"\xb3\xf4\xd1\x7f`f\xeb\xab\x1f\x8c\x9c\xfc\xef\x96\xfe|" + "\xd7\x09\xf3\xaf_\xf7G??\xf5\xc7\xc3\x97\x16\xdc\xfe" +
"r\xc5\x00*g\xf8\x96\xd3+8\xfck\xae\xff\x93\xf7" + "\xdc\x8a!T_[Ar\xbc\xba\xe2K\xea\xc5\x15\xe2" +
"\x8e\xfe\xd9\xc8\x07\x0bN\xff\xf8\xb2aT:{\xb9\x0b" + "\xf2\xafn\xdcv\xe7\x9a7?\xc8Z\xe2\x1fV| " +
"\xf4~M\xb9\xa1\x97\x1f\xfe\xcd\xb5\x9boZ\xf9\xf2\x87" + "Rq\x05Yb\xea\x8e\xff\xfa\xd2\x8d_\xfb\xdb\x0fZ" +
"i$\xfa{?\xe4\x11\xdeKHL\xdf\xf8__\xfb" + "\xdc#\x18q\xe5\xcd\xa8*+\xe9\xc6\xee\x95E\xc0\xf7" +
"\xc2\x03\xff\xf0a\xab\xa8U{W\xa1\xa2\xf1\x13o\xa3" + "7}\xebG}\x85\xbe_\xb6\x13t\xddJ\x8a\x93\x95" +
"\xc5\xef\xaf\xfb\xce\xcf\xfa\xb2}\xbfj\xa5\xe8\x1d\xbd;" + "\"NV\x0aA\xef\xfe\xd9\x99\x83\xc5o\xfc\xf2C\xd2" +
"Q9Jk\xd7\x1c\xe9\xe5\x8a\x1a\xbf\xf5\xc6\xaeg\xda" + "\x8b\xb5 \xcf\xfe\xbe\x1d\xa8\x1e\xef\xa3\x9b\xef\xef\xa3\xf0" +
"\xdf\xfe\xa8\xd5\xc9/\xf7\xf5\xa1\xf2z\x1f\x9d\xfc\x93>" + "\xdf\xfc\xc2O\xbe8}\xea\xfb\xbfj5\x82p\xc8\xda" +
"R\xe3\x96_\x1c\xdf[\xf8\xf6\xaf>\"\x10\xc4&\x87" + "\xeb\x8f\xa2:x=q\xff\xc1\xf5\x84\x1f\x87\xdf?=" +
"}\xafo+*x9->\xdfG\x91\xb2\xe1\xe97" + "\xfa\xe8\xce\x17>\xc9ju\xe3\xaa\xd7\x85\x7fW\x91V" +
"\xbfZ=\xfa\xa3\x8f\x9b\x11\xe3\xd6{\xf2\xf2C\xa8\x9c" + "{O\x1d\xf6F\x9fx$h\x13t\xeb'W\x0d\xa1" +
"\xe3\xab\xcf^N\xee\xfd\x9b[\xfe\xf2\x0f\xff\xf6\x8f\xfe" + "\xcaW\xd1m\xfa\xaa\x83\xb06\xf0|\xcb\xe2\xa6\xd3\xcc" +
"\xf4\x13P\xafF)\xb1\xfc\x94(\xa1@\x0e~\x05\xe7" + "U\x7f+\xfe\xac\xdeZ\xd5\x9bVs`d\xc6p=" +
"\x96\xd3W\x90\xe9\xf6\xbf\x7fl\xec\xc1mO\x7f\x9a\x86" + "\xc3\xaaO\x08z\xb1l\x9bFu\xb6\x8c\xa8u\xa1\x04" +
"\xab\xb3\xffE\x1e\xa4\xfd\xa4\xe7\xce\xa3\xfb\xdd\xb1G\xee" + "\xa0\xac\x1e\x00@T>\xb3\x03\x00%E\x19\x02(\x1a" +
"\xf7[x\xf3\x9a\x9b\xfa\x87Q)\xf6\xd3\xcd\xa3\xfd{" + "u\xcbvxP3\xdc\xaamY\x1cX\xd5;\xb2G" +
"a\xb5\xefz\xa6\xc9\x0c\xbb\x9e)\xffv\xf4\xb1|m" + "7u\xab\xca\x93\x87:\x16>4\xcaM\xd3\xbe\xcbv" +
"Y\xab\x9b\xf5\x81\xd1Y\xddqusf\x92\xcb\x0b%" + "\xcc\xda6\xc7\xa8\x1b\xd6\xb0mM\x19u\x802br" +
"\xcb\xd0\xcbs%D\xb5\x8b\x94\x92\xfb\x07\x00\x10\xe5K" + "L^xl\xd84\xb8\xe5U\xb8s\xc0\xa8\xf2[}" +
"\xb6\x02\xa0 \xcb\xc3\x00\x05}\xc6\xb4l\xe6Wt\xa7" + "\x97\x87\xe7|G\xf7\x0c\xdb\xbaa\x9c\xbb\xbe\xe9\xb9\x00" +
"l\x99&\x03\xb1\xec\x1e\xd8\xa1\x19\x9aYf\xf1Em" + "Z\x8e\xe5\x00r\x08\xa0t\x0f\x00hK\x18j\xbd\x12" +
"\x0b/\x1ac\x86a\xddl\xd9Fe\xb3\xad\xcf\xe8\xe6" + "\x16\x1d\xc1\x80=i\xe6\x01b\x0f\xa4o\xe6\x17\xbe\x19" +
"\x88eN\xeb3\x00%\xc4x\x9b\xb4p\xdb\x88\xa13" + "\xda\x82\xde\xe4\xce\xad\xbe\xe5\xf0\xba\xe1z\xdc\x09\xc97" +
"\xd3\x9d`\xf6\x1e\xbd\xcc\xae\xf5\x1c\x16\xec\xf3l\xcd\xd5" + "\x14\xcb\xba\xa37\xdc\xec\x83g\x00\xb4\x1e\x86\xda*\x09" +
"-\xf3\xcaq\xe6x\x86\xeb\x00\xa8\x191\x03\x90A\x00" + "\x83\xba\xa3Wy\x99;h\xd8\xb5\xad\xbaeW\x18\xaf" +
"\xb9{\x00@\xed\x10Q\xed\x11\xb0`\xf3\x05\x98KB" + "b\x07H\xd8\x91y\xb4\x8d#6\xe9\x86\xc9k\xa1v" +
"\x1a\x10s\x90\xdc\xd9\xbe\xf0\xce\x00\x0b\xba\x93\xd9\xd7z" + "\xb7V\xfb\xc5\xbfZ\x0f\xcbu\x05\x81xD\xdf\x01\xa0" +
"\xa6\xcdft\xc7ev \xbe\xb2P\xd2l\xad\xe6\xa4" + "\xedf\xa8\x99\x12v\xe3'A/\x15)\xc58\x04\xa0" +
"/<\x0e\xa0\xe6DT\xaf\x10\xd0\x9f\xb1\xb52+1" + "M3\xd4<\x09\xbb\xa5\x8f\x83^\xe1\xb5\xfdk\x004" +
"\x1bu\xab\xb2I3\xad\x09\x91\x95\xb1\x0d\x04lK]" + "\x93\xa16#a7\xfb(\xe8\xa5R\xa0\xf8{\x014" +
"\xda\xc2\x10\xeb4\xdd`\x95\xe0u\xd7\x96\xf3\xfc\xaf\x9a" + "\x8f\xa1v\x9f\x84\x81\xeb7\xc9\xa6.0\xdb\xc1\x9e4" +
"\x133]\xbe\xcf/\xd1\xb6\x02\xa8\xdbET\x0d\x01\xbb" + "\x94#\xeb\xf0Z\x9d,mA\x91W\xc9\xd0\xd8\x13#" +
"\xf1S\xbf\x87\x12\xa5\xac\xef\x03P\xab\"\xaa\xae\x80\xdd" + "n\xc8 \xd7\xeci\xecIKDt\xcc\xe1\x07\xb8\xe3" +
"\xc2y\xbf\x87[m\xf7J\x00\xd5\x10Q\x9d\x15\xb0[" + "\xf22\x14\x1c{f\x16{R\xe4m\xb1z\xf7\xd5Z" +
"\xfc\xc4\xef\xa1\x0c#{;\x01TWD\xf5\xa0\x80\xbe" + "=vtrj\xf1\xf3\"4\xab\xde\x0d\xe5\xfe\x05\xce" +
"\xe3\xd5\x09S\x07D\xcb\xc6\\\xe2\xf6!:\xac2C" + "\";v1\xd4VH\x184\xe9W\xeeq`\x8e\x8b" +
"H\x9bP`e\x02\x1as\x11\x91\x07\x0b\xa4\x8aU\xc5" + "=i\xe9m\x91\xb6M8\x0f\xd3\xff\x87\xc3W\xca\xd1" +
"\\\x92y\xc2m6\xdb\xc3l\x87\x95 k[\xb3s" + "-\x8e+\xc2Y\xebM\x1e\xfb\x0a=v\x98\xa1\xf6\xa0" +
"\x98K(\xbd\x09u\xb1\x85\xa5\xe9\xff\xb1\xc2\xe4di" + "\x84\x0ab\xe8\xb3\xe3\x0e\x80v\x8c\xa1vBB\x94B" +
"j|\x03y`\x0a\xe0\x95\x89E%\xcf6\xb0\x0b\x04" + "\x8f=r\x16@;\xc1P{RB\x85I\xa1\xc3N" +
"\xecJ\x1d\xd7\xfdy\x8d\x18\xf9M\xbck\xf1\xfd\xdc\xd3" + "\xdfL\xbd\x10C\xed\xbc\x84J\x8e\xf5R\x9b\xa1\xbcH" +
"\xcb\xee\x95\xa5\xfc\x02\xdb\x93Y\xbaDT/\x13\xd0\xaf" + "\xc1v\x9e\xa1vA\xc2\xc0\x0eS\x89\xe4\xf7\xb0\x1b$" +
"\xd3\xb7\xcce \xda\x0e\xe6\x92\x02\xa1\xe9\xf1m\x9f\xf1" + "\xec\x06\x0c\xaa\xa6\xed\xd7\xa6L\x1d\xfa\x1d^+mL" +
"\xf8\x91\xe0\x96Rx\x8a\xed\xf0\xe8P{\xe2\xcbn\xa7" + "\xe8\x96\xdf(;\xfc\x80\x81\xb6\xef\x0ez\x1eo\xc8M" +
"\xcb\xf6\x8b\xa8\xde#\xa0\x8c\x18\xb8\xc0]6\x80z\xa7" + "\xcf\xc5<H\x98\x07,xz\xdd\xc5e\x80e\x86\xd8" +
"\x88\xeaa\x01Q\x08\x1c\xe0\xfeS\x00\xeaa\x11\xd5G" + "\x93\xd64@\"&w\xa2\xc3k\xdb\xb9\xe3\x1a\xcc\xb6" +
"\x05\x94E!\xb0\xff\xb1U\x00\xea\xc3\"\xaa\xcf\x0a(" + "\xb0\x0b$\xecZ\xdcL\xe3Q@P8D\xb1m;" +
"g\xc4\x1e\xaa\x9c\xe4\xd3\xe4\xbb\xcf\x8a\xa8\xbe$\xa0o" + "\x86\\7,\xad\x8b\xe5V\x05Ad\x93\x11Ru\x03" +
"\x05\x91I\xfa\xbb\xd8\x0d\x02v\x03\xfae\xc3\xf2*\xd3" + "Cm\xb3\x84\xab\xf1\x13\"\x93YJ\xe3\x00\xda(C" +
"\x86\x06y\x9bU\x8akc\xb9\xe9\xd5J6\xdb\xa3\xa3" + "mB\xc2\xd5\xd2\xc7D&\xc3hd\xd62Cm\xa7" +
"\xe59C\xae\xcbjR\xddu\xb0\x1d\x04l\x07\xcc\xba" + "\x84\x85i\xcfkbOZ\xf0\"\xdf\x1d\xe4{\\\xbb" +
"\xda\x8c\x83\xcb\x00K\"b.\xc9\xbd\x80$\x8c\xcfD" + "\xba\x8f\x03R\xf6'\xe8\x1b\xfd:\x1d\xa1\x110\xb3\x86" +
"\x9bU\xb60\xdb\xd1E\xcb\\`\xd4\x160\x8d\x87\xfe" + "=i\x0b\xd9\xe2x\xd6\xc6\xf1\xc2\xe7Eo\xc4ql" +
"E\xde\x15\x86\x8ae\xeb\xd2\x8cn\xaa]b\xe6\x0a\xdf" + "G\x00e\xe2\xed\x91/\xa4J\xc4\xce.\xedH5P" +
"\x0f1\x19\xa5\xa7\x0e\x8a\xa8n\x10\xb0\x1f?%1\xc1" + "\xa4\x0d\xa1Z\xda\x9eT\xfe\xfe\xaa\xee\xbb<\xb1\xa5\xc3" +
"R\x1c\x07P\xc7DT'\x05\xec\x17\xce\x93\x98\x80Q" + "=gvp\xca\x03\xc6\x9d\x046\xdci\xdb7k\xe3" +
"\x09\xd6\x92\x88\xea6\x01\xb3U\xd7\xadc.\xa1\xe7\xd0" + "\x1cd\xcf\x99E\x04\x09qq0\xd9h\x8ffL\x1e" +
"v{\xd9\x0e\xc7*\xefb\x80D&1\xf1\x87\xdfV" + "FeFN\x92i#C\xad\x9c\xca\xb9\x85h\x9b\x19" +
"Cr\x03\xd1\xa8`.\xa9\x8a/\xc2\xeb\xb9\xcd\x0b\xee" + "jw\x93\x9c\x91\xf9'\xc9\xfc\x13\x0c\xb5\xa6\x84\x81I" +
"\xa8m[6\xe7\xdd\xd8\xda\xa3_N\x1e\x11\x19\xbb\xb8" + "\xe9h\x8d\xda\xc0\\/\x117$\x96m\x11\x802H" +
"5y\x81,\x0c\x06\xcfRw$\xfa\xe7\xcb\x9a\xe7\xb0" + "(\x03\x06~\xd3\xf5\x1c\xae7\x00\x93\x88\"\xfeeW" +
"\x18K\x9b\xb9\xf6\xdc\xd0\xb4\x0b\"\xb3c\x16r\xaa\x96" + "\x81\xba-\xd9_\xd6\x0b\"\x8d\xdb\xeb\x90d\xd6\x96\xb1" +
"gT\xc6\x19H\xae=\x87\x08\x02\xe2\xe2\xdc\xb4\xd6\x1a" + "\xac\x12QjM\x0e\xa5\xc6n\x9f0\xd3\xb6\xebYz" +
"KA\x1exeJO\xd2i\xad\x88j)\xd1s#" + "\x83\x03@\xac\xd8\x11\xbbI\xb0G\xa0\x90t\x83-\xb1" +
"\xc96\x88\xa8\xdeBz\x86\xf0O\x11\xfc\x93\"\xaau" + "q\xf5\xc5*,\x1c\xf3J\xd5\xd9L\xe5\xa8F\xa7Q" +
"\x01}\x83\xc2\xd1\x1c\xb3@t\xdcX\xdd@X\xb2\xb8" + "\x1c\x1f\xb6-y\xca\xa8cO\xda>\xb5\x08\xd0\xc6\xef" +
"\x03J \xa0\x04\xe8{u\xc7\xb5\x99V\x03\x8c=\x8a" + "\x83\xbe7\xcd-\xcf\xa8\x8a\x07\x17\xf8}M\x1a\x9f\x89" +
"\xd6/\xfb\x1c$\xde\x14\xfd%-\xcb\xc3\xb8\xf5\x1b\xe2" + "\xcdJ_\xc8\x182\xb6\xd9\x96=\xa9!\xe5}|6" +
"\xc8\xda\xb8>\xfd\x880\xb4\xa6\x86\x13\xb0[\x07L\xd5" + "6K?o\xe8\x86\x99x?\xb2\xe6 \xc8_Ny" +
"r\\S\xab1\x00\x88\x1ev\xc0\xaa\x13\x8b\x12)\xc4" + "\x16\xed6\xa2\xb2\x12\x16\x95bh\x9e\x16\xc8\x9c\x03\xd0" +
"Uk\x93o|\xfe\xdc\x17\xe4\xa1\x86\xccw*\x95\x88" + "\xeec\xa8=\x9c\x11\xf2\xa1\xc7\x00\xb4\x87\x19jOd" +
"\xca\xe1n\xe4\xdbG,S\x9a\xd6g0\x97\x94yM" + "\x84<5\x94\xc5L\x16a&Y\xf4I\x86\xda\xb3\x12" +
"\x0a\xb4\xb0\xfb\x90\xe7V\x99\xe9\xeae~\xe1\x02\xbb\xaf" + "b.\x84\xccs\x04\x99\xcf2\xd4^\x91\x04\x0a\x8e\x0e" +
"L\xfc3\xc6\xac\xf8\xe5\x14\x90\x11f\x1bw$@J" + "\x0e\xdb\x16FB\xb8\x001\x06\x06\xd3\\w\xbc=\\" +
"\xbb\xd8\\\x04K\x9e\xd54=a\xf3\x10\xcd!\x90\xbe" + "G\xafdy\xdc9\xa0\xa3\x19\xe7\xe0\x11\xcfhp\xdb" +
"\x9e\xacY\xb4x\x09\xb3T\x90\xa3\x0a\x01<\xa4d." + "\xf7\x92\x9cl\xe83\xa2dcm4<%\xeb\x9e\x8b" +
"VR\x9b\x07P+\x81\xcf\xc5J\xd6\x1e\x02P\xeb\"" + "\x9d a'\xa5\x80\xcb\x9da\x87\xd7\x90\xbc\xa1\x9be" +
"\xaa\xfbSJ\xce\x0d'\xe9Q\x16\xc5\x80\x1an'D" + "\x9dy\xd3Wb\xa0\xf9xYhc\x9eCiE\xa1" +
"\x0f\x8a\xa8\xde'p\xc6\x1b\x1b\x1a\xb1L\x0c/t\x00" + "\xff\xd2\xf9O9>\x00\x92H]\xd2\xb91\x946\x06" +
"\"\xbe\xf3\xabL\xb3\xdd\x1dLC\xb7h\xba\xcc\xde\xa3" + "\xa2\xa0tP_\xf0X\xda\x01\x88\x82\x92\xa7\x1b\xcf\xa4" +
"\xa1\x11\xc5\xdb\x01W\xaf1\xcbs\xe3\xf8\xabi\xb3<" + "\x06\x8fD\x1b\xb5\xa1\x18\xa6D,s1t\xf5\x11\x82" +
"\xdbce,\xd8%i\xae\x83\x9d `\xe7\xe2\xefm" + "'\x83\xa7zFu\xd6@\xdb\x9a\x10\x06\xc2\xd4BU" +
"\xa4\xbfl\xf4\xdaT\x82\xd8\x97$\x08\xfa\x974\x9d\xf2" + "\xbb\xd1t\xb8\xeb\xa2a[\x9a\xaf\x9b\x06\xf3f\x93\x83" +
"]\x03 \xf0H$\xd6\xaf\x0d'e\x03\xcf\x0fmT" + "\x8b\xda\x80r?\xcc\x99m\xcd~\xe1$2\xc2m\xb1" +
"5<\x94\x02\x80\xf2C;\x9dx<\x05@\xa0\xcf\x98" + "\x11\xd4A\x1c\x03\xa8l@\x86\x95\xcd\x98\x86\x89Z\xc2" +
"\x05\x85\xc0\xc3#\x1b\x15\x02\xcb\x1d \xb6\xd1Y\xf2\x94" + "!\x80\xcaF\xa2\x971\x8d\x14u\x0b\xf6\x01TF\x89" +
"0m\xeah\x99\x93\x1c\x03L@([\xb5\xba\xcd\x1c" + ">\x81\x12b\x18+\xaa\x86\xcf\x03T&\x88\xbc\x1b\xd3" +
"\x07u\xcbT=\xcd\xd0Ew\xee\xe20\xa0P\x0eB" + "\x12\xab\xee\x12\xd7\xef$\xfa4\xd1;r\xc2|*\xc7" +
"`s=\xcf\xed@ \\\x17\x81\xa0\x0c\xe1z\x80\x89" + "\x9b\x01*\xbb\x89~\x98\xe8yIXP\x9d\xc5\xbd\x00" +
"A\x14qb\x03&VW\x8a8\x0c0\xb1\x96\xe4%" + "\x95\x19\xa2\x1f#\xba\xdc\xd1\x8b\xa2\xf1G\x07\xa0r\x1f" +
"L\x0c\xafl\xc4>\x80\x891\x92O\xa2\x80\x18\x98^" + "\xd1\x1f&\xfa\x92\x15\xbd\xb8\x04@}H\xd0\x1f$\xfa" +
"Q\xf1)\x80\x89I\x12o\xc7$c*\xb7\xf1\xe3\xb7" + "\xe3D\xef\\\xd9\x8b\x9d\x00\xeaI<\x0aP9A\xf4" +
"\x91\xbcJ\xf2\xb6\x0c\x87Oa\xb8\x0a`b;\xc9\xf7" + "'\x89\xbe\x14{q)\x80z\x1a\xcf\x00T\x9e$\xfa" +
"\x93\xbc]\xe0\x08*s\xb8\x13`b\x96\xe4w\x92\\" + "\xb3D\xbf&\xdf\x8b\xd7\x00\xa8\xe7\x84<O\x11\xfd\x05" +
"j\xeb\xa1r^\xb9\x03m\x80\x89\x83$\xbf\x8f\xe4\x1d" + "L\x00\xa4T\xcb\xe2\x18\x85\x93\x91\xd6jf\xbbI\x18" +
"\x97\xf5`\x07\x80r/\x97\xdfC\xf2\x87I\xde\xd9\xdb" + "\xf2\xa8\xf5\xc7\x10d\xcbv\x81z\x7f,\xa4\xab\x17@" +
"\x83\x9d\x00\xca\x11<\x040q\x98\xe4\x8f\x92|\x09\xf6" + ",\x00\x06M\xdb6\xb7\xce\xc7\xc7\xcb\xb5\x0bQX@" +
"\xe0\x12\x00\xe5\x18\x1e\x07\x98x\x94\xe4\xdf%\xf9\xd2\xf6" + "\xc1\xb6J\xb5$\xbf\xc2 \xdalC\x7fU7K\xcd" +
"\x1e\\\x0a\xa0<\xce\xf59A\xf2\xa71\xe6\x83b%" + "D\x12\xc3\x1d\xf4=\xdboB\x7fM\xf7x-\xa9p" +
"MK\xe4Nz\x92zE\xcb\x89\xc3\x8e\x85\x8d\x01\x06" + "\x8eomr\xec\xc6\x04r\xa7aX\xba\x09\xc9/\x8b" +
"\x9cY\xb2\xb2\xd4\x19`6\x19\x0e\x01b\x16\xd0\xaf[" + "\xc5V\xc1\xf7\x8d\xda\x82d\x93Z\x03\xad\xbf90\xa1" +
"\x96\xb1\xa9\x91\xee.\x94\xfdC\xb7\x80\xace\x16+q" + "\x8b\xecZ\x92d\xd7M\xd4\x86\xdc\xc0P\xbb-\x03>" +
"\x08\x05N\xb4\xc1\x82|Y3\x8a\xf5X\x13\xdd\x19\xf2" + "k\x09!?\xc7P\xfbm\x09\x0b\xd9\xa4\xe8?\xa0\x9b" +
"\\\xcb\xabC\xbe\xa2\xb9\xac\x12',\xdb3\xd7\xd9V" + ">\xbf\x926h\xb2\xa5\x14\x84\xddl\x88\xcf\x99\xd7\x87" +
"m\x12\x99]\xd3M\xcd\x80\xf8\x9b\xc5|+\xebyz" + "\xd2\xd7\x93\xc7\xa9Y\xbc\x85\xa16*\xe1\x11\xd7\xafV" +
"e\x01\xb9\x08\xcd\x8e\x96\xaf\x0fLj<\xba:\xe2\xe8" + "I\xe9\xd8\x0aS\xd1\xcc\x00\xfdtw\xc6\x1f\xc98\x1f" +
"\xba\x8a\xaa\x8a+ET\xafKq\xc9j\"\xbc/\x89" + "\xf9\xe3J\xcbn\x9d{\xe1W\xc9\x9a\xb2\xa9^\xc9z" +
"\xa8~E\xc0l:(\xf2{4\xc3c\x17S\xd5L" + "\xc3\xfd\x7f\x9e\x1e\xe7n\x81Z\xf6\xcbNf\xc9\x80~" +
"51{P\x9c\x06t\x9b\xba}8\xb9=\xbe\x9cj" + "\xf9\xfa6:1QN\xc7G\x16\x82c\x16\x17\xc6\xb3" +
"\xbfkDT\xc7\x04<\xe0x\xe52=:Ba:" + "\xb8\x90\xc2\xc2\xdel\xfa\xc7m\x98\xaa\x89<,\x13}" +
"\xec( Og\xa7\xec\x11O\x11B{\\l\x16\x9d" + "'\xa6}\xb7z\x0f\x9e\x9d\x97\xff\xb9\xc1\x10\x17\xb8\xb8" +
"an\xf0\xa9hN[\x94~$\xad\xe6\xfc\x1fw\x8f" + "\xbeF\xf4\xa6\xc0\x05\x0cq\xa1!\xee7\x89>\x93\xc5" +
"3'K\x15\xf8\x05\xfb\xb6x.p\xe1t569" + "\x05\x1f\xe7\xe6\xe3\x02\x8bq\x81\xf2\xf9\x18\xd1O\x08\\" +
"YJ\x9aK1 G\xce\x0b\x98\xea\xbe\x95!\xdc\x0a" + "\xc8\x85\xb8\xf0\x08\xbe</\xff;;B\\8\x8d\xaf" +
"\x02\xb7\x1fE\xffj\x1e\x9e\xd7P\x98\xdc\xc8Y!\x17" + "\xcf\xcb\xff\xa5\xf9\x10\x17\xce\x09\xfeg\x89\xfe\x8a\xc0\x85" +
"\x84\xff\x0d<\x0c\xbfB\xf2A\x0cY\x92\xc2\xff\x0f\xf0" + "\xa1\x10\x17^\x128r\x9e\xe8\x17\x08\x17|\xc7\xacx" +
"T\x03\xbbd\xe4 \xfc\x8b8\x9ef\x11\xb9\x0d\x83\xf0" + "\x8ea\x01\xd6\xd3`\xad6\xbf\xccys\x10\x0a\xa6q" +
"W\xf9\xf9%\x92o\x8bh\x81\xc2\xff\x1b8\xdf@#" + "\x80'\x98]3ts\xa3\xaf\x9b\xd0_\xf1\xf4\xea\xbe" +
"\x92\x18\x84?\xe3\xe1\\%\xb9\xcbi!\x13\x84\xffn" + "\xb4\xd74\xddQ\xdd\xaa\xb98\xad\xef\xe3\x84\xf4r\xb6" +
"|\x0e`\xc2%\xf9AN\x0bmA\xf8\xdf\x8e/6" + "\x16z\xa6\xbb\x9d;\xc6\x14`\xda\x9d&\xbdA\xa1l" +
"\xd0\xc8\x920\xfc\xef\xe5\xeb\xef#\xf9#\x9c\x16\x96\xf7" + "\xdb\xad-\x83hr\xb8\x13\x82J\xf2[C\x9f)\xd5" +
"`\x17\x80r\x94\xd3\xc8\xc3$?\x81q\x093T\x01" + "L>\x8cq\x87\xc0\xac\xb4\xd2\x18\xf4\x8bmY\x18\x96" +
"\xb1b\xfbn\xb9\xfeu\xc6\xeaC\x905\xf4=,\xe6" + "\xed\x09\xa3\x7f~=nF\xfdn\\\xd7'\x8a-\x05" +
"\xea\x8a\xae\x19k=\xcd\x80\xfc\x84\xab\x95w%%\xa3" + "\x9b\xcf4y\xd5\x1b\xb6\xd1\xf2\x0c\xcb\xe7\x0b.\xa8N" +
"\xe1\x8cif\xc5\xc1\xaa\xb6\x8b\x11\xc3K\xe94\xe7\x1a" + "\xfb\xd6>^\x1bA\xabj\xd7\x0c\xab\x0e\x0b\x1am\xf6" +
"\xce\x16f\xeb\xd3\x80I\x91\x19\xa7\xf8l\xc9\xb2\x9a3" + "iS{\xa6\x91\x11\xd9\x8c\x99e\xb1r\x13\x95e\x8c" +
"?\xafU\x98\x1d\x90I\xfc]M\x9b-V\x0c6\x82" + "\xca\xb22\x90N\x9f\xc5\xaa8Ut\xb8\xee\xb6\x99\xa6" +
"Q\xa2\x17\xcd$\xc3\xe8\xf4\x8de\x9a\x18d\xe4I=" + "\xd8\xa7e[1L2z\xad\x87u\x00$\xabW\x8c" +
"\xdf\x98j\xeba\xd9\x1a\xa5\xec\xc9BS.f\xb3u" + "wa\xca\xfeC )\x86\x8c\xe9\x0a\x11\xe3\x8d\xa1\xb2" +
"VvG,4]\xdd\xf4\xd8\x82\x03\xcaU\xcf\xdc\xc5" + "\xcb\x01I\x99\x94QJ6\xde\x18o\xab\x95\xd2\x1cH" +
"*\xa3h\x96\xad\x8an\xce\xc0\x82zY\xfc\xac^>" + "\xca\x88\x8c,YSc\xbc\x89R\xee\x1c\x02IY+" +
"U\x8ft\x84N\x18\x8f\xb1\xe5\xab\x06B\x1f\xa4t," + "\x07qk\x0e\xc5P\x9c\x0d\x18\xc4\x89\x0f\xfd\"\xf57" +
"\x0f$Md\xa1\xccw\x15l\xa69-\x9a\"\xf1\xb3" + "`\x10\xcf\xef\x18\xb7\xf0\x00\x1b\xf0HT\x166`v" +
"\xa2\xac\x10\x04WP\xfd\xb4\x01\xc4s^\x8cFo\xf2" + "\xe9\xc3>\xad\x8fn\xdf\x1e\x12F\xce0\xd4\x8e\xa5\x18" +
"\xee} \xc8\xba\x84\xc9\xc4\x12\xa3\x01\xa5|\x9b\x0d\x82" + "y\xff\\:P'\xb3\xcb#\xcf\xb7\x9b\xa8\x8f\x02h" +
"<%\xa1\x10\xcf\xe21\x1a\x8d\xcb\xc5y\x10\xe4Q\x09" + "O\x84\x9d`2Q\xbfD-\xe3+\x0c\xb5\x1fJi" +
"\xc5x&\x8e\xd1|J\xbei\x18\x04y\xb5\xe4G\x15" + "\xbd\x8c\xc3.\xde\x93\xa0\xed\xc4\xc3\xd4\"\xeb\x92(8" +
"6\x14\x02u\x06\xd1\x8f\x02\x1e\xf2<\xe4\x07\xd1\x8f\xda" + "\xa3\xce\xadui\x12\xd4\xeci\xd1\xd9ax\x95\x0b)" +
"p\x8c*q\x80A<\x10\xa6\x83AL\x8f\x82\xc4\xcf" + "bg7)\xcb2\x9b\x14\x8c\xc78y\x1e\xc0g\xf7" +
"*\x87S\xa8\xa6\xea\x1e\xe2\xc6Y\x11\xd5;\x13n\xbc" + "*\xcb\x16\xc7\xccyC\x89\xa889\x115\xf1>\x1e" +
"c>\xe9\x8b\xe3\x16\xe4\xfe\xa7Z5\xc6\x87\x00\xd4G" + "\xe3\xbf\x8c(\x0ay\xbf[\x0e\xe2\xc1\x05\xe3rE\xce" +
"DT\x9fO5\xc6g\xa8\xf2{^D\xf5\xa7B\x92" + "\xcb\xba\xec*\xa7\xb7q\xde\xef^I%\x88\xb7\xaf\x97" +
"'#\xb7\x8b\xa6'h\xd9QO\xb4\xc8\x10%t\xce" + "\x1f\xc2\xc3w\x0a\x14l\xa1B\xc9\xbd{3\xdb\x1d\xd3" +
"\xb0bk\x1e\xa5\xf8\x15\xab\xca+:\x0c\x8er a" + "\x8e\xe6\xa0\xc2\xd6L\xd5^\xccV\xa1\xc0q\x03Z\xa0" +
"\xea\xf4|eYj\xbe\x82Q7&5\x10{z\xda" + "\xc3-\xe1\xb7&\x0d\xbf\xa4A\xb8\x7fMf\xcb\x13O" +
"\xb2lq\xael\xe8- \x18\xb7\x90\xd7D\xc3\x7f\x8c" + "'\xc7\xc7\xa2\xa0|*i8\x95oR\xa0>\xc5P" +
"~\xb3\x91e\xb2~\xb7\xe4G\xfd\x07Fi\x8a\x8c\x97" + "{!\x13~\xcf\x8d\xa5\xd3\x89\xcc\x1d'\x96S\xf6\x9d" +
"6\xd9\xe7l\xc2\xc6Y\xde\xb9\x98\x0c\x10\x0d{/\xdc" + "\x146M\xbb\xbe\xd9\xb0\xb8K-X\xcbH\xdd\xe4N" +
"K\x07\xf7d\xc9\xd9\x9a\xe6G;SC\x1a\xc3\x0a\xdb" + "C\xb7\xb8\x85\x1e\x81\x91\xef\x10\xa2\xceG\xae\xd2\xc6L" +
"\x99\xec\xa6T\xb6^\x0c\xab@\xe1\xa8\xf0\xcc\xd2\xe6&" + "\xe7\xb6\x98\xfa\x95(\xd8\xc3X\x8f\xcakf~<\x9b" +
"\xf7[\x99\xb8_\\\x18\xdc\xb125\xac\x89\x9a\x8c\xbb" + "\xd9e\xc4\xcak\xafG;\x82\xdd\x19\xe5w\xd1\xfc\xb8" +
"\xd6\x87Ny\".4\xe5\xc7\xc8QO\x88\xa8>\x9d" + "\x93\xa16-a\xa0\xfb\x9e=\xd9\xac\xe9\xe8\xf1M\x0e" +
"r\xbf'i\xe1w\x03\x9f\x94\x98mGz6\x8c\xbf" + "\xdf\xefs\xd9\xaa\xce\xa6s\x14M\x14Uw\x12\x9b\xd4" +
"\x0ckf\x83n2\x87J\xaf\xa6\xce\xb8\xce\xec\x9af" + "\xfbmrxq\xbf\xcf\xb3\x0c\xf1B\x15d\xc3\xae-" +
"2\x13]\"#\xcf&Fmd\xae\xe2\xdaT\xc5\xb6" + "\xd8\xa4\xb6i\xb6\xee\xe2{*vu\x1f\xf7\xe6-\x9a" +
"\x18\xacS\xa6>[\xd2D\xb7\xda\x04\xea\xaa\xc4X\xd9" + "C\xb8\x8cU\xd1\xc7\xd3mj\xac\x891\x9e\x19\x99b" +
"\xba\xe6V/\x06\xca\x890p\x82\xb8\x09St\xaa\xa5" + "\x18\xd9O\x01\xd5d\xa8\x1d\xce\xc0\xc8\xec\\\xea\xf0\xf6" +
"<\x95\x1aoD@\xaa/\x86c\x83\xed) o\xa3" + "\xd5\xf5\xd7S\x10\x17Q\xb2\xed\xb6s\xbc\xc8\xaf(\xd1" +
"\x96r\x9b\x88jU@_\xf3\\k\xaa^\xd1\xd0e" + "\xd2\xbf\x1e\\\xbe\xe5\x8a\xc6\xf5\xa8cmiX\xd7\xb4" +
"\xebl\xb6\xdbc\x92Y\x9eK\xda-\xeaJ\xca\xce\x14" + "k\x97wD\x1d\xeb\x1dQ<\xf7\xa4\x7fw\x8c\x9es" +
"\xd6\xa9~\\g\xb3\xc2n\x8f\xa5\x17D#[\x90t" + "\xa3.\x11\xd8\x94\xbd\xb0\x01\xfc\xbf\x00\x00\x00\xff\xff\xde" +
"\xab\xb2`V\xdb\xa2`\xbb\x99\xed\x98\xb0\xca\xbb\x98\xdb" + "T\x04\xc0"
"0\xcanj<W&\x0aF/a\xe3\xa9f4\xa2" +
"\xa4\xda\xcedX\x1bS\x927\x9fxT\xe3\xc4\xf3\xff" +
"'\xa9.6\xa8o\xa8\xa2\x82\x09\xdb\x8c\x9e7\x87*" +
"\x15\x9b\x12Y4\x90NW\xc3\xc9@z\xf5\xaaT9" +
"\x1c\xce\xd2\xe2\x1fd\x83\x10\xcez\xa6>\x8b\xb9\xe4w" +
"\x99\x0b\x8fG[\x0ec\xc7\x0b\xec\xa2\x08$\xf9\xad\xe4" +
"\xc2%d8M\x08+\xf0\xa6\x02|e\xab\xf2\x7fk" +
"X\x81\xdf\x18\xc6i.\xf9\xf16\xbc\xce\x09\xab^\x10" +
"\xa7\xad\x85\x05\xed\xff\x06\x00\x00\xff\xff\xa8\xfcvR"
func init() { func init() {
schemas.Register(schema_db8274f9144abc7e, schemas.Register(schema_db8274f9144abc7e,
@ -3952,7 +3729,6 @@ func init() {
0x91f7a001ca145b9d, 0x91f7a001ca145b9d,
0x9b87b390babc2ccf, 0x9b87b390babc2ccf,
0x9e12cfad042ba4f1, 0x9e12cfad042ba4f1,
0xa160eb416f17c28e,
0xa29a916d4ebdd894, 0xa29a916d4ebdd894,
0xa766b24d4fe5da35, 0xa766b24d4fe5da35,
0xa78f37418c1077c8, 0xa78f37418c1077c8,
@ -3977,10 +3753,8 @@ func init() {
0xf2c122394f447e8e, 0xf2c122394f447e8e,
0xf2c68e2547ec3866, 0xf2c68e2547ec3866,
0xf41a0f001ad49e46, 0xf41a0f001ad49e46,
0xf7e406af6bd5236c,
0xf7f49b3f779ae258, 0xf7f49b3f779ae258,
0xf9c895683ed9ac4c, 0xf9c895683ed9ac4c,
0xfc9f83c37bab5621,
0xfeac5c8f4899ef7c, 0xfeac5c8f4899ef7c,
0xff8d9848747c956a) 0xff8d9848747c956a)
} }

View File

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

View File

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

View File

@ -0,0 +1 @@
module github.com/konsorten/go-windows-terminal-sequences

View File

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

View File

@ -0,0 +1,11 @@
// +build linux darwin
package sequences
import (
"fmt"
)
func EnableVirtualTerminalProcessing(stream uintptr, enable bool) error {
return fmt.Errorf("windows only package")
}

View File

@ -1 +1,2 @@
logrus logrus
vendor

View File

@ -1,15 +1,25 @@
language: go language: go
go: go_import_path: github.com/sirupsen/logrus
- 1.6.x git:
- 1.7.x depth: 1
- 1.8.x
- tip
env: env:
- GOMAXPROCS=4 GORACE=halt_on_error=1 - GO111MODULE=on
- GO111MODULE=off
go: [ 1.11.x, 1.12.x ]
os: [ linux, osx ]
matrix:
exclude:
- go: 1.12.x
env: GO111MODULE=off
- go: 1.11.x
os: osx
install: install:
- go get github.com/stretchr/testify/assert - ./travis/install.sh
- go get gopkg.in/gemnasium/logrus-airbrake-hook.v2 - if [[ "$GO111MODULE" == "on" ]]; then go mod download; fi
- go get golang.org/x/sys/unix - if [[ "$GO111MODULE" == "off" ]]; then go get github.com/stretchr/testify/assert golang.org/x/sys/unix github.com/konsorten/go-windows-terminal-sequences; fi
- go get golang.org/x/sys/windows
script: script:
- ./travis/cross_build.sh
- export GOMAXPROCS=4
- export GORACE=halt_on_error=1
- go test -race -v ./... - go test -race -v ./...
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then go test -race -v -tags appengine ./... ; fi

View File

@ -1,3 +1,90 @@
# 1.4.2
* Fixes build break for plan9, nacl, solaris
# 1.4.1
This new release introduces:
* Enhance TextFormatter to not print caller information when they are empty (#944)
* Remove dependency on golang.org/x/crypto (#932, #943)
Fixes:
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
# 1.4.0
This new release introduces:
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter (#909, #911)
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
Fixes:
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
* Fix infinite recursion on unknown `Level.String()` (#907)
* Fix race condition in `getCaller` (#916).
# 1.3.0
This new release introduces:
* Log, Logf, Logln functions for Logger and Entry that take a Level
Fixes:
* Building prometheus node_exporter on AIX (#840)
* Race condition in TextFormatter (#468)
* Travis CI import path (#868)
* Remove coloured output on Windows (#862)
* Pointer to func as field in JSONFormatter (#870)
* Properly marshal Levels (#873)
# 1.2.0
This new release introduces:
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
* A new trace level named `Trace` whose level is below `Debug`
* A configurable exit function to be called upon a Fatal trace
* The `Level` object now implements `encoding.TextUnmarshaler` interface
# 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
* the extra new line at the end of message in text formatter has been removed
* a new global public API to check if a level is activated: IsLevelEnabled
* the following methods have been added to the Logger object
* IsLevelEnabled
* SetFormatter
* SetOutput
* ReplaceHooks
* introduction of go module
* an indent configuration for the json formatter
* output colour support for windows
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
a new configuration field of the json formatter in order to put all the fields
in a nested dictionnary
* a new SetOutput method in the Logger
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3 # 1.0.3
* Replace example files with testable examples * Replace example files with testable examples

View File

@ -56,8 +56,39 @@ time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4 time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009 time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
exit status 1
``` ```
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
```go
log.SetFormatter(&log.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
```
#### Logging Method Name
If you wish to add the calling method as a field, instruct the logger via:
```go
log.SetReportCaller(true)
```
This adds the caller as 'method' like so:
```json
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
```
```text
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
```
Note that this does add measurable overhead - the cost will depend on the version of Go, but is
between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
environment via benchmarks:
```
go test -bench=.*CallerTracing
```
#### Case-sensitivity #### Case-sensitivity
@ -220,7 +251,7 @@ Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
```go ```go
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "aibrake" "gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
logrus_syslog "github.com/sirupsen/logrus/hooks/syslog" logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog" "log/syslog"
) )
@ -241,60 +272,15 @@ func init() {
``` ```
Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md). Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
| Hook | Description | A list of currently known of service hook can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
| ----- | ----------- |
| [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. |
| [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Amazon Kinesis](https://github.com/evalphobia/logrus_kinesis) | Hook for logging to [Amazon Kinesis](https://aws.amazon.com/kinesis/) |
| [Amqp-Hook](https://github.com/vladoatanasov/logrus_amqp) | Hook for logging to Amqp broker (Like RabbitMQ) |
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [DeferPanic](https://github.com/deferpanic/dp-logrus) | Hook for logging to DeferPanic |
| [Discordrus](https://github.com/kz/discordrus) | Hook for logging to [Discord](https://discordapp.com/) |
| [ElasticSearch](https://github.com/sohlich/elogrus) | Hook for logging to ElasticSearch|
| [Firehose](https://github.com/beaubrewer/logrus_firehose) | Hook for logging to [Amazon Firehose](https://aws.amazon.com/kinesis/firehose/)
| [Fluentd](https://github.com/evalphobia/logrus_fluent) | Hook for logging to fluentd |
| [Go-Slack](https://github.com/multiplay/go-slack) | Hook for logging to [Slack](https://slack.com) |
| [Graylog](https://github.com/gemnasium/logrus-graylog-hook) | Hook for logging to [Graylog](http://graylog2.org/) |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
| [Honeybadger](https://github.com/agonzalezro/logrus_honeybadger) | Hook for sending exceptions to Honeybadger |
| [InfluxDB](https://github.com/Abramovic/logrus_influxdb) | Hook for logging to influxdb |
| [Influxus](http://github.com/vlad-doru/influxus) | Hook for concurrently logging to [InfluxDB](http://influxdata.com/) |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [KafkaLogrus](https://github.com/goibibo/KafkaLogrus) | Hook for logging to kafka |
| [LFShook](https://github.com/rifflock/lfshook) | Hook for logging to the local filesystem |
| [Logentries](https://github.com/jcftang/logentriesrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logentrus](https://github.com/puddingfactory/logentrus) | Hook for logging to [Logentries](https://logentries.com/) |
| [Logmatic.io](https://github.com/logmatic/logmatic-go) | Hook for logging to [Logmatic.io](http://logmatic.io/) |
| [Logrusly](https://github.com/sebest/logrusly) | Send logs to [Loggly](https://www.loggly.com/) |
| [Logstash](https://github.com/bshuster-repo/logrus-logstash-hook) | Hook for logging to [Logstash](https://www.elastic.co/products/logstash) |
| [Mail](https://github.com/zbindenren/logrus_mail) | Hook for sending exceptions via mail |
| [Mattermost](https://github.com/shuLhan/mattermost-integration/tree/master/hooks/logrus) | Hook for logging to [Mattermost](https://mattermost.com/) |
| [Mongodb](https://github.com/weekface/mgorus) | Hook for logging to mongodb |
| [NATS-Hook](https://github.com/rybit/nats_logrus_hook) | Hook for logging to [NATS](https://nats.io) |
| [Octokit](https://github.com/dorajistyle/logrus-octokit-hook) | Hook for logging to github via octokit |
| [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. |
| [PostgreSQL](https://github.com/gemnasium/logrus-postgresql-hook) | Send logs to [PostgreSQL](http://postgresql.org) |
| [Pushover](https://github.com/toorop/logrus_pushover) | Send error via [Pushover](https://pushover.net) |
| [Raygun](https://github.com/squirkle/logrus-raygun-hook) | Hook for logging to [Raygun.io](http://raygun.io/) |
| [Redis-Hook](https://github.com/rogierlommers/logrus-redis-hook) | Hook for logging to a ELK stack (through Redis) |
| [Rollrus](https://github.com/heroku/rollrus) | Hook for sending errors to rollbar |
| [Scribe](https://github.com/sagar8192/logrus-scribe-hook) | Hook for logging to [Scribe](https://github.com/facebookarchive/scribe)|
| [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. |
| [Slackrus](https://github.com/johntdyer/slackrus) | Hook for Slack chat. |
| [Stackdriver](https://github.com/knq/sdhook) | Hook for logging to [Google Stackdriver](https://cloud.google.com/logging/) |
| [Sumorus](https://github.com/doublefree/sumorus) | Hook for logging to [SumoLogic](https://www.sumologic.com/)|
| [Syslog](https://github.com/sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [Syslog TLS](https://github.com/shinji62/logrus-syslog-ng) | Send errors to remote syslog server with TLS support. |
| [TraceView](https://github.com/evalphobia/logrus_appneta) | Hook for logging to [AppNeta TraceView](https://www.appneta.com/products/traceview/) |
| [Typetalk](https://github.com/dragon3/logrus-typetalk-hook) | Hook for logging to [Typetalk](https://www.typetalk.in/) |
| [logz.io](https://github.com/ripcurld00d/logrus-logzio-hook) | Hook for logging to [logz.io](https://logz.io), a Log as a Service using Logstash |
| [SQS-Hook](https://github.com/tsarpaul/logrus_sqs) | Hook for logging to [Amazon Simple Queue Service (SQS)](https://aws.amazon.com/sqs/) |
#### Level logging #### Level logging
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic. Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
```go ```go
log.Trace("Something very low level.")
log.Debug("Useful debugging information.") log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!") log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.") log.Warn("You should probably take a look at this.")
@ -366,16 +352,20 @@ The built-in logging formatters are:
field to `true`. To force no colored output even if there is a TTY set the field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`. For Windows, see `DisableColors` field to `true`. For Windows, see
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable). [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
* When colors are enabled, levels are truncated to 4 characters by default. To disable
truncation set the `DisableLevelTruncation` field to `true`.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
* `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.JSONFormatter`. Logs fields as JSON.
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter). * All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).
Third party logging formatters: Third party logging formatters:
* [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can by parsed by Kubernetes and Google Container Engine. * [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
* [`GELF`](https://github.com/fabienm/go-logrus-formatters). Formats entries so they comply to Graylog's [GELF 1.1 specification](http://docs.graylog.org/en/2.4/pages/gelf.html).
* [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events. * [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
* [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout. * [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦. * [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
You can define your formatter by implementing the `Formatter` interface, You can define your formatter by implementing the `Formatter` interface,
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
@ -489,7 +479,7 @@ logrus.RegisterExitHandler(handler)
#### Thread safety #### Thread safety
By default Logger is protected by mutex for concurrent writes, this mutex is invoked when calling hooks and writing logs. By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking. If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
Situation when locking is not needed includes: Situation when locking is not needed includes:

View File

@ -51,9 +51,9 @@ func Exit(code int) {
os.Exit(code) os.Exit(code)
} }
// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke // RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
// all handlers. The handlers will also be invoked when any Fatal log entry is // call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// made. // any Fatal log entry is made.
// //
// This method is useful when a caller wishes to use logrus to log a fatal // This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be // message but also needs to gracefully shutdown. An example usecase could be
@ -62,3 +62,15 @@ func Exit(code int) {
func RegisterExitHandler(handler func()) { func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler) handlers = append(handlers, handler)
} }
// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// any Fatal log entry is made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
}

View File

@ -2,13 +2,33 @@ package logrus
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"os" "os"
"reflect"
"runtime"
"strings"
"sync" "sync"
"time" "time"
) )
var bufferPool *sync.Pool var (
bufferPool *sync.Pool
// qualified package name, cached at first use
logrusPackage string
// Positions in the call stack when tracing to report the calling method
minimumCallerDepth int
// Used for caller information initialisation
callerInitOnce sync.Once
)
const (
maximumCallerDepth int = 25
knownLogrusFrames int = 4
)
func init() { func init() {
bufferPool = &sync.Pool{ bufferPool = &sync.Pool{
@ -16,15 +36,18 @@ func init() {
return new(bytes.Buffer) return new(bytes.Buffer)
}, },
} }
// start at the bottom of the stack before the package-name cache is primed
minimumCallerDepth = 1
} }
// Defines the key when adding errors using WithError. // Defines the key when adding errors using WithError.
var ErrorKey = "error" var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all // An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info, // the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and // Info, Warn, Error, Fatal or Panic is called on it. These objects can be
// passed around as much as you wish to avoid field duplication. // reused and passed around as much as you wish to avoid field duplication.
type Entry struct { type Entry struct {
Logger *Logger Logger *Logger
@ -34,22 +57,31 @@ type Entry struct {
// Time at which the log entry was created // Time at which the log entry was created
Time time.Time Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field. // This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic // Calling method, with package name
Caller *runtime.Frame
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
Message string Message string
// When formatter is called in entry.log(), an Buffer may be set to entry // When formatter is called in entry.log(), a Buffer may be set to entry
Buffer *bytes.Buffer Buffer *bytes.Buffer
// Contains the context set by the user. Useful for hook processing etc.
Context context.Context
// err may contain a field formatting error
err string
} }
func NewEntry(logger *Logger) *Entry { func NewEntry(logger *Logger) *Entry {
return &Entry{ return &Entry{
Logger: logger, Logger: logger,
// Default is three fields, give a little extra room // Default is three fields, plus one optional. Give a little extra room.
Data: make(Fields, 5), Data: make(Fields, 6),
} }
} }
@ -69,6 +101,11 @@ func (entry *Entry) WithError(err error) *Entry {
return entry.WithField(ErrorKey, err) return entry.WithField(ErrorKey, err)
} }
// Add a context to the Entry.
func (entry *Entry) WithContext(ctx context.Context) *Entry {
return &Entry{Logger: entry.Logger, Data: entry.Data, Time: entry.Time, err: entry.err, Context: ctx}
}
// Add a single field to the Entry. // Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry { func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value}) return entry.WithFields(Fields{key: value})
@ -80,43 +117,120 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
for k, v := range entry.Data { for k, v := range entry.Data {
data[k] = v data[k] = v
} }
fieldErr := entry.err
for k, v := range fields { for k, v := range fields {
data[k] = v isErrField := false
if t := reflect.TypeOf(v); t != nil {
switch t.Kind() {
case reflect.Func:
isErrField = true
case reflect.Ptr:
isErrField = t.Elem().Kind() == reflect.Func
}
}
if isErrField {
tmp := fmt.Sprintf("can not add field %q", k)
if fieldErr != "" {
fieldErr = entry.err + ", " + tmp
} else {
fieldErr = tmp
}
} else {
data[k] = v
}
} }
return &Entry{Logger: entry.Logger, Data: data} return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
}
// Overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t, err: entry.err, Context: entry.Context}
}
// getPackageName reduces a fully qualified function name to the package name
// There really ought to be to be a better way...
func getPackageName(f string) string {
for {
lastPeriod := strings.LastIndex(f, ".")
lastSlash := strings.LastIndex(f, "/")
if lastPeriod > lastSlash {
f = f[:lastPeriod]
} else {
break
}
}
return f
}
// getCaller retrieves the name of the first non-logrus calling function
func getCaller() *runtime.Frame {
// cache this package's fully-qualified name
callerInitOnce.Do(func() {
pcs := make([]uintptr, 2)
_ = runtime.Callers(0, pcs)
logrusPackage = getPackageName(runtime.FuncForPC(pcs[1]).Name())
// now that we have the cache, we can skip a minimum count of known-logrus functions
// XXX this is dubious, the number of frames may vary
minimumCallerDepth = knownLogrusFrames
})
// Restrict the lookback frames to avoid runaway lookups
pcs := make([]uintptr, maximumCallerDepth)
depth := runtime.Callers(minimumCallerDepth, pcs)
frames := runtime.CallersFrames(pcs[:depth])
for f, again := frames.Next(); again; f, again = frames.Next() {
pkg := getPackageName(f.Function)
// If the caller isn't part of this package, we're done
if pkg != logrusPackage {
return &f
}
}
// if we got here, we failed to find the caller's context
return nil
}
func (entry Entry) HasCaller() (has bool) {
return entry.Logger != nil &&
entry.Logger.ReportCaller &&
entry.Caller != nil
} }
// This function is not declared with a pointer value because otherwise // This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines // race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) { func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer var buffer *bytes.Buffer
entry.Time = time.Now()
// Default to now, but allow users to override if they want.
//
// We don't have to worry about polluting future calls to Entry#log()
// with this assignment because this function is declared with a
// non-pointer receiver.
if entry.Time.IsZero() {
entry.Time = time.Now()
}
entry.Level = level entry.Level = level
entry.Message = msg entry.Message = msg
if entry.Logger.ReportCaller {
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil { entry.Caller = getCaller()
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
} }
entry.fireHooks()
buffer = bufferPool.Get().(*bytes.Buffer) buffer = bufferPool.Get().(*bytes.Buffer)
buffer.Reset() buffer.Reset()
defer bufferPool.Put(buffer) defer bufferPool.Put(buffer)
entry.Buffer = buffer entry.Buffer = buffer
serialized, err := entry.Logger.Formatter.Format(&entry)
entry.write()
entry.Buffer = nil entry.Buffer = nil
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock()
} else {
entry.Logger.mu.Lock()
_, err = entry.Logger.Out.Write(serialized)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
entry.Logger.mu.Unlock()
}
// To avoid Entry#log() returning a value that only would make sense for // To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking // panic() to use in Entry#Panic(), we avoid the allocation by checking
@ -126,26 +240,53 @@ func (entry Entry) log(level Level, msg string) {
} }
} }
func (entry *Entry) Debug(args ...interface{}) { func (entry *Entry) fireHooks() {
if entry.Logger.level() >= DebugLevel { entry.Logger.mu.Lock()
entry.log(DebugLevel, fmt.Sprint(args...)) defer entry.Logger.mu.Unlock()
err := entry.Logger.Hooks.Fire(entry.Level, entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
} }
} }
func (entry *Entry) write() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
} else {
_, err = entry.Logger.Out.Write(serialized)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
}
func (entry *Entry) Log(level Level, args ...interface{}) {
if entry.Logger.IsLevelEnabled(level) {
entry.log(level, fmt.Sprint(args...))
}
}
func (entry *Entry) Trace(args ...interface{}) {
entry.Log(TraceLevel, args...)
}
func (entry *Entry) Debug(args ...interface{}) {
entry.Log(DebugLevel, args...)
}
func (entry *Entry) Print(args ...interface{}) { func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...) entry.Info(args...)
} }
func (entry *Entry) Info(args ...interface{}) { func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.level() >= InfoLevel { entry.Log(InfoLevel, args...)
entry.log(InfoLevel, fmt.Sprint(args...))
}
} }
func (entry *Entry) Warn(args ...interface{}) { func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.level() >= WarnLevel { entry.Log(WarnLevel, args...)
entry.log(WarnLevel, fmt.Sprint(args...))
}
} }
func (entry *Entry) Warning(args ...interface{}) { func (entry *Entry) Warning(args ...interface{}) {
@ -153,37 +294,37 @@ func (entry *Entry) Warning(args ...interface{}) {
} }
func (entry *Entry) Error(args ...interface{}) { func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { entry.Log(ErrorLevel, args...)
entry.log(ErrorLevel, fmt.Sprint(args...))
}
} }
func (entry *Entry) Fatal(args ...interface{}) { func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.level() >= FatalLevel { entry.Log(FatalLevel, args...)
entry.log(FatalLevel, fmt.Sprint(args...)) entry.Logger.Exit(1)
}
Exit(1)
} }
func (entry *Entry) Panic(args ...interface{}) { func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.level() >= PanicLevel { entry.Log(PanicLevel, args...)
entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...)) panic(fmt.Sprint(args...))
} }
// Entry Printf family functions // Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) { func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
if entry.Logger.level() >= DebugLevel { if entry.Logger.IsLevelEnabled(level) {
entry.Debug(fmt.Sprintf(format, args...)) entry.Log(level, fmt.Sprintf(format, args...))
} }
} }
func (entry *Entry) Tracef(format string, args ...interface{}) {
entry.Logf(TraceLevel, format, args...)
}
func (entry *Entry) Debugf(format string, args ...interface{}) {
entry.Logf(DebugLevel, format, args...)
}
func (entry *Entry) Infof(format string, args ...interface{}) { func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.level() >= InfoLevel { entry.Logf(InfoLevel, format, args...)
entry.Info(fmt.Sprintf(format, args...))
}
} }
func (entry *Entry) Printf(format string, args ...interface{}) { func (entry *Entry) Printf(format string, args ...interface{}) {
@ -191,9 +332,7 @@ func (entry *Entry) Printf(format string, args ...interface{}) {
} }
func (entry *Entry) Warnf(format string, args ...interface{}) { func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.level() >= WarnLevel { entry.Logf(WarnLevel, format, args...)
entry.Warn(fmt.Sprintf(format, args...))
}
} }
func (entry *Entry) Warningf(format string, args ...interface{}) { func (entry *Entry) Warningf(format string, args ...interface{}) {
@ -201,36 +340,36 @@ func (entry *Entry) Warningf(format string, args ...interface{}) {
} }
func (entry *Entry) Errorf(format string, args ...interface{}) { func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { entry.Logf(ErrorLevel, format, args...)
entry.Error(fmt.Sprintf(format, args...))
}
} }
func (entry *Entry) Fatalf(format string, args ...interface{}) { func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.level() >= FatalLevel { entry.Logf(FatalLevel, format, args...)
entry.Fatal(fmt.Sprintf(format, args...)) entry.Logger.Exit(1)
}
Exit(1)
} }
func (entry *Entry) Panicf(format string, args ...interface{}) { func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.level() >= PanicLevel { entry.Logf(PanicLevel, format, args...)
entry.Panic(fmt.Sprintf(format, args...))
}
} }
// Entry Println family functions // Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) { func (entry *Entry) Logln(level Level, args ...interface{}) {
if entry.Logger.level() >= DebugLevel { if entry.Logger.IsLevelEnabled(level) {
entry.Debug(entry.sprintlnn(args...)) entry.Log(level, entry.sprintlnn(args...))
} }
} }
func (entry *Entry) Traceln(args ...interface{}) {
entry.Logln(TraceLevel, args...)
}
func (entry *Entry) Debugln(args ...interface{}) {
entry.Logln(DebugLevel, args...)
}
func (entry *Entry) Infoln(args ...interface{}) { func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.level() >= InfoLevel { entry.Logln(InfoLevel, args...)
entry.Info(entry.sprintlnn(args...))
}
} }
func (entry *Entry) Println(args ...interface{}) { func (entry *Entry) Println(args ...interface{}) {
@ -238,9 +377,7 @@ func (entry *Entry) Println(args ...interface{}) {
} }
func (entry *Entry) Warnln(args ...interface{}) { func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.level() >= WarnLevel { entry.Logln(WarnLevel, args...)
entry.Warn(entry.sprintlnn(args...))
}
} }
func (entry *Entry) Warningln(args ...interface{}) { func (entry *Entry) Warningln(args ...interface{}) {
@ -248,22 +385,16 @@ func (entry *Entry) Warningln(args ...interface{}) {
} }
func (entry *Entry) Errorln(args ...interface{}) { func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel { entry.Logln(ErrorLevel, args...)
entry.Error(entry.sprintlnn(args...))
}
} }
func (entry *Entry) Fatalln(args ...interface{}) { func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.level() >= FatalLevel { entry.Logln(FatalLevel, args...)
entry.Fatal(entry.sprintlnn(args...)) entry.Logger.Exit(1)
}
Exit(1)
} }
func (entry *Entry) Panicln(args ...interface{}) { func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.level() >= PanicLevel { entry.Logln(PanicLevel, args...)
entry.Panic(entry.sprintlnn(args...))
}
} }
// Sprintlnn => Sprint no newline. This is to get the behavior of how // Sprintlnn => Sprint no newline. This is to get the behavior of how

View File

@ -1,7 +1,9 @@
package logrus package logrus
import ( import (
"context"
"io" "io"
"time"
) )
var ( var (
@ -15,37 +17,38 @@ func StandardLogger() *Logger {
// SetOutput sets the standard logger output. // SetOutput sets the standard logger output.
func SetOutput(out io.Writer) { func SetOutput(out io.Writer) {
std.mu.Lock() std.SetOutput(out)
defer std.mu.Unlock()
std.Out = out
} }
// SetFormatter sets the standard logger formatter. // SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) { func SetFormatter(formatter Formatter) {
std.mu.Lock() std.SetFormatter(formatter)
defer std.mu.Unlock() }
std.Formatter = formatter
// SetReportCaller sets whether the standard logger will include the calling
// method as a field.
func SetReportCaller(include bool) {
std.SetReportCaller(include)
} }
// SetLevel sets the standard logger level. // SetLevel sets the standard logger level.
func SetLevel(level Level) { func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.SetLevel(level) std.SetLevel(level)
} }
// GetLevel returns the standard logger level. // GetLevel returns the standard logger level.
func GetLevel() Level { func GetLevel() Level {
std.mu.Lock() return std.GetLevel()
defer std.mu.Unlock() }
return std.level()
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
} }
// AddHook adds a hook to the standard logger hooks. // AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) { func AddHook(hook Hook) {
std.mu.Lock() std.AddHook(hook)
defer std.mu.Unlock()
std.Hooks.Add(hook)
} }
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key. // WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
@ -53,6 +56,11 @@ func WithError(err error) *Entry {
return std.WithField(ErrorKey, err) return std.WithField(ErrorKey, err)
} }
// WithContext creates an entry from the standard logger and adds a context to it.
func WithContext(ctx context.Context) *Entry {
return std.WithContext(ctx)
}
// WithField creates an entry from the standard logger and adds a field to // WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`. // it. If you want multiple fields, use `WithFields`.
// //
@ -72,6 +80,20 @@ func WithFields(fields Fields) *Entry {
return std.WithFields(fields) return std.WithFields(fields)
} }
// WithTime creats an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger. // Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) { func Debug(args ...interface{}) {
std.Debug(args...) std.Debug(args...)
@ -107,11 +129,16 @@ func Panic(args ...interface{}) {
std.Panic(args...) std.Panic(args...)
} }
// Fatal logs a message at level Fatal on the standard logger. // Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) { func Fatal(args ...interface{}) {
std.Fatal(args...) std.Fatal(args...)
} }
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger. // Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) { func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...) std.Debugf(format, args...)
@ -147,11 +174,16 @@ func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...) std.Panicf(format, args...)
} }
// Fatalf logs a message at level Fatal on the standard logger. // Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) { func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...) std.Fatalf(format, args...)
} }
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger. // Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) { func Debugln(args ...interface{}) {
std.Debugln(args...) std.Debugln(args...)
@ -187,7 +219,7 @@ func Panicln(args ...interface{}) {
std.Panicln(args...) std.Panicln(args...)
} }
// Fatalln logs a message at level Fatal on the standard logger. // Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) { func Fatalln(args ...interface{}) {
std.Fatalln(args...) std.Fatalln(args...)
} }

View File

@ -2,7 +2,16 @@ package logrus
import "time" import "time"
const defaultTimestampFormat = time.RFC3339 // Default key names for the default fields
const (
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
)
// The Formatter interface is used to implement a custom Formatter. It takes an // The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones: // `Entry`. It exposes all the fields, including the default ones:
@ -18,7 +27,7 @@ type Formatter interface {
Format(*Entry) ([]byte, error) Format(*Entry) ([]byte, error)
} }
// This is to not silently overwrite `time`, `msg` and `level` fields when // This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
// dumping it. If this code wasn't there doing: // dumping it. If this code wasn't there doing:
// //
// logrus.WithField("level", 1).Info("hello") // logrus.WithField("level", 1).Info("hello")
@ -30,16 +39,40 @@ type Formatter interface {
// //
// It's not exported because it's still using Data in an opinionated way. It's to // It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters. // avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) { func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
if t, ok := data["time"]; ok { timeKey := fieldMap.resolve(FieldKeyTime)
data["fields.time"] = t if t, ok := data[timeKey]; ok {
data["fields."+timeKey] = t
delete(data, timeKey)
} }
if m, ok := data["msg"]; ok { msgKey := fieldMap.resolve(FieldKeyMsg)
data["fields.msg"] = m if m, ok := data[msgKey]; ok {
data["fields."+msgKey] = m
delete(data, msgKey)
} }
if l, ok := data["level"]; ok { levelKey := fieldMap.resolve(FieldKeyLevel)
data["fields.level"] = l if l, ok := data[levelKey]; ok {
data["fields."+levelKey] = l
delete(data, levelKey)
}
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
if l, ok := data[logrusErrKey]; ok {
data["fields."+logrusErrKey] = l
delete(data, logrusErrKey)
}
// If reportCaller is not set, 'func' will not conflict.
if reportCaller {
funcKey := fieldMap.resolve(FieldKeyFunc)
if l, ok := data[funcKey]; ok {
data["fields."+funcKey] = l
}
fileKey := fieldMap.resolve(FieldKeyFile)
if l, ok := data[fileKey]; ok {
data["fields."+fileKey] = l
}
} }
} }

10
vendor/github.com/sirupsen/logrus/go.mod generated vendored Normal file
View File

@ -0,0 +1,10 @@
module github.com/sirupsen/logrus
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/sys v0.0.0-20190422165155-953cdadca894
)

16
vendor/github.com/sirupsen/logrus/go.sum generated vendored Normal file
View File

@ -0,0 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,8 +1,10 @@
package logrus package logrus
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"runtime"
) )
type fieldKey string type fieldKey string
@ -10,13 +12,6 @@ type fieldKey string
// FieldMap allows customization of the key names for default fields. // FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string type FieldMap map[fieldKey]string
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
)
func (f FieldMap) resolve(key fieldKey) string { func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok { if k, ok := f[key]; ok {
return k return k
@ -33,21 +28,34 @@ type JSONFormatter struct {
// DisableTimestamp allows disabling automatic timestamps in output // DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool DisableTimestamp bool
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string
// FieldMap allows users to customize the names of keys for default fields. // FieldMap allows users to customize the names of keys for default fields.
// As an example: // As an example:
// formatter := &JSONFormatter{ // formatter := &JSONFormatter{
// FieldMap: FieldMap{ // FieldMap: FieldMap{
// FieldKeyTime: "@timestamp", // FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level", // FieldKeyLevel: "@level",
// FieldKeyMsg: "@message", // FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller",
// }, // },
// } // }
FieldMap FieldMap FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the json data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from json fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
// PrettyPrint will indent all json logs
PrettyPrint bool
} }
// Format renders a single log entry // Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3) data := make(Fields, len(entry.Data)+4)
for k, v := range entry.Data { for k, v := range entry.Data {
switch v := v.(type) { switch v := v.(type) {
case error: case error:
@ -58,22 +66,56 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data[k] = v data[k] = v
} }
} }
prefixFieldClashes(data)
if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = defaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
if entry.err != "" {
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
}
if !f.DisableTimestamp { if !f.DisableTimestamp {
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat) data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
} }
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String() data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
if entry.HasCaller() {
serialized, err := json.Marshal(data) funcVal := entry.Caller.Function
if err != nil { fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
}
if fileVal != "" {
data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
}
} }
return append(serialized, '\n'), nil
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
encoder := json.NewEncoder(b)
if f.PrettyPrint {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(data); err != nil {
return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err)
}
return b.Bytes(), nil
} }

View File

@ -1,16 +1,18 @@
package logrus package logrus
import ( import (
"context"
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
) )
type Logger struct { type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to // file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventorous, such as logging to Kafka. // something more adventurous, such as logging to Kafka.
Out io.Writer Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging // Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking // levels and log entries. For example, to send errors to an error tracking
@ -23,6 +25,10 @@ type Logger struct {
// own that implements the `Formatter` interface, see the `README` or included // own that implements the `Formatter` interface, see the `README` or included
// formatters for examples. // formatters for examples.
Formatter Formatter Formatter Formatter
// Flag for whether to log caller info (off by default)
ReportCaller bool
// The logging level the logger should log at. This is typically (and defaults // The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. // logged.
@ -31,8 +37,12 @@ type Logger struct {
mu MutexWrap mu MutexWrap
// Reusable empty entry // Reusable empty entry
entryPool sync.Pool entryPool sync.Pool
// Function to exit the application, defaults to `os.Exit()`
ExitFunc exitFunc
} }
type exitFunc func(int)
type MutexWrap struct { type MutexWrap struct {
lock sync.Mutex lock sync.Mutex
disabled bool disabled bool
@ -68,10 +78,12 @@ func (mw *MutexWrap) Disable() {
// It's recommended to make this a global instance called `log`. // It's recommended to make this a global instance called `log`.
func New() *Logger { func New() *Logger {
return &Logger{ return &Logger{
Out: os.Stderr, Out: os.Stderr,
Formatter: new(TextFormatter), Formatter: new(TextFormatter),
Hooks: make(LevelHooks), Hooks: make(LevelHooks),
Level: InfoLevel, Level: InfoLevel,
ExitFunc: os.Exit,
ReportCaller: false,
} }
} }
@ -84,11 +96,12 @@ func (logger *Logger) newEntry() *Entry {
} }
func (logger *Logger) releaseEntry(entry *Entry) { func (logger *Logger) releaseEntry(entry *Entry) {
entry.Data = map[string]interface{}{}
logger.entryPool.Put(entry) logger.entryPool.Put(entry)
} }
// Adds a field to the log entry, note that it doesn't log until you call // Adds a field to the log entry, note that it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. // Debug, Print, Info, Warn, Error, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`. // If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry { func (logger *Logger) WithField(key string, value interface{}) *Entry {
entry := logger.newEntry() entry := logger.newEntry()
@ -112,20 +125,38 @@ func (logger *Logger) WithError(err error) *Entry {
return entry.WithError(err) return entry.WithError(err)
} }
func (logger *Logger) Debugf(format string, args ...interface{}) { // Add a context to the log entry.
if logger.level() >= DebugLevel { func (logger *Logger) WithContext(ctx context.Context) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithContext(ctx)
}
// Overrides the time of the log entry.
func (logger *Logger) WithTime(t time.Time) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithTime(t)
}
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Debugf(format, args...) entry.Logf(level, format, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Tracef(format string, args ...interface{}) {
logger.Logf(TraceLevel, format, args...)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
logger.Logf(DebugLevel, format, args...)
}
func (logger *Logger) Infof(format string, args ...interface{}) { func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.level() >= InfoLevel { logger.Logf(InfoLevel, format, args...)
entry := logger.newEntry()
entry.Infof(format, args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Printf(format string, args ...interface{}) { func (logger *Logger) Printf(format string, args ...interface{}) {
@ -135,123 +166,91 @@ func (logger *Logger) Printf(format string, args ...interface{}) {
} }
func (logger *Logger) Warnf(format string, args ...interface{}) { func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.level() >= WarnLevel { logger.Logf(WarnLevel, format, args...)
entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Warningf(format string, args ...interface{}) { func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.level() >= WarnLevel { logger.Warnf(format, args...)
entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Errorf(format string, args ...interface{}) { func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.level() >= ErrorLevel { logger.Logf(ErrorLevel, format, args...)
entry := logger.newEntry()
entry.Errorf(format, args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Fatalf(format string, args ...interface{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.level() >= FatalLevel { logger.Logf(FatalLevel, format, args...)
entry := logger.newEntry() logger.Exit(1)
entry.Fatalf(format, args...)
logger.releaseEntry(entry)
}
Exit(1)
} }
func (logger *Logger) Panicf(format string, args ...interface{}) { func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.level() >= PanicLevel { logger.Logf(PanicLevel, format, args...)
}
func (logger *Logger) Log(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Panicf(format, args...) entry.Log(level, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Trace(args ...interface{}) {
logger.Log(TraceLevel, args...)
}
func (logger *Logger) Debug(args ...interface{}) { func (logger *Logger) Debug(args ...interface{}) {
if logger.level() >= DebugLevel { logger.Log(DebugLevel, args...)
entry := logger.newEntry()
entry.Debug(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Info(args ...interface{}) { func (logger *Logger) Info(args ...interface{}) {
if logger.level() >= InfoLevel { logger.Log(InfoLevel, args...)
entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Print(args ...interface{}) { func (logger *Logger) Print(args ...interface{}) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Info(args...) entry.Print(args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
func (logger *Logger) Warn(args ...interface{}) { func (logger *Logger) Warn(args ...interface{}) {
if logger.level() >= WarnLevel { logger.Log(WarnLevel, args...)
entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Warning(args ...interface{}) { func (logger *Logger) Warning(args ...interface{}) {
if logger.level() >= WarnLevel { logger.Warn(args...)
entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Error(args ...interface{}) { func (logger *Logger) Error(args ...interface{}) {
if logger.level() >= ErrorLevel { logger.Log(ErrorLevel, args...)
entry := logger.newEntry()
entry.Error(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Fatal(args ...interface{}) { func (logger *Logger) Fatal(args ...interface{}) {
if logger.level() >= FatalLevel { logger.Log(FatalLevel, args...)
entry := logger.newEntry() logger.Exit(1)
entry.Fatal(args...)
logger.releaseEntry(entry)
}
Exit(1)
} }
func (logger *Logger) Panic(args ...interface{}) { func (logger *Logger) Panic(args ...interface{}) {
if logger.level() >= PanicLevel { logger.Log(PanicLevel, args...)
}
func (logger *Logger) Logln(level Level, args ...interface{}) {
if logger.IsLevelEnabled(level) {
entry := logger.newEntry() entry := logger.newEntry()
entry.Panic(args...) entry.Logln(level, args...)
logger.releaseEntry(entry) logger.releaseEntry(entry)
} }
} }
func (logger *Logger) Traceln(args ...interface{}) {
logger.Logln(TraceLevel, args...)
}
func (logger *Logger) Debugln(args ...interface{}) { func (logger *Logger) Debugln(args ...interface{}) {
if logger.level() >= DebugLevel { logger.Logln(DebugLevel, args...)
entry := logger.newEntry()
entry.Debugln(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Infoln(args ...interface{}) { func (logger *Logger) Infoln(args ...interface{}) {
if logger.level() >= InfoLevel { logger.Logln(InfoLevel, args...)
entry := logger.newEntry()
entry.Infoln(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Println(args ...interface{}) { func (logger *Logger) Println(args ...interface{}) {
@ -261,44 +260,32 @@ func (logger *Logger) Println(args ...interface{}) {
} }
func (logger *Logger) Warnln(args ...interface{}) { func (logger *Logger) Warnln(args ...interface{}) {
if logger.level() >= WarnLevel { logger.Logln(WarnLevel, args...)
entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Warningln(args ...interface{}) { func (logger *Logger) Warningln(args ...interface{}) {
if logger.level() >= WarnLevel { logger.Warnln(args...)
entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Errorln(args ...interface{}) { func (logger *Logger) Errorln(args ...interface{}) {
if logger.level() >= ErrorLevel { logger.Logln(ErrorLevel, args...)
entry := logger.newEntry()
entry.Errorln(args...)
logger.releaseEntry(entry)
}
} }
func (logger *Logger) Fatalln(args ...interface{}) { func (logger *Logger) Fatalln(args ...interface{}) {
if logger.level() >= FatalLevel { logger.Logln(FatalLevel, args...)
entry := logger.newEntry() logger.Exit(1)
entry.Fatalln(args...)
logger.releaseEntry(entry)
}
Exit(1)
} }
func (logger *Logger) Panicln(args ...interface{}) { func (logger *Logger) Panicln(args ...interface{}) {
if logger.level() >= PanicLevel { logger.Logln(PanicLevel, args...)
entry := logger.newEntry() }
entry.Panicln(args...)
logger.releaseEntry(entry) func (logger *Logger) Exit(code int) {
runHandlers()
if logger.ExitFunc == nil {
logger.ExitFunc = os.Exit
} }
logger.ExitFunc(code)
} }
//When file is opened with appending mode, it's safe to //When file is opened with appending mode, it's safe to
@ -312,6 +299,53 @@ func (logger *Logger) level() Level {
return Level(atomic.LoadUint32((*uint32)(&logger.Level))) return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
} }
// SetLevel sets the logger level.
func (logger *Logger) SetLevel(level Level) { func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level)) atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
} }
// GetLevel returns the logger level.
func (logger *Logger) GetLevel() Level {
return logger.level()
}
// AddHook adds a hook to the logger hooks.
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}
// IsLevelEnabled checks if the log level of the logger is greater than the level param
func (logger *Logger) IsLevelEnabled(level Level) bool {
return logger.level() >= level
}
// SetFormatter sets the logger formatter.
func (logger *Logger) SetFormatter(formatter Formatter) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Formatter = formatter
}
// SetOutput sets the logger output.
func (logger *Logger) SetOutput(output io.Writer) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Out = output
}
func (logger *Logger) SetReportCaller(reportCaller bool) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.ReportCaller = reportCaller
}
// ReplaceHooks replaces the logger hooks and returns the old ones
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
logger.mu.Lock()
oldHooks := logger.Hooks
logger.Hooks = hooks
logger.mu.Unlock()
return oldHooks
}

View File

@ -14,22 +14,11 @@ type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic". // Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string { func (level Level) String() string {
switch level { if b, err := level.MarshalText(); err == nil {
case DebugLevel: return string(b)
return "debug" } else {
case InfoLevel: return "unknown"
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
} }
return "unknown"
} }
// ParseLevel takes a string level and returns the Logrus log level constant. // ParseLevel takes a string level and returns the Logrus log level constant.
@ -47,12 +36,47 @@ func ParseLevel(lvl string) (Level, error) {
return InfoLevel, nil return InfoLevel, nil
case "debug": case "debug":
return DebugLevel, nil return DebugLevel, nil
case "trace":
return TraceLevel, nil
} }
var l Level var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl) return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
} }
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := ParseLevel(string(text))
if err != nil {
return err
}
*level = Level(l)
return nil
}
func (level Level) MarshalText() ([]byte, error) {
switch level {
case TraceLevel:
return []byte("trace"), nil
case DebugLevel:
return []byte("debug"), nil
case InfoLevel:
return []byte("info"), nil
case WarnLevel:
return []byte("warning"), nil
case ErrorLevel:
return []byte("error"), nil
case FatalLevel:
return []byte("fatal"), nil
case PanicLevel:
return []byte("panic"), nil
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// A constant exposing all logging levels // A constant exposing all logging levels
var AllLevels = []Level{ var AllLevels = []Level{
PanicLevel, PanicLevel,
@ -61,6 +85,7 @@ var AllLevels = []Level{
WarnLevel, WarnLevel,
InfoLevel, InfoLevel,
DebugLevel, DebugLevel,
TraceLevel,
} }
// These are the different logging levels. You can set the logging level to log // These are the different logging levels. You can set the logging level to log
@ -69,7 +94,7 @@ const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the // PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ... // message passed to Debug, Info, ...
PanicLevel Level = iota PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
// logging level is set to Panic. // logging level is set to Panic.
FatalLevel FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted. // ErrorLevel level. Logs. Used for errors that should definitely be noted.
@ -82,6 +107,8 @@ const (
InfoLevel InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging. // DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel DebugLevel
// TraceLevel level. Designates finer-grained informational events than the Debug.
TraceLevel
) )
// Won't compile if StdLogger can't be realized by a log.Logger // Won't compile if StdLogger can't be realized by a log.Logger
@ -140,4 +167,20 @@ type FieldLogger interface {
Errorln(args ...interface{}) Errorln(args ...interface{})
Fatalln(args ...interface{}) Fatalln(args ...interface{})
Panicln(args ...interface{}) Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
}
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
// here for consistancy. Do not use. Use Logger or Entry instead.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
} }

View File

@ -1,10 +0,0 @@
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
type Termios unix.Termios

View File

@ -0,0 +1,11 @@
// +build appengine
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}

View File

@ -0,0 +1,13 @@
// +build darwin dragonfly freebsd netbsd openbsd
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

View File

@ -0,0 +1,11 @@
// +build js nacl plan9
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return false
}

View File

@ -0,0 +1,17 @@
// +build !appengine,!js,!windows,!nacl,!plan9
package logrus
import (
"io"
"os"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return isTerminal(int(v.Fd()))
default:
return false
}
}

View File

@ -0,0 +1,11 @@
package logrus
import (
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
return err == nil
}

View File

@ -0,0 +1,13 @@
// +build linux aix
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}

View File

@ -0,0 +1,34 @@
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"syscall"
sequences "github.com/konsorten/go-windows-terminal-sequences"
)
func initTerminal(w io.Writer) {
switch v := w.(type) {
case *os.File:
sequences.EnableVirtualTerminalProcessing(syscall.Handle(v.Fd()), true)
}
}
func checkIfTerminal(w io.Writer) bool {
var ret bool
switch v := w.(type) {
case *os.File:
var mode uint32
err := syscall.GetConsoleMode(syscall.Handle(v.Fd()), &mode)
ret = (err == nil)
default:
ret = false
}
if ret {
initTerminal(w)
}
return ret
}

View File

@ -1,14 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
type Termios unix.Termios

View File

@ -3,28 +3,22 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"runtime"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"golang.org/x/crypto/ssh/terminal"
) )
const ( const (
nocolor = 0 red = 31
red = 31 yellow = 33
green = 32 blue = 36
yellow = 33 gray = 37
blue = 36
gray = 37
) )
var ( var baseTimestamp time.Time
baseTimestamp time.Time
)
func init() { func init() {
baseTimestamp = time.Now() baseTimestamp = time.Now()
@ -38,6 +32,9 @@ type TextFormatter struct {
// Force disabling colors. // Force disabling colors.
DisableColors bool DisableColors bool
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool
// Disable timestamp logging. useful when output is redirected to logging // Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps. // system that already adds timestamps.
DisableTimestamp bool DisableTimestamp bool
@ -54,69 +51,151 @@ type TextFormatter struct {
// be desired. // be desired.
DisableSorting bool DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool
// QuoteEmptyFields will wrap empty fields in quotes if true // QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool QuoteEmptyFields bool
// Whether the logger's out is to a terminal // Whether the logger's out is to a terminal
isTerminal bool isTerminal bool
sync.Once // FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once
} }
func (f *TextFormatter) init(entry *Entry) { func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil { if entry.Logger != nil {
f.isTerminal = f.checkIfTerminal(entry.Logger.Out) f.isTerminal = checkIfTerminal(entry.Logger.Out)
} }
} }
func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { func (f *TextFormatter) isColored() bool {
switch v := w.(type) { isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
case *os.File:
return terminal.IsTerminal(int(v.Fd())) if f.EnvironmentOverrideColors {
default: if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
return false isColored = true
} else if ok && force == "0" {
isColored = false
} else if os.Getenv("CLICOLOR") == "0" {
isColored = false
}
} }
return isColored && !f.DisableColors
} }
// Format renders a single log entry // Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var b *bytes.Buffer data := make(Fields)
keys := make([]string, 0, len(entry.Data)) for k, v := range entry.Data {
for k := range entry.Data { data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k) keys = append(keys, k)
} }
if !f.DisableSorting { var funcVal, fileVal string
sort.Strings(keys)
fixedKeys := make([]string, 0, 4+len(data))
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
} }
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
if entry.Message != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
}
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
} else {
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
}
if funcVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
}
if fileVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
}
}
if !f.DisableSorting {
if f.SortingFunc == nil {
sort.Strings(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil { if entry.Buffer != nil {
b = entry.Buffer b = entry.Buffer
} else { } else {
b = &bytes.Buffer{} b = &bytes.Buffer{}
} }
prefixFieldClashes(entry.Data) f.terminalInitOnce.Do(func() { f.init(entry) })
f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat timestampFormat := f.TimestampFormat
if timestampFormat == "" { if timestampFormat == "" {
timestampFormat = defaultTimestampFormat timestampFormat = defaultTimestampFormat
} }
if isColored { if f.isColored() {
f.printColored(b, entry, keys, timestampFormat) f.printColored(b, entry, keys, data, timestampFormat)
} else { } else {
if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat)) for _, key := range fixedKeys {
} var value interface{}
f.appendKeyValue(b, "level", entry.Level.String()) switch {
if entry.Message != "" { case key == f.FieldMap.resolve(FieldKeyTime):
f.appendKeyValue(b, "msg", entry.Message) value = entry.Time.Format(timestampFormat)
} case key == f.FieldMap.resolve(FieldKeyLevel):
for _, key := range keys { value = entry.Level.String()
f.appendKeyValue(b, key, entry.Data[key]) case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
} }
} }
@ -124,10 +203,10 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
return b.Bytes(), nil return b.Bytes(), nil
} }
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) { func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
var levelColor int var levelColor int
switch entry.Level { switch entry.Level {
case DebugLevel: case DebugLevel, TraceLevel:
levelColor = gray levelColor = gray
case WarnLevel: case WarnLevel:
levelColor = yellow levelColor = yellow
@ -137,17 +216,42 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelColor = blue levelColor = blue
} }
levelText := strings.ToUpper(entry.Level.String())[0:4] levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation {
levelText = levelText[0:4]
}
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if fileVal == "" {
caller = funcVal
} else if funcVal == "" {
caller = fileVal
} else {
caller = fileVal + " " + funcVal
}
}
if f.DisableTimestamp { if f.DisableTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
} else if !f.FullTimestamp { } else if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
} else { } else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
} }
for _, k := range keys { for _, k := range keys {
v := entry.Data[k] v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v) f.appendValue(b, v)
} }

View File

@ -24,6 +24,8 @@ func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
var printFunc func(args ...interface{}) var printFunc func(args ...interface{})
switch level { switch level {
case TraceLevel:
printFunc = entry.Trace
case DebugLevel: case DebugLevel:
printFunc = entry.Debug printFunc = entry.Debug
case InfoLevel: case InfoLevel: