TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
This commit is contained in:
parent
d26a8c5d44
commit
80a15547e3
|
@ -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)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
@ -12,8 +13,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
"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,7 +238,7 @@ 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)
|
logger.Infof("Build info: %+v", *buildInfo)
|
||||||
logger.Infof("Version %s", version)
|
logger.Infof("Version %s", version)
|
||||||
logClientOptions(c)
|
logClientOptions(c)
|
||||||
|
@ -280,6 +283,18 @@ 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()
|
||||||
|
}()
|
||||||
|
|
||||||
// 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()
|
||||||
|
@ -324,7 +339,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"))
|
||||||
|
|
|
@ -12,6 +12,7 @@ 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/origin"
|
"github.com/cloudflare/cloudflared/origin"
|
||||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||||
|
@ -145,7 +146,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) {
|
||||||
|
|
|
@ -2,15 +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/streamhandler"
|
|
||||||
"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"
|
||||||
|
|
||||||
|
@ -18,7 +17,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dialTimeout = 5 * time.Second
|
|
||||||
openStreamTimeout = 30 * time.Second
|
openStreamTimeout = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,123 +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 {
|
|
||||||
return "muxer shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionConfig struct {
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
HeartbeatInterval time.Duration
|
|
||||||
MaxHeartbeats uint64
|
|
||||||
Logger *logrus.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
type connectionHandler interface {
|
|
||||||
serve(ctx context.Context) error
|
|
||||||
connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error)
|
|
||||||
shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
type h2muxHandler struct {
|
|
||||||
muxer *h2mux.Muxer
|
muxer *h2mux.Muxer
|
||||||
logger *logrus.Entry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *h2muxHandler) serve(ctx context.Context) error {
|
func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) {
|
||||||
// Serve doesn't return until h2mux is shutdown
|
id, err := uuid.NewRandom()
|
||||||
if err := h.muxer.Serve(ctx); err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return muxerShutdownError{}
|
return &Connection{
|
||||||
|
id: id,
|
||||||
|
muxer: muxer,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Serve(ctx context.Context) error {
|
||||||
|
// Serve doesn't return until h2mux is shutdown
|
||||||
|
return c.muxer.Serve(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect is used to establish connections with cloudflare's edge network
|
// 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.OpenRPCStream(ctx)
|
stream, err := c.muxer.OpenRPCStream(ctx)
|
||||||
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,
|
|
||||||
streamHandler *streamhandler.StreamHandler,
|
|
||||||
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: streamHandler,
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
Refresh() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeAddrResolver discovers the addresses of Cloudflare's edge network through SRV record.
|
||||||
|
// It implements EdgeServiceDiscoverer interface
|
||||||
|
type EdgeAddrResolver struct {
|
||||||
|
sync.Mutex
|
||||||
|
// Addrs to connect to cloudflare's edge network
|
||||||
|
addrs []*net.TCPAddr
|
||||||
|
// index of the next element to use in addrs
|
||||||
|
nextAddrIndex int
|
||||||
|
logger *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEdgeAddrResolver(logger *logrus.Logger) (EdgeServiceDiscoverer, error) {
|
||||||
|
r := &EdgeAddrResolver{
|
||||||
|
logger: logger.WithField("subsystem", " edgeAddrResolver"),
|
||||||
|
}
|
||||||
|
if err := r.Refresh(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
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
|
||||||
}
|
}
|
||||||
return tcpAddrs, nil
|
r.Lock()
|
||||||
}
|
defer r.Unlock()
|
||||||
// HA service discovery lookup
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/h2mux"
|
||||||
|
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configurable = &EdgeManagerConfigurable{
|
||||||
|
[]h2mux.TunnelHostname{
|
||||||
|
"http.example.com",
|
||||||
|
"ws.example.com",
|
||||||
|
"hello.example.com",
|
||||||
|
},
|
||||||
|
&pogs.EdgeConnectionConfig{
|
||||||
|
NumHAConnections: 1,
|
||||||
|
HeartbeatInterval: 1 * time.Second,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
MaxFailedHeartbeats: 3,
|
||||||
|
UserCredentialPath: "/etc/cloudflared/cert.pem",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cloudflaredConfig = &CloudflaredConfig{
|
||||||
|
CloudflaredID: uuid.New(),
|
||||||
|
Tags: []pogs.Tag{
|
||||||
|
{Name: "pool", Value: "east-6"},
|
||||||
|
},
|
||||||
|
BuildInfo: &buildinfo.BuildInfo{
|
||||||
|
GoOS: "linux",
|
||||||
|
GoVersion: "1.12",
|
||||||
|
GoArch: "amd64",
|
||||||
|
CloudflaredVersion: "2019.6.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockStreamHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msh *mockStreamHandler) ServeStream(*h2mux.MuxedStream) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockEdgeManager() *EdgeManager {
|
||||||
|
return NewEdgeManager(
|
||||||
|
&mockStreamHandler{},
|
||||||
|
configurable,
|
||||||
|
[]byte{},
|
||||||
|
nil,
|
||||||
|
&mockEdgeServiceDiscoverer{},
|
||||||
|
cloudflaredConfig,
|
||||||
|
logrus.New(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateConfigurable(t *testing.T) {
|
||||||
|
m := mockEdgeManager()
|
||||||
|
newConfigurable := &EdgeManagerConfigurable{
|
||||||
|
[]h2mux.TunnelHostname{
|
||||||
|
"second.example.com",
|
||||||
|
},
|
||||||
|
&pogs.EdgeConnectionConfig{
|
||||||
|
NumHAConnections: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m.UpdateConfigurable(newConfigurable)
|
||||||
|
|
||||||
|
assert.Equal(t, newConfigurable, m.state.getConfigurable())
|
||||||
|
}
|
|
@ -1,158 +0,0 @@
|
||||||
package connection
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/streamhandler"
|
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
|
||||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
streamHandler *streamhandler.StreamHandler
|
|
||||||
newConfigChan chan<- *pogs.ClientConfig
|
|
||||||
useConfigResultChan <-chan *pogs.UseConfigurationResult
|
|
||||||
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 {
|
|
||||||
newConfigChan := make(chan *pogs.ClientConfig)
|
|
||||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
|
||||||
return &Supervisor{
|
|
||||||
streamHandler: streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, config.Logger),
|
|
||||||
newConfigChan: newConfigChan,
|
|
||||||
useConfigResultChan: useConfigResultChan,
|
|
||||||
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.streamHandler, 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)
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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/streamhandler"
|
||||||
|
@ -42,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
|
||||||
|
@ -140,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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3723,227 +3742,229 @@ 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{p\\\xe5u?\xe7\xde]]\xc9\x96" +
|
||||||
"\xde\xbd\xbe\x02#\x81f[\x97L\x82\xc1\x14\xe2\xd0\x82" +
|
"\xbc{\xb9K\x1d\xc9\xd6l\xeb\x92I0\x98\xe2(\xb4" +
|
||||||
"\xdaf\xf5\xb0\x1c\xad\xe3\xc7^=\x0c1f\xc6\xd7\xbb" +
|
"\xa06Y\xadd9Z\xc7\x8f\xbdz\x180f\xc6\xd7" +
|
||||||
"\x9f\xb4\xd7\xbe{\xef\xfa>l\xc95\xb1q\xa1\x80\xca" +
|
"\xbb\x9f\xa4k\xef\xde\xbb\xbe\x0f[rMl\\(\xa0" +
|
||||||
"\xc3&x\x06;\x90\xdan)\x81\xe2\x82\x09L\xc7\x14" +
|
"\x1a0\x04\xcd\x80CR\xdb\xad\x0b\xa1P0!\xd3\x09" +
|
||||||
"gB\xfa 4\x93!LC\xa7\xb4\xe9?\x01\xa63" +
|
"%\x994}\x904\xd3!\x99\x86Ni\x93?\x1a\xf0" +
|
||||||
"\xb4\x0c\x85$\xc3\xd0\xc1\xdc\xce\xf9\xeesW\x8bl:" +
|
"t\x86\x96\xa1&x\x18:\x98\xdb9\xdf}j\xb5\xc8" +
|
||||||
"\xf5\x1f\xd6\xce\xd9\xefq\xbe\xdf9\xe7w\x1e{\x9d\xdf" +
|
"v\xa7\xfe\xc3\xda9\xfb=\xce\xf7;\xe7\xfc\xceco" +
|
||||||
"1(\\\xdf\xf6J7\x80z\xa2\xad\xdd\xff\xfd\xdak" +
|
"\xccv\x0c\x08\xeb\xd3\xafv\x01\xa8'\xd3m\xde\xef\xd5" +
|
||||||
"\xa7~\xe7\xe8\x8f\xef\x04\xb9O\xf0\xbf\xf9\xd2\xfa\x9e\x8f" +
|
"_;\xfd\xdb\xf3?\xbe\x07\xe4\x1e\xc1\xfb\xca+\x9br" +
|
||||||
"\xddC\xff\x06\x80k^m\xdf\x87\xca\xbf\xb7K\x00\xca" +
|
"\x1f:G\xff\x0d\x00\xfb~\xd4v\x10\x95_\xb4I\x00" +
|
||||||
"\x9b\xed\x9b\x01\xfd\x7f\xban\xff\xdb\xdb\x7fy\xe4\x1e\x90" +
|
"\xca\x1bm\xdb\x00\xbd\x7f\xba\xf1\xd0[\xbb~\xf5\xc8\xfd" +
|
||||||
"\xfb0Y\x99\x91\x00\xd6|\xd0>\x8fJ\xa7$\x81\xe8" +
|
" \xf7`\xbc2%\x01\xf4\x9do\x9bC\xa5C\x92@" +
|
||||||
"?vk\xcf?\xe2\x89\x8f\x8e\x80\xfc%\x04hC\xfa" +
|
"\xf4\xbe~G\xee\x1f\xf0\xe4\x07\x8f\x80\xfcY\x04H#" +
|
||||||
"\xfa\x9d\xf6%\x02\xa0r\xbe\xbd\x00\xe8\xbfv\xcdK/" +
|
"}}\xaem\x99\x00\xa8\\l+\x00z\xaf]\xff\xca" +
|
||||||
"\x1e\xfe\xde\xdd\xdf\x06\xf5\x8b\x88\x10\xec\xef\x97\xfe\x07\x01" +
|
"\xcb\xc7\xbfu\xdf\xd7@\xfd\x0c\"\xf8\xfb{\xa5\xffA" +
|
||||||
"\x95\xeb%Z\xf0\xc1\x9f_\x9d9\xfd\xda\xf2\xef\xf0\x05" +
|
"@e\xbdD\x0b\xce\xff\xe9u\xa9\xe7^\xbb\xea\x1b|" +
|
||||||
"\xfe\xe3\xaf\xdf\xfc\xdc\xe1\xef\xfd\xc6\xbb0%H\x98\x01" +
|
"\x81w\xe6\xa7\xb7\xbex\xfc[\xbf\xfe6L\x08\x12\xa6" +
|
||||||
"X\xf3\x0d\xc9\xa6\xb5L\xfa\x0f@\xff\x81\x1f\xae\xb0\x86" +
|
"\x00\xfan\x97,Z\xcb\xa4\xff\x00\xf4\x1e\xfa\xc1J\xb3" +
|
||||||
"\xfes\xfb\xc9F\x9d\x82[G;\x06P\x99\xea\xa0\x07" +
|
"\xf8\x9f\xbbN-\xd4\xc9\xbfu\xb8\xbd\x1f\x95\x89vz" +
|
||||||
"\xa8\x1dt\xf0\xc3\xffrnS\xed\xc8\xf1S \x7f1" +
|
"\x80\xdaN\x07?\xf6/\xdf\xddZ\x7f\xe4\xc4i\x90?" +
|
||||||
"\xbaxw\x87 @\xc6\xbf\xe1_\xdf\xd9\xbc\xf1\xb9\xe9" +
|
"\x13^\xbc\xaf]\x10 \xe5\xdd\xf4\xaf\xe7\xb6myq" +
|
||||||
"'\x82o\x82\xed\xac\xe39\xba\xc7\xe3[\x7f\xb47w" +
|
"\xf2)\xff\x1b\x7f;k\x7f\x91\xeeq\xf9\xd6\x1f\x1e\xc8" +
|
||||||
"\xdf\xd0\xef>\xf8\x04\xa8}\x98\xbe\x88\x1fr\xacc\x1e" +
|
"\x1e+\xfe\xce\xc3O\x81\xda\x83\xc9\x8b\xf8!O\xb4\xcf" +
|
||||||
"\x953t\xd1\x9a\xd3\x1dy\x04\xf4\xe7o:\xb7\xe5\x97" +
|
"\xa1r\x96.\xea{\xae=\x8f\x80\xde\xdc-\xdf\xdd\xfe" +
|
||||||
"\x7f\xec<\x05\xeaj\xcc\xf8\x7fw\xef[{\xaezr" +
|
"\xab?\xb4\x9f\x01u\x1d\xa6\xbc\xbf}\xe0\xcd\xfd\xd7~" +
|
||||||
"\xfa\x15\xfe\x04\x91\xf0\xe8<EG\xff\xba\xf3\x19@\xbf" +
|
"s\xf2U\xfe\x04\x91\xf0\xe88MG_\xe8x\x1e\xd0" +
|
||||||
"\xfboVmz\xf0\xed\x0dg\xe8h\xa1\xf9\x0dG\x97" +
|
"\xeb\xfa\xab\xb5[\x1f~k\xf3Y:Zh~\xc3\xfc" +
|
||||||
"\x0c\xa0\xf2\xf8\x12z\xc3\xc9%\xb4\xfa\xa7\xd7l\xf9\xfe" +
|
"\xb2~T\xce,\xa37\x9cZF\xab\x7fr\xfd\xf6\xef" +
|
||||||
"\xf7\x9f\x9d9\xd3\xac\x88@\xab\x87\x96\xaeGej)" +
|
"}\xef\x85\xa9\xb3\xcd\x8a\x08\xb4\xba\xb8|\x13*\x13\xcb" +
|
||||||
"\x7f\xf1RZ}I\x11\x7f\xfe\x83\xeb3\x7f\x1d\xbeK" +
|
"\xf9\x8b\x97\xd3\xea\xabK\xf8\xf3\xef\xafO\xfde\xf0." +
|
||||||
"\xa4Em]\xef\xd2\xe5\xbd]\xb4\xe0\xd6O^\xf8\xe1" +
|
"\x91\x16\xa5;\xdf\xa6\xcb\xbb;i\xc1\x1d\x1f}\xfb\x07" +
|
||||||
"\xe8\xfb?;\x9b\xb6\xd6\xd9.\x81\xac\xf5\x93.zx" +
|
"\xc3\xef\xfe\xec;Ik}\xa7S k\xfdc'=" +
|
||||||
"\xff{\xc3\xdd\xe6\xfb\x87~\xd0\x040?\xe9\xd7]\xeb" +
|
"\xbc\xf7\x9d\xc1.\xe3\xdd\xa3\xdfo\x02\x98\x9ft\xa1s" +
|
||||||
"Q\xe9\xec\xa6\xeb\xda\xba\x9f\x01\xfc\xe8\xa9\xbb\x0f\x17\xdf" +
|
"\x13*\x1d]t]\xba\xeby\xc0\x0f\x9e\xb9\xefx\xe9" +
|
||||||
"Z\xfb\x8a\xda\x87\x99&\x079\xd9\xbd\x0f\x95\x17\xf8\xd2" +
|
"\xcd\x0d\xaf\xaa=\x98j~\xc8\xa9\xae\x83\xa8|\x9b\xd6" +
|
||||||
"3\xddd\xb8\x18\x93\xc6\xc5\xc1;\x1e[\xb6\x13\x953" +
|
"\xf6\x9d\xed\xe2\x18E\xa84-\xe7/\xf9\xf7\x15{P" +
|
||||||
"\xcb8\xa0\xcb8\xa0\xebo\xfd\xd6Cm\xef|\xeb\x95" +
|
"\xb9\xb0\x82\xfb\xd6\x0a\xbe|\xd3\x1d_}4}\xee\xab" +
|
||||||
"f\x90$Z\xf3B\xd6F\xe5\xd5,}\xfc\xfb\xec\x13" +
|
"\xaf6\xc3$\xd1\x9a\x0f3\x16*]Y\xfa\xd8\x91}" +
|
||||||
"\x02\xa0\xdf\xf7\xec\xef\xfd\xd5p\xe5\xcd\x1f7i-\xf0" +
|
"J\x00\xf4z^\xf8\xdd\xbf\x18\xac\xbe\xf1\xe3&\xbd\xe9" +
|
||||||
"\xfb\x97\x7f\xa8\x9c[N\x9f\xce.\xdf\x0b\xe8\xdf}\xf5" +
|
"p\xe5\xc2U\xef)\xa8\xd0\xa7\x8bW\x1d\x00\xf4\xee\xbb" +
|
||||||
"\xdc\xbeM_\x98\x7f\xa3\x19Q\x8e\xc5%\xca<*\xab" +
|
"n\xf6\xe0\xd6O\xcf\xbd\xde\x8c)W\xfcve\x0e\x95" +
|
||||||
"\x15Z}\x95B\xab\x85w\xb4\xde\x83\xff\xfc\xd5\x9f\xa7" +
|
"}|u]\xa1\xd5\xc29\xad\xfb\xc8?\x7f\xf1\xe7\x09" +
|
||||||
"|\xe8\x98\xf2\x0b\x84\x8c\xbfi\xcb\xad;;o\x7f\xeb" +
|
"/\xfa\x85\xf2K\x84\x94\xb7u\xfb\x1d{:\xeez\xf3" +
|
||||||
"\xad\xb4\x0f\xdd\xafp\xacO*\x04\xe5\xf3\xf2C\xcaK" +
|
"\xcd\xa4\x17\xfdT\xe1h\x9fS\x08\xcc\x97\xe4G\x95W" +
|
||||||
"'\xff\xe2m\xbaHj\xc6\xf2ee+*o\xd0E" +
|
"N\xfd\xd9[t\x91\xd4\x8cf:\xb7\x03\x95\xee\x1c}" +
|
||||||
"k^W\xf8\x1bb\xc7oe\xe93\x97\x0e\xa0\xf2\xf2" +
|
"\xbc:\xc7\xdf\x10\xb9~+[_\xf8\xb5~T\xd2+" +
|
||||||
"\xa5\xa4\xd7\xb9KI\xaf\x1b\xb6\x0f\xb1m7\xde\xf2." +
|
"I/\\Iz\xdd\xb4\xab\xc8v\xde|\xdb\xdb \xf7" +
|
||||||
"\xc8}bC\x18\x9f\xa7\x95\xdd+hS\xe7\x0a\x09\x95" +
|
"\x88\x0b\x02\xb9\xb8\xb2\x1f\x15\x95V\xf6mY)\xa1r" +
|
||||||
"\xb3\xf4\xd1\x7f`f\xeb\xab\x1f\x8c\x9c\xfc\xef\x96\xfe|" +
|
"\x91>z\x0fM\xed\xf8\xd1\xf9\xa1S\xff\xdd\xd2\xa3\xcf" +
|
||||||
"r\xc5\x00*g\xf8\x96\xd3+8\xfck\xae\xff\x93\xf7" +
|
"\xd1\x96\x0b|\xcb\xf9\x95\x1c\xfe\xbe\xf5\x7f\xf4\xce\xfc\x9f" +
|
||||||
"\x8e\xfe\xd9\xc8\x07\x0bN\xff\xf8\xb2aT:{\xb9\x0b" +
|
"\x0c\x9d_t\xfa\x17\xba\x07Q\xd9\xd2Mz\x94\xba\xbf" +
|
||||||
"\xf4~M\xb9\xa1\x97\x1f\xfe\xcd\xb5\x9boZ\xf9\xf2\x87" +
|
"\xa4\xccv\xf3\xc3\xbf\xb2a\xdb-k\xfe\xfa\xbd$\x12" +
|
||||||
"i$\xfa{?\xe4\x11\xdeKHL\xdf\xf8__\xfb" +
|
"Z\xf7{<\x9e\xba\x09\x89\xc9\x9b\xff\xebK\x9f~\xe8" +
|
||||||
"\xc2\x03\xff\xf0a\xab\xa8U{W\xa1\xa2\xf1\x13o\xa3" +
|
"\xef\xdfk\x15\xb7\xf3\xddkQ9\xc3O<E\x8b\xdf" +
|
||||||
"\xc5\xef\xaf\xfb\xce\xcf\xfa\xb2}\xbfj\xa5\xe8\x1d\xbd;" +
|
"\xdd\xf8\x8d\x9f\xf5dz\xdeo\xa5\xe8\xdfu\xefA\xe5" +
|
||||||
"Q9Jk\xd7\x1c\xe9\xe5\x8a\x1a\xbf\xf5\xc6\xaeg\xda" +
|
"\x0dZ\xdb\xf7z7W\xb4\xf6\x9b\xaf\xef}\xbe\xed\xad" +
|
||||||
"\xdf\xfe\xa8\xd5\xc9/\xf7\xf5\xa1\xf2z\x1f\x9d\xfc\x93>" +
|
"\x0fZ\x9d\x9c^\xd5\x83\xca\xd5\xab\xe8dy\x15\xa9q" +
|
||||||
"R\xe3\x96_\x1c\xdf[\xf8\xf6\xaf>\"\x10\xc4&\x87" +
|
"\xdb/O\x1c(|\xed\xfd\x0f\x08\x04\xb1\x89\xd3\xd6\xaf" +
|
||||||
"}\xafo+*x9->\xdfG\x91\xb2\xe1\xe97" +
|
"\xda\x81\xca0_\\\\E\xb1\xb2\xf9\xd97\xbe8=" +
|
||||||
"\xbfZ=\xfa\xa3\x8f\x9b\x11\xe3\xd6{\xf2\xf2C\xa8\x9c" +
|
"\xff\xc3\x0f\x9b\x11\xe3\xd6{g\xd5QTp5w\xaa" +
|
||||||
"\xe3\xab\xcf^N\xee\xfd\x9b[\xfe\xf2\x0f\xff\xf6\x8f\xfe" +
|
"U\xc4L\xbf\xb1\xfd\xcf\x7f\xffo\xfe\xe0\x8f?\x02\xf5" +
|
||||||
"\xf4\x13P\xafF)\xb1\xfc\x94(\xa1@\x0e~\x05\xe7" +
|
":\x94b\xcbO\x88\x12\x0a\xe4\xe0\xab9\xbb\x9c_M" +
|
||||||
"\x96\xd3W\x90\xe9\xf6\xbf\x7fl\xec\xc1mO\x7f\x9a\x86" +
|
"\xa6;\xf4\xee\x13#\x0f\xef|\xf6\xe3$\\[z_" +
|
||||||
"\xab\xb3\xffE\x1e\xa4\xfd\xa4\xe7\xce\xa3\xfb\xdd\xb1G\xee" +
|
"\xa6\x05w\xf6\x92\x9e{\xe6\x0f9#\x8f?\xe8\xb5\xf0" +
|
||||||
"\xf7[x\xf3\x9a\x9b\xfa\x87Q)\xf6\xd3\xcd\xa3\xfd{" +
|
"\xe6\xbe\xbbz\x07Qy\xb0\x97n~\xa0\xf7\x00\xac\xf3" +
|
||||||
"a\xb5\xefz\xa6\xc9\x0c\xbb\x9e)\xffv\xf4\xb1|m" +
|
"\x1c\xd70X\xcdj\xa4*\xbf\x15~\xac\xdcP\xd1\x1a" +
|
||||||
"Y\xab\x9b\xf5\x81\xd1Y\xddqusf\x92\xcb\x0b%" +
|
"F\xa3\x7fxF\xb7\x1d\xdd\x98\x1a\xe7\xf2B\xd9\xac\xe9" +
|
||||||
"\xcb\xd0\xcbs%D\xb5\x8b\x94\x92\xfb\x07\x00\x10\xe5K" +
|
"\x95\xd92\xa2\xdaIJ\xc9\xbd\xfd\x00\x88\xf2\xd5;\x00" +
|
||||||
"\xb6\x02\xa0 \xcb\xc3\x00\x05}\xc6\xb4l\xe6Wt\xa7" +
|
"P\x90\xe5A\x80\x82>e\x98\x16\xf3\xaa\xba]1\x0d" +
|
||||||
"l\x99&\x03\xb1\xec\x1e\xd8\xa1\x19\x9aYf\xf1Em" +
|
"\x83\x81Xq\x0e\xef\xd6j\x9aQa\xd1E\xe9\xc5\x17" +
|
||||||
"\x0b/\x1ac\x86a\xddl\xd9Fe\xb3\xad\xcf\xe8\xe6" +
|
"\x8d\xb0Z\xcd\xbc\xd5\xb4j\xd5m\x96>\xa5\x1bC\xa6" +
|
||||||
"\x88eN\xeb3\x00%\xc4x\x9b\xb4p\xdb\x88\xa13" +
|
"1\xa9O\x01\x94\x11\xa3m\xd2\xe2mC5\x9d\x19\xce" +
|
||||||
"\xd3\x9d`\xf6\x1e\xbd\xcc\xae\xf5\x1c\x16\xec\xf3l\xcd\xd5" +
|
"\x18\xb3\xf6\xeb\x15v\x83k3\x7f\x9fki\x8en\x1a" +
|
||||||
"-\xf3\xcaq\xe6x\x86\xeb\x00\xa8\x191\x03\x90A\x00" +
|
"\xd7\x8c2\xdb\xad96\x80\x9a\x12S\x00)\x04\x90\xbb" +
|
||||||
"\xb9{\x00@\xed\x10Q\xed\x11\xb0`\xf3\x05\x98KB" +
|
"\xfa\x01\xd4v\x11\xd5\x9c\x80\x05\x8b/\xc0l\x1c\xd2\x80" +
|
||||||
"\x1a\x10s\x90\xdc\xd9\xbe\xf0\xce\x00\x0b\xba\x93\xd9\xd7z" +
|
"\x98\x85\xf8\xce\xb6\xc5w\xfaX\xd0\x9d\xcc\xba\xc15," +
|
||||||
"\xa6\xcdft\xc7ev \xbe\xb2P\xd2l\xad\xe6\xa4" +
|
"6\xa5\xdb\x0e\xb3|\xf15\x85\xb2fiu;y\xe1" +
|
||||||
"/<\x0e\xa0\xe6DT\xaf\x10\xd0\x9f\xb1\xb52+1" +
|
"\x09\x005+\xa2\xbaZ@o\xca\xd2*\xac\xcc,\xd4" +
|
||||||
"\x1bu\xab\xb2I3\xad\x09\x91\x95\xb1\x0d\x04lK]" +
|
"\xcd\xeaV\xcd0\xc7DV\xc14\x08\x98N\\\xda\xc2" +
|
||||||
"\xda\xc2\x10\xeb4\xdd`\x95\xe0u\xd7\x96\xf3\xfc\xaf\x9a" +
|
"\x10\x1b5\xbd\xc6\xaa\xfe\xebn\xa8\xe4\xf9_5+\xa6" +
|
||||||
"\x133]\xbe\xcf/\xd1\xb6\x02\xa8\xdbET\x0d\x01\xbb" +
|
":=\x8f_\xa2\xed\x00Pw\x89\xa8\xd6\x04\xec\xc2\x8f" +
|
||||||
"\xf1S\xbf\x87\x12\xa5\xac\xef\x03P\xab\"\xaa\xae\x80\xdd" +
|
"\xbd\x1c\xa5JY?\x08\xa0N\x8b\xa8:\x02v\x09\x17" +
|
||||||
"\xc2y\xbf\x87[m\xf7J\x00\xd5\x10Q\x9d\x15\xb0[" +
|
"\xbd\x1c\xb7\xda\xbe5\x00jMDuF\xc0.\xf1#" +
|
||||||
"\xfc\xc4\xef\xa1\x0c#{;\x01TWD\xf5\xa0\x80\xbe" +
|
"/G9Fv\xf7\x00\xa8\x8e\x88\xea\x11\x01=\xdbm" +
|
||||||
"\xe3\xd5\x09S\x07D\xcb\xc6\\\xe2\xf6!:\xac2C" +
|
"\x10\xa66\x88\xa6\x85\xd9\xd8\xed\x03tXu\x8a\x906" +
|
||||||
"H\x9bP`e\x02\x1as\x11\x91\x07\x0b\xa4\x8aU\xc5" +
|
"\xa0\xc0*\x044fC*\xf7\x17HUs\x1a\xb3q" +
|
||||||
"\\\x92y\xc2m6\xdb\xc3l\x87\x95 k[\xb3s" +
|
"\xee\x09\xb6Yl?\xb3lV\x86\x8ce\xce\xccb6" +
|
||||||
"\x98K(\xbd\x09u\xb1\x85\xa5\xe9\xff\xb1\xc2\xe4di" +
|
"\xa6\xf4&\xd4\xc5\x16\x96\xa6\xffG\x0a\xe3\xe3\xe5\x89\xd1" +
|
||||||
"j|\x03y`\x0a\xe0\x95\x89E%\xcf6\xb0\x0b\x04" +
|
"\xcd\xe4\x81\x09\x80\xd7\xc4\x16\x95\\\xab\x86\x9d `g" +
|
||||||
"\xecJ\x1d\xd7\xfdy\x8d\x18\xf9M\xbck\xf1\xfd\xdc\xd3" +
|
"\xe2\xb8\xae+5b\xe87\xd1\xae\xa5\xf7sO\xaf8" +
|
||||||
"\xcb\xee\x95\xa5\xfc\x02\xdb\x93Y\xbaDT/\x13\xd0\xaf" +
|
"\xd7\x94\xf3\x8blOf\xe9\x14Q\xfd\x94\x80^\x83\xbe" +
|
||||||
"\xd3\xb7\xcce \xda\x0e\xe6\x92\x02\xa1\xe9\xf1m\x9f\xf1" +
|
"e\x0e\x03\xd1\xb21\x1b\x97\x08M\x8fO\x7f\xc2\xe3\x87" +
|
||||||
"\xf8\x91\xe0\x96Rx\x8a\xed\xf0\xe8P{\xe2\xcbn\xa7" +
|
"\xfc[\xca\xc1)\x96\xcd\xa3C\xcdE\x97\xddE\x97\x1d" +
|
||||||
"\xcb\xf6\x8b\xa8\xde#\xa0\x8c\x18\xb8\xc0]6\x80z\xa7" +
|
"\x12Q\xbd_@\x19\xd1w\x81{-\x00\xf5\x1e\x11\xd5" +
|
||||||
"\x88\xeaa\x01Q\x08\x1c\xe0\xfeS\x00\xeaa\x11\xd5G" +
|
"\xe3\x02\xa2\xe0;\xc0\x83\xa7\x01\xd4\xe3\"\xaaO\x0a(" +
|
||||||
"\x05\x94E!\xb0\xff\xb1U\x00\xea\xc3\"\xaa\xcf\x0a(" +
|
"\x8b\x82o\xff'\xd6\x02\xa8\x8f\x89\xa8\xbe \xa0\x9c\x12" +
|
||||||
"g\xc4\x1e\xaa\x9c\xe4\xd3\xe4\xbb\xcf\x8a\xa8\xbe$\xa0o" +
|
"sT;\xc9\xcf\x91\xef\xbe \xa2\xfa\x8a\x80\x9e\xe9G" +
|
||||||
"\x05\x91I\xfa\xbb\xd8\x0d\x02v\x03\xfae\xc3\xf2*\xd3" +
|
"&\xe9\xef`\x17\x08\xd8\x05\xe8Uj\xa6[\x9d\xaci" +
|
||||||
"\x86\x06y\x9bU\x8akc\xb9\xe9\xd5J6\xdb\xa3\xa3" +
|
"\x90\xb7X\xb5\xb4!\x92\x1bn\xbdl\xb1\xfd:\x9a\xae" +
|
||||||
"\xe59C\xae\xcbjR\xddu\xb0\x1d\x04l\x07\xcc\xba" +
|
"]t\x1cV\x97\x1a\x8e\x8dm `\x1b`\xc6\xd1\xa6" +
|
||||||
"\xda\x8c\x83\xcb\x00K\"b.\xc9\xbd\x80$\x8c\xcfD" +
|
"l\\\x01X\x16\x11\xb3q\xee\x05$at&Z\xac" +
|
||||||
"\x9bU\xb60\xdb\xd1E\xcb\\`\xd4\x160\x8d\x87\xfe" +
|
"\xba\x9dY\xb6.\x9a\xc6\"\xa3\xb6\x80i4\xf0/\xf2" +
|
||||||
"E\xde\x15\x86\x8ae\xeb\xd2\x8cn\xaa]b\xe6\x0a\xdf" +
|
"\xae TLK\x97\xa6tC\xed\x14S\xab=/\xc0" +
|
||||||
"\x0f1\x19\xa5\xa7\x0e\x8a\xa8n\x10\xb0\x1f?%1\xc1" +
|
"d\x98\x9e: \xa2\xbaY\xc0^\xfc\x98\xc4\x04Ki" +
|
||||||
"R\x1c\x07P\xc7DT'\x05\xec\x17\xce\x93\x98\x80Q" +
|
"\x14@\x1d\x11Q\x1d\x17\xb0W\xb8Hb\x02F%X" +
|
||||||
"\x09\xd6\x92\x88\xea6\x01\xb3U\xd7\xadc.\xa1\xe7\xd0" +
|
"\xcb\"\xaa;\x05\xccL;N\x03\xb31=\x07\xb6;" +
|
||||||
"v{\xd9\x0e\xc7*\xefb\x80D&1\xf1\x87\xdfV" +
|
"\xc0v\xdbfe/\x03$2\x89\x88?\xf8v: " +
|
||||||
"Cr\x03\xd1\xa8`.\xa9\x8a/\xc2\xeb\xb9\xcd\x0b\xee" +
|
"7\x10kU\xcc\xc6u\xf1ex=\xb7y\xc1\x19\xb6" +
|
||||||
"\xa8m[6\xe7\xdd\xd8\xda\xa3_N\x1e\x11\x19\xbb\xb8" +
|
",\xd3\xe2\xbc\x1bY{\xf8s\xf1#Bc\x97v\xc4" +
|
||||||
"5y\x81,\x0c\x06\xcfRw$\xfa\xe7\xcb\x9a\xe7\xb0" +
|
"/\x90\x85\x01\xffY\xea\xeeX\xff|Esm\x16a" +
|
||||||
"\x18K\x9b\xb9\xf6\xdc\xd0\xb4\x0b\"\xb3c\x16r\xaa\x96" +
|
"i1\xc7\x9a-N: 2+b!{\xdatk" +
|
||||||
"gT\xc6\x19H\xae=\x87\x08\x02\xe2\xe2\xdc\xb4\xd6\x1a" +
|
"\xd5Q\x06\x92c\xcd\"\x82\x80\xb847m0G\x12" +
|
||||||
"KA\x1exeJO\xd2i\xad\x88j)\xd1s#" +
|
"\x90\xfb^\x99\xd0\x93t\xda \xa2Z\x8e\xf5\xdcB\xb2" +
|
||||||
"\xc96\x88\xa8\xdeBz\x86\xf0O\x11\xfc\x93\"\xaau" +
|
"\xcd\"\xaa\xb7\x91\x9e\x01\xfc\x13\x04\xff\xb8\x88jC@" +
|
||||||
"\x01}\x83\xc2\xd1\x1c\xb3@t\xdcX\xdd@X\xb2\xb8" +
|
"\xafF\xe1h\x8c\x98 \xdaN\xa4\xae/,\x9b\xdc\x01" +
|
||||||
"\x03J \xa0\x04\xe8{u\xc7\xb5\x99V\x03\x8c=\x8a" +
|
"%\x10P\x02\xf4\xdc\x86\xedXL\xab\x03F\x1eE\xeb" +
|
||||||
"\xd6/\xfb\x1c$\xde\x14\xfd%-\xcb\xc3\xb8\xf5\x1b\xe2" +
|
"W\\\x01\x897E\x7fY\xcb\xf00n\xfd\x86(\xb2" +
|
||||||
"\xc8\xda\xb8>\xfd\x880\xb4\xa6\x86\x13\xb0[\x07L\xd5" +
|
"\xb6lJ>\"\x08\xad\x89\xc1\x18\xec\xd6\x013m\xda" +
|
||||||
"r\\S\xab1\x00\x88\x1ev\xc0\xaa\x13\x8b\x12)\xc4" +
|
"\x8e\xa1\xd5\x19\x00\x84\x0f;l6\x88E\x89\x14\xa2\xaa" +
|
||||||
"Uk\x93o|\xfe\xdc\x17\xe4\xa1\x86\xccw*\x95\x88" +
|
"\xb5\xc97\xae<\xf7\xf9yhA\xe6;\x9dHD\x95" +
|
||||||
"\xca\xe1n\xe4\xdbG,S\x9a\xd6g0\x97\x94yM" +
|
"`7\xf2\xedC\xa6!M\xeaS\x98\x8d\xcb\xbc&\x05" +
|
||||||
"\x0a\xb4\xb0\xfb\x90\xe7V\x99\xe9\xeae~\xe1\x02\xbb\xaf" +
|
"Z\xd8\xbd\xe8:\xd3\xccp\xf4\x0a\xbfp\x91\xdd\xd7\xc4" +
|
||||||
"L\xfc3\xc6\xac\xf8\xe5\x14\x90\x11f\x1bw$@J" +
|
"\xfe\x19aV\xfa\\\x02\xc8\x10\xb3-\xbbc \xa5\xbd" +
|
||||||
"\xbb\xd8\\\x04K\x9e\xd54=a\xf3\x10\xcd!\x90\xbe" +
|
"l6\x84%\xcf\xea\x9a\x1e\xb3y\x80f\x11\xa4/\xc7" +
|
||||||
"\x9e\xacY\xb4x\x09\xb3T\x90\xa3\x0a\x01<\xa4d." +
|
"k\x96,^\x82,\xe5\xe7\xa8\x82\x0fO\x13e\xce\x01" +
|
||||||
"VR\x9b\x07P+\x81\xcf\xc5J\xd6\x1e\x02P\xeb\"" +
|
"\xa8GDT\x8f%\x94|\xe0Q\x00\xf5\x98\x88\xea\xe3" +
|
||||||
"\xaa\xfbSJ\xce\x0d'\xe9Q\x16\xc5\x80\x1an'D" +
|
"\x09%\xe7\x07\x93\x9c)\x06\x9cI\x88>)\xa2\xfa\xb4" +
|
||||||
"\x0f\x8a\xa8\xde'p\xc6\x1b\x1b\x1a\xb1L\x0c/t\x00" +
|
"\x80\x98\xf2)\xf3\x0cQ\xe6\xd3\"\xaa/\x09\x9c\x05G" +
|
||||||
"\"\xbe\xf3\xabL\xb3\xdd\x1dLC\xb7h\xba\xcc\xde\xa3" +
|
"\x8aC\xa6\x81\x81\x126@\xc8\x81\xde4\xd3,g7" +
|
||||||
"\xa1\x11\xc5\xdb\x01W\xaf1\xcbs\xe3\xf8\xabi\xb3<" +
|
"\xd3\xd0)\x19\x0e\xb3\xf6kX\x0bc\xf0\xb0\xa3\xd7\x99" +
|
||||||
"\xdbce,\xd8%i\xae\x83\x9d `\xe7\xe2\xefm" +
|
"\xe9:QL\xd6\xb5\x19^\x01`u\xc4\xdf%i\x8e" +
|
||||||
"\xa4\xbfl\xf4\xdaT\x82\xd8\x97$\x08\xfa\x974\x9d\xf2" +
|
"\x8d\x1d `\x07\x85\x80\xcd\xac!\x8bU\x91\xac\xa1\xd5" +
|
||||||
"]\x03 \xf0H$\xd6\xaf\x0d'e\x03\xcf\x0fmT" +
|
"\xca\x9a\xe8L_\x0e@\x0b\xf92\xd3\x02\x9e\x83qF" +
|
||||||
"5<\x94\x02\x80\xf2C;\x9dx<\x05@\xa0\xcf\x98" +
|
"\xa1\x7fq\x9f*\xdf\xdb\x0f\x02\x0f]zs}0\xae" +
|
||||||
"\x05\x85\xc0\xc3#\x1b\x15\x02\xcb\x1d \xb6\xd1Y\xf2\x94" +
|
"3xBIS\x99\xf1h\\P\xf0\x84\xd2F'\x9e" +
|
||||||
"0m\xeah\x99\x93\x1c\x03L@([\xb5\xba\xcd\x1c" +
|
"\x88\x01\x0fT\x1b1\xa1\xe0\x87D\xa8s\xc17\xf5a" +
|
||||||
"\x07u\xcbT=\xcd\xd0Ew\xee\xe20\xa0P\x0eB" +
|
"\xa2'\x9d\xc5\xef\x0c\xf2\xac\x8e\xa61\xce\x01\xc2\x18\xa1" +
|
||||||
"`s=\xcf\xed@ \\\x17\x81\xa0\x0c\xe1z\x80\x89" +
|
"\x8aYoX\xcc\xb6Q7\x0d\xd5\xd5j\xba\xe8\xccF" +
|
||||||
"A\x14qb\x03&VW\x8a8\x0c0\xb1\x96\xe4%" +
|
"\x1b\x97\xc4\x80b\xdf\x8f\x99m\x8d<7\x12\x81pc" +
|
||||||
"L\x0c\xafl\xc4>\x80\x891\x92O\xa2\x80\x18\x98^" +
|
"\x08\x82R\xc4M\x00c\x03(\xe2\xd8f\x8c\xddD)" +
|
||||||
"Q\xf1)\x80\x89I\x12o\xc7$c*\xb7\xf1\xe3\xb7" +
|
"\xe1 \xc0\xd8\x06\x92\x971\xf6\x14e\x0b\xf6\x00\x8c\x8d" +
|
||||||
"\x91\xbcJ\xf2\xb6\x0c\x87Oa\xb8\x0a`b;\xc9\xf7" +
|
"\x90|\x1c\x05D\xdfW\x14\x15\x9f\x01\x18\x1b'\xf1." +
|
||||||
"\x93\xbc]\xe0\x08*s\xb8\x13`b\x96\xe4w\x92\\" +
|
"\x8cS\xacr'?~'\xc9\xa7I\x9eNq\xf8\x14" +
|
||||||
"j\xeb\xa1r^\xb9\x03m\x80\x89\x83$\xbf\x8f\xe4\x1d" +
|
"\x86k\x01\xc6v\x91\xfc\x10\xc9\xdb\x04\x8e\xa02\x8b{" +
|
||||||
"\x97\xf5`\x07\x80r/\x97\xdfC\xf2\x87I\xde\xd9\xdb" +
|
"\x00\xc6fH~\x0f\xc9\xa5t\x8e\xea\x7f\xe5n\xb4\x00" +
|
||||||
"\x83\x9d\x00\xca\x11<\x040q\x98\xe4\x8f\x92|\x09\xf6" +
|
"\xc6\x8e\x90\xfc\x18\xc9\xdb?\x95\xc3v\xaa\xf1\xb9\xfc~" +
|
||||||
"\xe0\x12\x00\xe5\x18\x1e\x07\x98x\x94\xe4\xdf%\xf9\xd2\xf6" +
|
"\x92?F\xf2\x8e\xee\x1cv\x00(\x8f\xe0Q\x80\xb1\xe3" +
|
||||||
"\x1e\\\x0a\xa0<\xce\xf59A\xf2\xa71\xe6\x83b%" +
|
"$\x7f\x92\xe4\xcb0\x87\xcb\x00\x94'\xf0\x04\xc0\xd8\x93" +
|
||||||
"MK\xe4Nz\x92zE\xcb\x89\xc3\x8e\x85\x8d\x01\x06" +
|
"$\x7f\x9a\xe4\xcb\xdbr\xb8\x1c@9\xc3\xf59I\xf2" +
|
||||||
"\x9cY\xb2\xb2\xd4\x19`6\x19\x0e\x01b\x16\xd0\xaf[" +
|
"g1\"\x90R5\xc9c\xe4Nz\x9c\xabE\xd3\x8e" +
|
||||||
"\x96\xb1\xa9\x91\xee.\x94\xfdC\xb7\x80\xace\x16+q" +
|
"\xdc\x90\x05\x9d\x04\xfa$[63\xd4J`&\x9e'" +
|
||||||
"\x08\x05N\xb4\xc1\x82|Y3\x8a\xf5X\x13\xdd\x19\xf2" +
|
"\x01b\x06\xd0k\x98fm\xebB~\xbcT\xb9\x10\xb8" +
|
||||||
"\\\xcb\xabC\xbe\xa2\xb9\xac\x12',\xdb3\xd7\xd9V" +
|
"\x05dL\xa3T\x8d\xe2\xcbw\xa2\xcd&\xe4+Z\xad" +
|
||||||
"m\x12\x99]\xd3M\xcd\x80\xf8\x9b\xc5|+\xebyz" +
|
"\xd4\x884\xd1\xed\xa2\xeb\x98n\x03\xf2U\xcda\xd5(" +
|
||||||
"e\x01\xb9\x08\xcd\x8e\x96\xaf\x0fLj<\xba:\xe2\xe8" +
|
"\xc3Y\xae\xb1\xd12\xeb\xe3\xc8\xac\xbanh5\x88\xbe" +
|
||||||
"\xba\x8a\xaa\x8a+ET\xafKq\xc9j\"\xbc/\x89" +
|
"Y\xca\xb72\xae\xabW\x17\x05\x9b\xd0\xech\xf9F\xff" +
|
||||||
"\xa8~E\xc0l:(\xf2{4\xc3c\x17S\xd5L" +
|
"\xb8\xc6\xa3\xab=\x8a\xaek\xa9\x0c\xb9FD\xf5\xc6\x04" +
|
||||||
"51{P\x9c\x06t\x9b\xba}8\xb9=\xbe\x9cj" +
|
"\xf9\xac#\x86\xfc\xac\x88\xea\xe7\x05\xcc$\x83\"\xbf_" +
|
||||||
"\xbfkDT\xc7\x04<\xe0x\xe52=:Ba:" +
|
"\xab\xb9\xecr\xca\xa0\x89\xa6T\xe0W\xb3>?'n" +
|
||||||
"\xec( Og\xa7\xec\x11O\x11B{\\l\x16\x9d" +
|
"\x1f\x8co\x8f.\xa7b\xf1z\x11\xd5\x11\x01\x0f\xdbn" +
|
||||||
"an\xf0\xa9hN[\x94~$\xad\xe6\xfc\x1fw\x8f" +
|
"\xa5B\x8f\x0eQ\x98\x0cZ\x10\xc8\xd3\xd9\x09{Dc" +
|
||||||
"3'K\x15\xf8\x05\xfb\xb6x.p\xe1t569" +
|
"\x87\xc0\x1e\x97\x9bv\xa7\x98\xe3\x7f*\x19\x93&\xe5+" +
|
||||||
"YJ\x9aK1 G\xce\x0b\x98\xea\xbe\x95!\xdc\x0a" +
|
"I\xab\xdb\xff\xc7\xdd\xa3\xcc\xceP\xc9~\xc9F/\x1a" +
|
||||||
"\x02\xb7\x1fE\xffj\x1e\x9e\xd7P\x98\xdc\xc8Y!\x17" +
|
"$\\:\xbf\x8d\x8c\x8f\x97\xe3nT\xf4\xc9\x91\xf3\x02" +
|
||||||
"\x84\xff\x0d<\x0c\xbfB\xf2A\x0cY\x92\xc2\xff\x0f\xf0" +
|
"&\xdau\xa5\x88;@\xe0\xf6\xa3\xe8_\xc7\xc3\xf3z" +
|
||||||
"T\x03\xbbd\xe4 \xfc\x8b8\x9ef\x11\xb9\x0d\x83\xf0" +
|
"\x0a\x93\x9b9+d\xfd\xf0\xbf\x89\x87\xe1\xe7I>\x80" +
|
||||||
"W\xf9\xf9%\x92o\x8bh\x81\xc2\xff\x1b8\xdf@#" +
|
"\x01KR\xf8\x7f\x01O/`\x97\x94\xec\x87\x7f\x09G" +
|
||||||
"\x92\x18\x84?\xe3\xe1\\%\xb9\xcbi!\x13\x84\xffn" +
|
"\x93,\"\xa7\xd1\x0f\x7f\x95\x9f_&\xf9\xce\x90\x16(" +
|
||||||
"|\x0e`\xc2%\xf9AN\x0bmA\xf8\xdf\x8e/6" +
|
"\xfco\xc7\xb9\x054\"\x89~\xf83\x1e\xce\xd3$w" +
|
||||||
"\xd0\xc8\x920\xfc\xef\xe5\xeb\xef#\xf9#\x9c\x16\x96\xf7" +
|
"8-\xa4\xfc\xf0\xdf\x87/\x02\x8c9$?\xc2i!" +
|
||||||
"`\x17\x80r\x94\xd3\xc8\xc3$?\x81q\x093T\x01" +
|
"\xed\x87\xff]\xf8\xf2\x02\x1aY\x16\x84\xff\x03|\xfd1" +
|
||||||
"\xb1b\xfbn\xb9\xfeu\xc6\xeaC\x905\xf4=,\xe6" +
|
"\x92?\xcei\xe1\xaa\x1cv\x02(\xf3\x9cF\x1e#\xf9" +
|
||||||
"\xea\x8a\xae\x19k=\xcd\x80\xfc\x84\xab\x95w%%\xa3" +
|
"I\x8cj\x9eb\x15\xc4\xaa\xe59\x95\xc6\x97\x19k\x14" +
|
||||||
"\xe1\x8cif\xc5\xc1\xaa\xb6\x8b\x11\xc3K\xe94\xe7\x1a" +
|
"!S\xd3\xf7\xb3\x88\xab\xab\xbaV\xdb\xe0j5\xc8\x8f" +
|
||||||
"\xce\x16f\xeb\xd3\x80I\x91\x19\xa7\xf8l\xc9\xb2\x9a3" +
|
"9Zeo\\c\xd6\xec\x11\xcd\xa8\xda8\xad\xede" +
|
||||||
"?\xafU\x98\x1d\x90I\xfc]M\x9b-V\x0c6\x82" +
|
"\xc4\xf0R2\x07:5{;\xb3\xf4I\xc0\xb8*\x8d" +
|
||||||
"Q\xa2\x17\xcd$\xc3\xe8\xf4\x8de\x9a\x18d\xe4I=" +
|
"j\x82L\xd94\x9bK\x05^\xdc0\xcb'\x93\xe8\xbb" +
|
||||||
"\xdf\x98j\xeba\xd9\x1a\xa5\xec\xc9BS.f\xb3u" +
|
"\xba6S\xaa\xd6\xd8\x10\x86\x95\x81h\xc4\x19F\xa7o" +
|
||||||
"VvG,4]\xdd\xf4\xd8\x82\x03\xcaU\xcf\xdc\xc5" +
|
"L\xc3@?]\x8f\xeb\xf9\x85y\xb8\x11\xd4\xb9a>" +
|
||||||
"*\xa3h\x96\xad\x8an\xce\xc0\x82zY\xfc\xac^>" +
|
"\x1f/4%j6\xd3`\x15g\xc8D\xc3\xd1\x0d\x97" +
|
||||||
"U\x8ft\x84N\x18\x8f\xb1\xe5\xab\x06B\x1f\xa4t," +
|
"-:\xa02\xed\x1a{Yu\x18\x8d\x8aY\xd5\x8d)" +
|
||||||
"\x0f$Md\xa1\xccw\x15l\xa69-\x9a\"\xf1\xb3" +
|
"XT`\x8b\x9f\xd4\xfc'\x0a\x98\xf6\xc0\x09\xa3\xc9\xb7" +
|
||||||
"\xa2\xac\x10\x04WP\xfd\xb4\x01\xc4s^\x8cFo\xf2" +
|
"|m\x7f\xe0\x83\x94\x8e\xe5\xfe\xb8\xeb,T\xf8\xae\x82" +
|
||||||
"\xee} \xc8\xba\x84\xc9\xc4\x12\xa3\x01\xa5|\x9b\x0d\x82" +
|
"\xc54\xbbE\x17%~R\x94\x15\xfc\xe0\xa2\xdb\xb2b" +
|
||||||
"<%\xa1\x10\xcf\xe21\x1a\x8d\xcb\xc5y\x10\xe4Q\x09" +
|
"\x1a \x1a\x0dc8\xab\x93\xf7\x1d\x04A\xd6%\x8cG" +
|
||||||
"\xc5x&\x8e\xd1|J\xbei\x18\x04y\xb5\xe4G\x15" +
|
"\x9c\x18N4\xe5;-\x10\xe4\x09\x09\x85h|\x8f\xe1" +
|
||||||
"6\x14\x02u\x06\xd1\x8f\x02\x1e\xf2<\xe4\x07\xd1\x8f\xda" +
|
"4].\xcd\x81 \x0fK(Fct\x0c\x07Z\xf2" +
|
||||||
"p\x8c*q\x80A<\x10\xa6\x83AL\x8f\x82\xc4\xcf" +
|
"-\x83 \xc8\xeb$/,\xc9\xa1\xe0\xab3\x80^\x18" +
|
||||||
"*\x87S\xa8\xa6\xea\x1e\xe2\xc6Y\x11\xd5;\x13n\xbc" +
|
"\xf0\x90\xe7!?\x80^\xd8\xb7cX\xba\x03\x0c\xe0\xe1" +
|
||||||
"c>\xe9\x8b\xe3\x16\xe4\xfe\xa7Z5\xc6\x87\x00\xd4G" +
|
" \x1d\x0c`rv$~R\xfd\xdc\xba,$n\x9c" +
|
||||||
"DT\x9fO5\xc6g\xa8\xf2{^D\xf5\xa7B\x92" +
|
"\x11Q\xbd'\xe6\xc6\xbb\xe7\xe2F:\xeaY\x1e|\xa6" +
|
||||||
"'#\xb7\x8b\xa6'h\xd9QO\xb4\xc8\x10%t\xce" +
|
"U'}\x14@}\xdc\xaf\x00\xa3N\xfa,\x95\x8a/" +
|
||||||
"\xb0bk\x1e\xa5\xf8\x15\xab\xca+:\x0c\x8er a" +
|
"\x89\xa8\xfeD\x88\xf3d\xe8v\xe1\xb8\x05M+l\xa2" +
|
||||||
"\xea\xf4|eYj\xbe\x82Q7&5\x10{z\xda" +
|
"\x96\x98\xba\x04\xce\x19Tl\xcd\xb3\x17\xafjN\xf3\x8a" +
|
||||||
"\xb2lq\xael\xe8- \x18\xb7\x90\xd7D\xc3\x7f\x8c" +
|
"\x0e\xfd\xa3l\x88\x99:9\x90Y\x91\x18\xc8`\xd8\xbe" +
|
||||||
"~\xb3\x91e\xb2~\xb7\xe4G\xfd\x07Fi\x8a\x8c\x97" +
|
"I\x0b\x88=9\x9eY\xb14W.hF\xc0\x9f\xcf" +
|
||||||
"6\xd9\xe7l\xc2\xc6Y\xde\xb9\x98\x0c\x10\x0d{/\xdc" +
|
"\x90\xd7\x84\xbf\x17`\xf83\x8f,\x93\xf5\xbb$/l" +
|
||||||
"K\x07\xf7d\xc9\xd9\x9a\xe6G;SC\x1a\xc3\x0a\xdb" +
|
"X0LSd\xbc\xa4\xc9\xae\xb0k\x1bey\xfbr" +
|
||||||
"\x99\xec\xa6T\xb6^\x0c\xab@\xe1\xa8\xf0\xcc\xd2\xe6&" +
|
"2@8\x1d\xbet\xf3\xed\xdf\x93!gk\x1a8\xed" +
|
||||||
"\xf7[\x99\xb8_\\\x18\xdc\xb125\xac\x89\x9a\x8c\xbb" +
|
"ILujf\xd0\xffd\xb6&\xb2\xf5RX\xf9\x0a" +
|
||||||
"\xd6\x87Ny\".4\xe5\xc7\xc8QO\x88\xa8>\x9d" +
|
"\x87\x85g\x8667\xb9\xdf\x9a\xd8\xfd\xa2\xc2\xe0\xee5" +
|
||||||
"r\xbf'i\xe1w\x03\x9f\x94\x98mGz6\x8c\xbf" +
|
"\x89\xe9N\xd8\x95\xdc\xbb)p\xca\x93Q\xa1)\x7f\x9d" +
|
||||||
"\x0ckf\x83n2\x87J\xaf\xa6\xce\xb8\xce\xec\x9af" +
|
"\x1c\xf5\xa4\x88\xea\xb3\x09\xf7\xfb\xe6\xa6\xb8+\x91\x98e" +
|
||||||
"2\x13]\"#\xcf&Fmd\xae\xe2\xdaT\xc5\xb6" +
|
"\x85z.\x98\x97\xd5\xcc\xa9\xcd\xba\xc1l*\xbd\x9aZ" +
|
||||||
"\x18\xacS\xa6>[\xd2D\xb7\xda\x04\xea\xaa\xc4X\xd9" +
|
"\xe9\x06\xb3\xea\x9a\xc1\x0ct\x88\x8c\\\x8b\x18u!s" +
|
||||||
"\xba\xe6V/\x06\xca\x890p\x82\xb8\x09St\xaa\xa5" +
|
"\x956$*\xb6\xa5`\x9d0\xf4\x19\xde\xa24\x81\xba" +
|
||||||
"<\x95\x1aoD@\xaa/\x86c\x83\xed) o\xa3" +
|
"66V\xa6\xa1]^\x073\x16\x04\x8e\x1f7A\x8a" +
|
||||||
"\x96r\x9b\x88jU@_\xf3\\k\xaa^\xd1\xd0e" +
|
"N\xf4\xa0\xa7\x13\xf3\x90\x10H\xf5\xe5`\xce\xb0+\x01" +
|
||||||
"\xebl\xb6\xdbc\x92Y\x9eK\xda-\xeaJ\xca\xce\x14" +
|
"\xe4\x9d\xd4\x83\xee\x14Q\x9d\x16\xd0\xd3\\\xc7\x9chT" +
|
||||||
"\xd6\xa9~\\g\xb3\xc2n\x8f\xa5\x17D#[\x90t" +
|
"5t\xd8F\x8b\xeds\x99dTf\xe3^\x8c\xba\x92" +
|
||||||
"\xab\xb2`V\xdb\xa2`\xbb\x99\xed\x98\xb0\xca\xbb\x98\xdb" +
|
"\x8a=\x81\x0d\xaa\x1f7Z\xac\xb0\xcfe\xc9\x05\xe1\x8c" +
|
||||||
"0\xcanj<W&\x0aF/a\xe3\xa9f4\xa2" +
|
"\x17$\xdd\xac.\x1a\xee\xb6(\xd8ne\xbb\xc7\xcc\xca" +
|
||||||
"\xa4\xda\xcedX\x1bS\x927\x9fxT\xe3\xc4\xf3\xff" +
|
"^\xe6,\x98}\xfb\xd4\x1b>E[\x13+\x18\xbe\x84" +
|
||||||
"'\xa9.6\xa8o\xa8\xa2\x82\x09\xdb\x8c\x9e7\x87*" +
|
"\x8d\x02\xa8U\x7fb\x12QR}O<\xdd\x8d(\xc9" +
|
||||||
"\x15\x9b\x12Y4\x90NW\xc3\xc9@z\xf5\xaaT9" +
|
"\x9d\x8b=j\xe1\x88\xf4\xff'\xa9.5\xd9_PE" +
|
||||||
"\x1c\xce\xd2\xe2\x1fd\x83\x10\xcez\xa6>\x8b\xb9\xe4w" +
|
"\xf9#\xb9)=o\x14\xabU\x8b\x12Y8\xc1NV" +
|
||||||
"\x99\x0b\x8fG[\x0ec\xc7\x0b\xec\xa2\x08$\xf9\xad\xe4" +
|
"\xc3\xf1\x04{\xdd\xdaD9\x1c\x0c\xdf\xa2\xdfp\xfd\x10" +
|
||||||
"\xc2%d8M\x08+\xf0\xa6\x02|e\xab\xf2\x7fk" +
|
"\xce\xb8\x86>\x83\xd9\xf8\x87\x9cK\xcfS[NoG" +
|
||||||
"X\x81\xdf\x18\xc6i.\xf9\xf16\xbc\xce\x09\xab^\x10" +
|
"\x0b\xec\xb2\x08$\xfeq\xe5\xd2%d0~\x08*\xf0" +
|
||||||
"\xa7\xad\x85\x05\xed\xff\x06\x00\x00\xff\xff\xa8\xfcvR"
|
"\xa6\x02|M\xab\xf2\x7fGP\x81\xdf\x1c\xc4i6\xfe" +
|
||||||
|
"\xbd7\xb8\xce\x0e\xaa^\x10'\xcd\xc5\x05\xed\xff\x06\x00" +
|
||||||
|
"\x00\xff\xff\xb0\x8e\x80\xdd"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
schemas.Register(schema_db8274f9144abc7e,
|
schemas.Register(schema_db8274f9144abc7e,
|
||||||
|
|
Loading…
Reference in New Issue