TUN-5488: Close session after it's idle for a period defined by registerUdpSession RPC
This commit is contained in:
parent
9bc59bc78c
commit
73a265f2fc
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/lucas-clemente/quic-go"
|
||||||
|
@ -167,7 +168,7 @@ func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) er
|
||||||
return rpcStream.Serve(q, q.logger)
|
return rpcStream.Serve(q, q.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16) error {
|
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error {
|
||||||
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
|
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
|
||||||
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
|
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
|
||||||
originProxy, err := ingress.DialUDP(dstIP, dstPort)
|
originProxy, err := ingress.DialUDP(dstIP, dstPort)
|
||||||
|
@ -182,7 +183,7 @@ func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer q.sessionManager.UnregisterSession(q.session.Context(), sessionID)
|
defer q.sessionManager.UnregisterSession(q.session.Context(), sessionID)
|
||||||
if err := session.Serve(q.session.Context()); err != nil {
|
if err := session.Serve(q.session.Context(), closeAfterIdleHint); err != nil {
|
||||||
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).Msg("session terminated")
|
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).Msg("session terminated")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -127,7 +127,7 @@ func (m *manager) sendToSession(datagram *newDatagram) {
|
||||||
}
|
}
|
||||||
// session writes to destination over a connected UDP socket, which should not be blocking, so this call doesn't
|
// session writes to destination over a connected UDP socket, which should not be blocking, so this call doesn't
|
||||||
// need to run in another go routine
|
// need to run in another go routine
|
||||||
_, err := session.writeToDst(datagram.payload)
|
_, err := session.transportToDst(datagram.payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.log.Err(err).Str("sessionID", datagram.sessionID.String()).Msg("Failed to write payload to session")
|
m.log.Err(err).Str("sessionID", datagram.sessionID.String()).Msg("Failed to write payload to session")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -21,15 +22,15 @@ func TestManagerServe(t *testing.T) {
|
||||||
)
|
)
|
||||||
log := zerolog.Nop()
|
log := zerolog.Nop()
|
||||||
transport := &mockQUICTransport{
|
transport := &mockQUICTransport{
|
||||||
reqChan: newDatagramChannel(),
|
reqChan: newDatagramChannel(1),
|
||||||
respChan: newDatagramChannel(),
|
respChan: newDatagramChannel(1),
|
||||||
}
|
}
|
||||||
mg := NewManager(transport, &log)
|
mg := NewManager(transport, &log)
|
||||||
|
|
||||||
eyeballTracker := make(map[uuid.UUID]*datagramChannel)
|
eyeballTracker := make(map[uuid.UUID]*datagramChannel)
|
||||||
for i := 0; i < sessions; i++ {
|
for i := 0; i < sessions; i++ {
|
||||||
sessionID := uuid.New()
|
sessionID := uuid.New()
|
||||||
eyeballTracker[sessionID] = newDatagramChannel()
|
eyeballTracker[sessionID] = newDatagramChannel(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
@ -88,7 +89,7 @@ func TestManagerServe(t *testing.T) {
|
||||||
|
|
||||||
sessionDone := make(chan struct{})
|
sessionDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
session.Serve(ctx)
|
session.Serve(ctx, time.Minute*2)
|
||||||
close(sessionDone)
|
close(sessionDone)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -179,9 +180,9 @@ type datagramChannel struct {
|
||||||
closedChan chan struct{}
|
closedChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDatagramChannel() *datagramChannel {
|
func newDatagramChannel(capacity uint) *datagramChannel {
|
||||||
return &datagramChannel{
|
return &datagramChannel{
|
||||||
datagramChan: make(chan *newDatagram, 1),
|
datagramChan: make(chan *newDatagram, capacity),
|
||||||
closedChan: make(chan struct{}),
|
closedChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,15 @@ package datagramsession
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultCloseIdleAfter = time.Second * 210
|
||||||
|
)
|
||||||
|
|
||||||
// Each Session is a bidirectional pipe of datagrams between transport and dstConn
|
// Each Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||||
// Currently the only implementation of transport is quic DatagramMuxer
|
// Currently the only implementation of transport is quic DatagramMuxer
|
||||||
// Destination can be a connection with origin or with eyeball
|
// Destination can be a connection with origin or with eyeball
|
||||||
|
@ -22,7 +27,9 @@ type Session struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
transport transport
|
transport transport
|
||||||
dstConn io.ReadWriteCloser
|
dstConn io.ReadWriteCloser
|
||||||
doneChan chan struct{}
|
// activeAtChan is used to communicate the last read/write time
|
||||||
|
activeAtChan chan time.Time
|
||||||
|
doneChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session {
|
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session {
|
||||||
|
@ -30,41 +37,81 @@ func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *
|
||||||
id: id,
|
id: id,
|
||||||
transport: transport,
|
transport: transport,
|
||||||
dstConn: dstConn,
|
dstConn: dstConn,
|
||||||
doneChan: make(chan struct{}),
|
// activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will
|
||||||
|
// drop instead of blocking because last active time only needs to be an approximation
|
||||||
|
activeAtChan: make(chan time.Time, 2),
|
||||||
|
doneChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Serve(ctx context.Context) error {
|
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) error {
|
||||||
serveCtx, cancel := context.WithCancel(ctx)
|
serveCtx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go func() {
|
go s.waitForCloseCondition(serveCtx, closeAfterIdle)
|
||||||
select {
|
|
||||||
case <-serveCtx.Done():
|
|
||||||
case <-s.doneChan:
|
|
||||||
}
|
|
||||||
s.dstConn.Close()
|
|
||||||
}()
|
|
||||||
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||||
// This makes it safe to share readBuffer between iterations
|
// This makes it safe to share readBuffer between iterations
|
||||||
readBuffer := make([]byte, 1280)
|
readBuffer := make([]byte, s.transport.MTU())
|
||||||
for {
|
for {
|
||||||
// TODO: TUN-5303: origin proxy should determine the buffer size
|
if err := s.dstToTransport(readBuffer); err != nil {
|
||||||
n, err := s.dstConn.Read(readBuffer)
|
|
||||||
if n > 0 {
|
|
||||||
if err := s.transport.SendTo(s.id, readBuffer[:n]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) writeToDst(payload []byte) (int, error) {
|
func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) {
|
||||||
|
if closeAfterIdle == 0 {
|
||||||
|
// provide deafult is caller doesn't specify one
|
||||||
|
closeAfterIdle = defaultCloseIdleAfter
|
||||||
|
}
|
||||||
|
// Closing dstConn cancels read so Serve function can return
|
||||||
|
defer s.dstConn.Close()
|
||||||
|
|
||||||
|
checkIdleFreq := closeAfterIdle / 8
|
||||||
|
checkIdleTicker := time.NewTicker(checkIdleFreq)
|
||||||
|
defer checkIdleTicker.Stop()
|
||||||
|
|
||||||
|
activeAt := time.Now()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-s.doneChan:
|
||||||
|
return
|
||||||
|
case <-checkIdleTicker.C:
|
||||||
|
// The session is considered inactive if current time is after (last active time + allowed idle time)
|
||||||
|
if time.Now().After(activeAt.Add(closeAfterIdle)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case activeAt = <-s.activeAtChan: // Update last active time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) dstToTransport(buffer []byte) error {
|
||||||
|
n, err := s.dstConn.Read(buffer)
|
||||||
|
s.markActive()
|
||||||
|
if n > 0 {
|
||||||
|
if err := s.transport.SendTo(s.id, buffer[:n]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) transportToDst(payload []byte) (int, error) {
|
||||||
|
s.markActive()
|
||||||
return s.dstConn.Write(payload)
|
return s.dstConn.Write(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sends the last active time to the idle checker loop without blocking. activeAtChan will only be full when there
|
||||||
|
// are many concurrent read/write. It is fine to lose some precision
|
||||||
|
func (s *Session) markActive() {
|
||||||
|
select {
|
||||||
|
case s.activeAtChan <- time.Now():
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) close() {
|
func (s *Session) close() {
|
||||||
close(s.doneChan)
|
close(s.doneChan)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,54 @@
|
||||||
package datagramsession
|
package datagramsession
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCloseSession makes sure a session will stop after context is done
|
// TestCloseSession makes sure a session will stop after context is done
|
||||||
func TestSessionCtxDone(t *testing.T) {
|
func TestSessionCtxDone(t *testing.T) {
|
||||||
testSessionReturns(t, true)
|
testSessionReturns(t, closeByContext, time.Minute*2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCloseSession makes sure a session will stop after close method is called
|
// TestCloseSession makes sure a session will stop after close method is called
|
||||||
func TestCloseSession(t *testing.T) {
|
func TestCloseSession(t *testing.T) {
|
||||||
testSessionReturns(t, false)
|
testSessionReturns(t, closeByCallingClose, time.Minute*2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSessionReturns(t *testing.T, closeByContext bool) {
|
// TestCloseIdle makess sure a session will stop after there is no read/write for a period defined by closeAfterIdle
|
||||||
|
func TestCloseIdle(t *testing.T) {
|
||||||
|
testSessionReturns(t, closeByTimeout, time.Millisecond*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.Duration) {
|
||||||
sessionID := uuid.New()
|
sessionID := uuid.New()
|
||||||
cfdConn, originConn := net.Pipe()
|
cfdConn, originConn := net.Pipe()
|
||||||
payload := testPayload(sessionID)
|
payload := testPayload(sessionID)
|
||||||
transport := &mockQUICTransport{
|
transport := &mockQUICTransport{
|
||||||
reqChan: newDatagramChannel(),
|
reqChan: newDatagramChannel(1),
|
||||||
respChan: newDatagramChannel(),
|
respChan: newDatagramChannel(1),
|
||||||
}
|
}
|
||||||
session := newSession(sessionID, transport, cfdConn)
|
session := newSession(sessionID, transport, cfdConn)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
sessionDone := make(chan struct{})
|
sessionDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
session.Serve(ctx)
|
session.Serve(ctx, closeAfterIdle)
|
||||||
close(sessionDone)
|
close(sessionDone)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
n, err := session.writeToDst(payload)
|
n, err := session.transportToDst(payload)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(payload), n)
|
require.Equal(t, len(payload), n)
|
||||||
}()
|
}()
|
||||||
|
@ -47,13 +58,120 @@ func testSessionReturns(t *testing.T, closeByContext bool) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(payload), n)
|
require.Equal(t, len(payload), n)
|
||||||
|
|
||||||
if closeByContext {
|
lastRead := time.Now()
|
||||||
|
|
||||||
|
switch closeBy {
|
||||||
|
case closeByContext:
|
||||||
cancel()
|
cancel()
|
||||||
} else {
|
case closeByCallingClose:
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
<-sessionDone
|
<-sessionDone
|
||||||
|
if closeBy == closeByTimeout {
|
||||||
|
require.True(t, time.Now().After(lastRead.Add(closeAfterIdle)))
|
||||||
|
}
|
||||||
// call cancelled again otherwise the linter will warn about possible context leak
|
// call cancelled again otherwise the linter will warn about possible context leak
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type closeMethod int
|
||||||
|
|
||||||
|
const (
|
||||||
|
closeByContext closeMethod = iota
|
||||||
|
closeByCallingClose
|
||||||
|
closeByTimeout
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteToDstSessionPreventClosed(t *testing.T) {
|
||||||
|
testActiveSessionNotClosed(t, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadFromDstSessionPreventClosed(t *testing.T) {
|
||||||
|
testActiveSessionNotClosed(t, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool) {
|
||||||
|
const closeAfterIdle = time.Millisecond * 100
|
||||||
|
const activeTime = time.Millisecond * 500
|
||||||
|
|
||||||
|
sessionID := uuid.New()
|
||||||
|
cfdConn, originConn := net.Pipe()
|
||||||
|
payload := testPayload(sessionID)
|
||||||
|
transport := &mockQUICTransport{
|
||||||
|
reqChan: newDatagramChannel(100),
|
||||||
|
respChan: newDatagramChannel(100),
|
||||||
|
}
|
||||||
|
session := newSession(sessionID, transport, cfdConn)
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
activeUntil := startTime.Add(activeTime)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
session.Serve(ctx, closeAfterIdle)
|
||||||
|
if time.Now().Before(startTime.Add(activeTime)) {
|
||||||
|
return fmt.Errorf("session closed while it's still active")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if readFromDst {
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
for {
|
||||||
|
if time.Now().After(activeUntil) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := originConn.Write(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(closeAfterIdle / 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if writeToDst {
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
readBuffer := make([]byte, len(payload))
|
||||||
|
for {
|
||||||
|
n, err := originConn.Read(readBuffer)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF || err == io.ErrClosedPipe {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(payload, readBuffer[:n]) {
|
||||||
|
return fmt.Errorf("payload %v is not equal to %v", readBuffer[:n], payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
for {
|
||||||
|
if time.Now().After(activeUntil) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := session.transportToDst(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(closeAfterIdle / 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, errGroup.Wait())
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkActiveNotBlocking(t *testing.T) {
|
||||||
|
const concurrentCalls = 50
|
||||||
|
session := newSession(uuid.New(), nil, nil)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(concurrentCalls)
|
||||||
|
for i := 0; i < concurrentCalls; i++ {
|
||||||
|
go func() {
|
||||||
|
session.markActive()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
|
@ -8,4 +8,6 @@ type transport interface {
|
||||||
SendTo(sessionID uuid.UUID, payload []byte) error
|
SendTo(sessionID uuid.UUID, payload []byte) error
|
||||||
// ReceiveFrom reads the next datagram from the transport
|
// ReceiveFrom reads the next datagram from the transport
|
||||||
ReceiveFrom() (uuid.UUID, []byte, error)
|
ReceiveFrom() (uuid.UUID, []byte, error)
|
||||||
|
// Max transmission unit of the transport
|
||||||
|
MTU() uint
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@ func (mt *mockQUICTransport) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||||
return mt.reqChan.Receive(context.Background())
|
return mt.reqChan.Receive(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mt *mockQUICTransport) MTU() uint {
|
||||||
|
return 1220
|
||||||
|
}
|
||||||
|
|
||||||
func (mt *mockQUICTransport) newRequest(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
|
func (mt *mockQUICTransport) newRequest(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
|
||||||
return mt.reqChan.Send(ctx, sessionID, payload)
|
return mt.reqChan.Send(ctx, sessionID, payload)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,10 @@ func (dm *DatagramMuxer) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||||
return ExtractSessionID(msg)
|
return ExtractSessionID(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dm *DatagramMuxer) MTU() uint {
|
||||||
|
return MaxDatagramFrameSize
|
||||||
|
}
|
||||||
|
|
||||||
// Each QUIC datagram should be suffixed with session ID.
|
// Each QUIC datagram should be suffixed with session ID.
|
||||||
// ExtractSessionID extracts the session ID and a slice with only the payload
|
// ExtractSessionID extracts the session ID and a slice with only the payload
|
||||||
func ExtractSessionID(b []byte) (uuid.UUID, []byte, error) {
|
func ExtractSessionID(b []byte) (uuid.UUID, []byte, error) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
capnp "zombiezen.com/go/capnproto2"
|
capnp "zombiezen.com/go/capnproto2"
|
||||||
"zombiezen.com/go/capnproto2/rpc"
|
"zombiezen.com/go/capnproto2/rpc"
|
||||||
|
@ -239,8 +240,8 @@ func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16) error {
|
func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration) error {
|
||||||
resp, err := rcs.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort)
|
resp, err := rcs.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -15,6 +16,10 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testCloseIdleAfterHint = time.Minute * 2
|
||||||
|
)
|
||||||
|
|
||||||
func TestConnectRequestData(t *testing.T) {
|
func TestConnectRequestData(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -110,9 +115,10 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||||
serverStream := mockRPCStream{serverReader, serverWriter}
|
serverStream := mockRPCStream{serverReader, serverWriter}
|
||||||
|
|
||||||
rpcServer := mockRPCServer{
|
rpcServer := mockRPCServer{
|
||||||
sessionID: uuid.New(),
|
sessionID: uuid.New(),
|
||||||
dstIP: net.IP{172, 16, 0, 1},
|
dstIP: net.IP{172, 16, 0, 1},
|
||||||
dstPort: 8000,
|
dstPort: 8000,
|
||||||
|
closeIdleAfter: testCloseIdleAfterHint,
|
||||||
}
|
}
|
||||||
logger := zerolog.Nop()
|
logger := zerolog.Nop()
|
||||||
sessionRegisteredChan := make(chan struct{})
|
sessionRegisteredChan := make(chan struct{})
|
||||||
|
@ -131,10 +137,10 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||||
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
|
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.NoError(t, rpcClientStream.RegisterUdpSession(context.Background(), rpcServer.sessionID, rpcServer.dstIP, rpcServer.dstPort))
|
assert.NoError(t, rpcClientStream.RegisterUdpSession(context.Background(), rpcServer.sessionID, rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint))
|
||||||
|
|
||||||
// Different sessionID, the RPC server should reject the registraion
|
// Different sessionID, the RPC server should reject the registraion
|
||||||
assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort))
|
assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint))
|
||||||
|
|
||||||
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID))
|
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID))
|
||||||
|
|
||||||
|
@ -146,12 +152,13 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockRPCServer struct {
|
type mockRPCServer struct {
|
||||||
sessionID uuid.UUID
|
sessionID uuid.UUID
|
||||||
dstIP net.IP
|
dstIP net.IP
|
||||||
dstPort uint16
|
dstPort uint16
|
||||||
|
closeIdleAfter time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16) error {
|
func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
|
||||||
if s.sessionID != sessionID {
|
if s.sessionID != sessionID {
|
||||||
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
|
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +166,10 @@ func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UU
|
||||||
return fmt.Errorf("expect destination IP %s, got %s", s.dstIP, dstIP)
|
return fmt.Errorf("expect destination IP %s, got %s", s.dstIP, dstIP)
|
||||||
}
|
}
|
||||||
if s.dstPort != dstPort {
|
if s.dstPort != dstPort {
|
||||||
return fmt.Errorf("expect session ID %d, got %d", s.dstPort, dstPort)
|
return fmt.Errorf("expect destination port %d, got %d", s.dstPort, dstPort)
|
||||||
|
}
|
||||||
|
if s.closeIdleAfter != closeIdleAfter {
|
||||||
|
return fmt.Errorf("expect closeIdleAfter %d, got %d", s.closeIdleAfter, closeIdleAfter)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -13,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionManager interface {
|
type SessionManager interface {
|
||||||
RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16) error
|
RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error
|
||||||
UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error
|
UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +48,10 @@ func (i SessionManager_PogsImpl) RegisterUdpSession(p tunnelrpc.SessionManager_r
|
||||||
}
|
}
|
||||||
dstPort := p.Params.DstPort()
|
dstPort := p.Params.DstPort()
|
||||||
|
|
||||||
|
closeIdleAfterHint := time.Duration(p.Params.CloseAfterIdleHint())
|
||||||
|
|
||||||
resp := RegisterUdpSessionResponse{}
|
resp := RegisterUdpSessionResponse{}
|
||||||
registrationErr := i.impl.RegisterUdpSession(p.Ctx, sessionID, dstIP, dstPort)
|
registrationErr := i.impl.RegisterUdpSession(p.Ctx, sessionID, dstIP, dstPort, closeIdleAfterHint)
|
||||||
if registrationErr != nil {
|
if registrationErr != nil {
|
||||||
resp.Err = registrationErr
|
resp.Err = registrationErr
|
||||||
}
|
}
|
||||||
|
@ -108,7 +111,7 @@ func (c SessionManager_PogsClient) Close() error {
|
||||||
return c.Conn.Close()
|
return c.Conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16) (*RegisterUdpSessionResponse, error) {
|
func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) (*RegisterUdpSessionResponse, error) {
|
||||||
client := tunnelrpc.SessionManager{Client: c.Client}
|
client := tunnelrpc.SessionManager{Client: c.Client}
|
||||||
promise := client.RegisterUdpSession(ctx, func(p tunnelrpc.SessionManager_registerUdpSession_Params) error {
|
promise := client.RegisterUdpSession(ctx, func(p tunnelrpc.SessionManager_registerUdpSession_Params) error {
|
||||||
if err := p.SetSessionId(sessionID[:]); err != nil {
|
if err := p.SetSessionId(sessionID[:]); err != nil {
|
||||||
|
@ -118,6 +121,7 @@ func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.SetDstPort(dstPort)
|
p.SetDstPort(dstPort)
|
||||||
|
p.SetCloseAfterIdleHint(int64(closeAfterIdleHint))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
result, err := promise.Result().Struct()
|
result, err := promise.Result().Struct()
|
||||||
|
|
|
@ -148,6 +148,7 @@ struct RegisterUdpSessionResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionManager {
|
interface SessionManager {
|
||||||
registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16) -> (result :RegisterUdpSessionResponse);
|
# Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side
|
||||||
|
registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse);
|
||||||
unregisterUdpSession @1 (sessionId :Data) -> ();
|
unregisterUdpSession @1 (sessionId :Data) -> ();
|
||||||
}
|
}
|
|
@ -3465,7 +3465,7 @@ func (c SessionManager) RegisterUdpSession(ctx context.Context, params func(Sess
|
||||||
Options: capnp.NewCallOptions(opts),
|
Options: capnp.NewCallOptions(opts),
|
||||||
}
|
}
|
||||||
if params != nil {
|
if params != nil {
|
||||||
call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 2}
|
call.ParamsSize = capnp.ObjectSize{DataSize: 16, PointerCount: 2}
|
||||||
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) }
|
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) }
|
||||||
}
|
}
|
||||||
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
|
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
|
||||||
|
@ -3560,12 +3560,12 @@ type SessionManager_registerUdpSession_Params struct{ capnp.Struct }
|
||||||
const SessionManager_registerUdpSession_Params_TypeID = 0x904e297b87fbecea
|
const SessionManager_registerUdpSession_Params_TypeID = 0x904e297b87fbecea
|
||||||
|
|
||||||
func NewSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
|
func NewSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
|
||||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
|
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2})
|
||||||
return SessionManager_registerUdpSession_Params{st}, err
|
return SessionManager_registerUdpSession_Params{st}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRootSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
|
func NewRootSessionManager_registerUdpSession_Params(s *capnp.Segment) (SessionManager_registerUdpSession_Params, error) {
|
||||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
|
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2})
|
||||||
return SessionManager_registerUdpSession_Params{st}, err
|
return SessionManager_registerUdpSession_Params{st}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3615,12 +3615,20 @@ func (s SessionManager_registerUdpSession_Params) SetDstPort(v uint16) {
|
||||||
s.Struct.SetUint16(0, v)
|
s.Struct.SetUint16(0, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SessionManager_registerUdpSession_Params) CloseAfterIdleHint() int64 {
|
||||||
|
return int64(s.Struct.Uint64(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SessionManager_registerUdpSession_Params) SetCloseAfterIdleHint(v int64) {
|
||||||
|
s.Struct.SetUint64(8, uint64(v))
|
||||||
|
}
|
||||||
|
|
||||||
// SessionManager_registerUdpSession_Params_List is a list of SessionManager_registerUdpSession_Params.
|
// SessionManager_registerUdpSession_Params_List is a list of SessionManager_registerUdpSession_Params.
|
||||||
type SessionManager_registerUdpSession_Params_List struct{ capnp.List }
|
type SessionManager_registerUdpSession_Params_List struct{ capnp.List }
|
||||||
|
|
||||||
// NewSessionManager_registerUdpSession_Params creates a new list of SessionManager_registerUdpSession_Params.
|
// NewSessionManager_registerUdpSession_Params creates a new list of SessionManager_registerUdpSession_Params.
|
||||||
func NewSessionManager_registerUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Params_List, error) {
|
func NewSessionManager_registerUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_registerUdpSession_Params_List, error) {
|
||||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz)
|
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 2}, sz)
|
||||||
return SessionManager_registerUdpSession_Params_List{l}, err
|
return SessionManager_registerUdpSession_Params_List{l}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3853,201 +3861,203 @@ func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionMa
|
||||||
return SessionManager_unregisterUdpSession_Results{s}, err
|
return SessionManager_unregisterUdpSession_Results{s}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema_db8274f9144abc7e = "x\xda\xccY}p\x15\xe5\xd5?g\xf7\xdelB>" +
|
const schema_db8274f9144abc7e = "x\xda\xccY{p\x14e\xb6?\xa7{&\x9d@\x86" +
|
||||||
"nv\xf6BHF\xdf\xbc/\x03\xe3K\x14\x14(\x1d" +
|
"IW\x0f\x04\xa6\xe4\xe6^\x0a\xcaK\x14\x14\xb8\xdeB" +
|
||||||
"\xa0\xda\x04L\xa8\x89|d\xef\x85\x8e\x05t\xdc\xdc\xfb" +
|
"\xae\xde\x04L\xb8&\xf2H\xcf\x90\xbb\x96\xa0eg\xe6" +
|
||||||
"\x106\xbdw\xf7\xb2\xbb7\x12\x04\xf9\x10D\x1c\xbf@" +
|
"#tv\xa6{\xe8\xee\x89\x04A\x1e\x82\x88\xe5\x0b\x04" +
|
||||||
"PD\xa9\x88\xd3vDm\xa1j\xad\x1d\x9dJ\xeb\xe7" +
|
"E\x94\x95\xc5r\xb7D\xdd\x85U\xd7eKke\xd7" +
|
||||||
"(*\x0etPqZD\xfa\xc1`\xad\x88uh\xd5" +
|
"\x17\xa5\xa8X\xb8\x05\x8a\xb5\xab\xc8>(\\W\xc4\xb5" +
|
||||||
"\xed\x9c\xdd\xbb\x1f\xb9\x09I\x90\xfe\xd1\xff\x92\xb3\xcfs" +
|
"\xdcu\xed\xad\xd3=\xfd\xc8$$A\xf6\x8f\xfdor" +
|
||||||
"\x9es~\xe7\xf7\x9cs\x9es/\x9bY\xda\xc4M\x88" +
|
"\xfa\xfb\xcew\xce\xef\xfc\xbes\xcewr\xe9w*\x1b" +
|
||||||
"\xd6T\x02\xc8\xdb\xa2%6kxg\xf9\xce1\xbf[" +
|
"\xb9)\xd1\xda\x18\x80\xbc-Za\xb3\xfaw\x96\xef\x9c" +
|
||||||
"\x0br\x1d\xa2}\xd3sm\xf13\xd6\xda\xf7!\xca\x0b" +
|
"\xf0\xab\xb5 '\x11\xed[\x9eoM|e\xad}\x1f" +
|
||||||
"\x00\x93\x96\x96,Gi}\x89\x00 \xad)\xf93\xa0" +
|
"\xa2\xbc\x000mi\xc5r\x94\xd6W\x08\x00\xd2\x9a\x8a" +
|
||||||
"}\xcb\x88=\x0f\xfd\xb8e\xcb\xcd \xd6\xf1\xc1b\xc0" +
|
"\xdf\x03\xda\xb7\x8d\xda\xf3\xc8c\xcd[n\x051\xc9\x07" +
|
||||||
"ILhC\xa9G\xa0\x95ya\x83t\x88\xfe\xb2\xaf" +
|
"\x8b\x01\xa71\xa1\x15\xa5\x1e\x81V\x16\x85\x0d\xd2\xbb\xf4" +
|
||||||
"\x16/]\x18\x7f\xfbMZ\x1dV\x1d!\xd5\xcf\x0b\x0d" +
|
"\xcb\xbeF\xbcda\xe2\xed7iuXu\x84T\xbf" +
|
||||||
"(\xedw6\xbc&\x90\xea\xcb\xb3o\xed\xfa\xf6\xd6\xd7" +
|
" \xd4\xa3t\xd0\xd9p@ \xd5W\xe4\xdf\xda\xf5\xdf" +
|
||||||
"\xd7\x81X\xc7\xf5R\xfdt\xe9r\x94^+\xa5\x95/" +
|
"[__\x07b\x92\xeb\xa5\xfa\xd9\xca\xe5(\x1d\xa8\xa4" +
|
||||||
"\x95\xce\x05\xb4?\xdb2\xf2\xb1\x87\xdf|u=\x88\x17" +
|
"\x95/W\xce\x07\xb4?\xdf2\xfa\x89\xef\xbf\xf9\xdaz" +
|
||||||
"!\x14,\xfd\xa0\xf4=\x04\x94>-\xfd9\xa0\xbd\xff" +
|
"\x10/D(Y\xfaA\xe5{\x08(}V\xf9c@" +
|
||||||
"\xf3\x85\xa7\x9fzy\xf2- \x8e\xa5\x05H\x0b6\x95" +
|
"\xfb\xe0\x17\x0b\xcf<\xf3\xcae\xb7\x818\x91\x16 -" +
|
||||||
"\x8d\xe2\x00\xa5G\xcb\x1a\x01\xed\x13'\xff\xb5\xe1\xc6\xb1" +
|
"\xd8T5\x8e\x03\x94\x1e\xafj\x00\xb4O\x9e\xfa\xdb\x86" +
|
||||||
"s\xee\x06y,\xd2\x0a\x8eV\xbcVVG+\x8e\x96" +
|
"\x9b'\xce\xbb\x17\xe4\x89\xc8\x01D9Zq\xa0*I" +
|
||||||
"\x91\x8a\xc6\x99\xfb\x9f\xad\x9bt\xef\x96\"\xd3\x9d\x85+" +
|
"+>\xac\"k\x1af\x1f\xdc\x97\x9cv\xff\x962\xd3" +
|
||||||
"\x875\xa0t\xc702h\xe3\xb0\x1b\x00\xed\x7fT=" +
|
"\x9d\x85\xfb\x87\xd5\xa3th\x18\x19tp\xd8M\x80\xf6" +
|
||||||
"\xf0f\xfe\xcag\xee\x0d\x9f\xf7\xf1\xb0\x06\xd2\x16-\xa7" +
|
"_F<\xf4f\xf1\xaa\xe7\xee\x0f\x9f7ex=i" +
|
||||||
"\xf3Fu\x8f\xb9\xfe\xb7/=y\x1f\xc8\xe3\x10\xed#" +
|
"k\x19N\xe7\x8d\xeb\x9ep\xe3/_~\xfa\x01\x90'" +
|
||||||
"\x1d\x17\x1f\xe2w\xec~\x1f\xe6\xa3\x80\x1c\xc0\xa4\xb1\xe5" +
|
"!\xda\xc7:.z\x97\xdf\xb1\xfb}hG\x81\x8e\x9f" +
|
||||||
"\xbb\xc8\xf8\xa9\xe5\xa4\xec\xadK\x9e\xfb\xf5\xddOnx" +
|
"\x96\x1f\xbe\x8b\x8c_9\x9c\x94\xbdu\xf1\xf3?\xbf\xf7" +
|
||||||
"\x00\xe4\x8b\xc86\x07\xac\x1d\xe5\xff\xa4\x05{\x1de[" +
|
"\xe9\x0d\x0f\x81|!\"\x80\x03\xd6\x87\xc3\xffJ\x0b\xbe" +
|
||||||
"\x0e??'\xbbi\xfb.\xd7}\xe7\xfb\xbb\xe5\x1c\x07" +
|
"p\x94m9\xf2\xc2\xbc\xfc\xa6\xed\xbb\\\xf7\x9d\xefc" +
|
||||||
"\x11{]\xeb\x17\xd9\xf9\x8f$\x1f)\x00\x13\xa5O\xfb" +
|
"\xab9\x0e\"\xf6\xba\x96/\xf3\xed\x8f\xa6\x1f-\x01\x13" +
|
||||||
"\xcbO!\xe0\xa4\xa3\xe5\xf5\x08hO~\xef\xf8\xdc\xd9" +
|
"\xa5Ob\xf5i\x04\x9c6\xa1\xba\x0e\x01\xed\xcb\xde;" +
|
||||||
"\xbfX\xfc\xd3\xd0\xde\xaf*\x96\xd3\xde\x0d\x8bO\xed\xab" +
|
"1\x7f\xeeO\x16\xff0\xb4wfl9\xed\xdd\xb0\xf8" +
|
||||||
"Nd\x1f+r\xd8\xf1\xe5L\xc5n\x94\xc4Jr\xb8" +
|
"\xf4\xfe\x9aT\xfe\x892\x87\x1d_\xae\x8c\xedF\xa9=" +
|
||||||
"\xb2\x92Lx\xe2\x7f\xae.[v|\xe6\x1e\x10\xc7y" +
|
"F\x0e\xcb12\xe1\xa9\x7f\xbb\xa6j\xd9\x89\xd9{@" +
|
||||||
"j\xc6U&HM\xe4Z\xfeke\xdbo\x9e*\xa6" +
|
"\x9c\xe4\xa9Y\x1aK\x91\x9a\xc8\xf5\xfc7\xca\xb6_<" +
|
||||||
"\x93\x03\xdc\xd8\xca\x0e\x94\xae =\x93\xa6V:\xf6\xdc" +
|
"SN'\x07\xb8|\xac\x03\xa55\xa4g\xda\xca\x98c" +
|
||||||
"\xb6o\xfb\xc5\xa5\x0f}\xf6t\x7f0+U\x1d(\xe5" +
|
"\xcf\x1d\xfb\xb7_T\xf9\xc8\xe7\xcf\xf6\x07\xf3c#:" +
|
||||||
"\xab\xe8\xd4\xa5U\x84\xcc\xf0V<\xf2\xc2\x84\xc83\xe1" +
|
"P\xda7\x82N}v\x04!3\xb2\x05\x8f\xbd8%" +
|
||||||
"\xb8\x1f\xa8:A\xc8\x1c\xaf\xa2\xa0]\xf8\xf1\x8cJ\xed" +
|
"\xf2\\8\xee#\xe3'\x09\x99\x89q\x8a\xfb\xd8Of" +
|
||||||
"\x93\xb5/\x14is\x16\xae\x8f\xb5\xa1t\x7f\x8c\xb4m" +
|
"\xc5\xb4O\xd7\xbeX\xa6\xcdYx \xde\x8a\xd2\x07q" +
|
||||||
"\x8d\xd1\xe2\xb6\x85\xf7l\x8e\x1e\xbf\xe7\x15\xb24D\xb8" +
|
"\xd2v\xd4Y\xdc\xba\xf0\xbe\xcd\xd1\x13\xf7\xbdJ\x96\x86" +
|
||||||
"(\x11m\xd2\x84j\x03\xa5\x96j\xfaszu\x0d\x0f" +
|
"\x08\x17%\xa2M+\xd6\x18(m\xac\xa1\x9f\xebkj" +
|
||||||
"h\xd7\xed\xf9\xce\xcff\xa4\xdf}\xbd\x1fK\xa5}\xd2" +
|
"y@;\xb9\xe7\x7f~4+{\xf4\xf5~,\x95\xa2" +
|
||||||
")i\xbf\xe4PY\"C\x8f\x8d\xdb{\xe3_\xef8" +
|
"\x89\xd3\x92\x98\xa0_\xb1\x04\x19z|\xd2\xde\x9b\xffx" +
|
||||||
"p\xb0`\xa8\x83\xe1\x98\xb8\x13\xc2\xa9q\xc2\xcfg@" +
|
"\xd7\xa1\xc3%C\x1d\x0c\xd5\x84\x13\xc2\x95\x09\xc2\xcfg" +
|
||||||
"\x11J\xce\xca\x1f\xc4\xbbP\xca\xc6I\x9d\xea\xac\xe6\x8e" +
|
"@\x19J\xce\xca\x1d\x89.\x94\xf6:\xea\x9erVs" +
|
||||||
"+\xb5\xab\x7f\xff\xdd#\xa1\xa0e\xe3\x1f\"D\xec9" +
|
"'\x941\xab\x7f\xfd\xbf\xc7BA\xdb\x9b\xf8\x08!b" +
|
||||||
"\xdf_\xd8U\xb6\xf2\xd8\xb1\xf0AJ\xdcA$\xefl" +
|
"\xcf\xfb\xff\x85]U+\x8f\x1f\x0f\x1f\xf4X\xc2Ad" +
|
||||||
"\xfd\xdbON\xdcu2\x9b\xfe\x93C<\x0f\xb3\xad\xf1" +
|
"\x9f\xb3\xf5O?8y\xcf\xa9|\xf6w\x0e\xf1<\xcc" +
|
||||||
"iD\xcd'\xe2t\xedj\xea+[F\x1dn?\xe1" +
|
"\x8e&f\x105?K\x10\xd1k\xebb\xcd\xe3\x8e\xb4" +
|
||||||
"\x86\xd2U\xb1q\xf8\x0cZ\xf0\xf0pR1\xf9\xfa\xe9" +
|
"\x9dtC\xe9\xaa88r\x16-81\x92T\\v" +
|
||||||
"l\xd1\x94kN\xf4\xb9\xf2\xfb\x86OC\xe9\xc0p\x87" +
|
"\xe3L\xb6h\xfa\xb5'\xfb\\\xf9\xe8\xa8\x19(\x8d\x1c" +
|
||||||
"d\xc37\xa0t|D\x0d\x80\xdd\xfd\xcbM\xd7<\xf6" +
|
"\xe5\x90l\xd4\x06\x94&\xd6\xd6\x02\xd8\xdd?\xddt\xed" +
|
||||||
"\xe2\x9cS\xee]p\x8c=4b\"Q\xe3\xce\x9b\x9a" +
|
"\x13/\xcd;\xed\xde\x05\xc7\xd81\xb5S\x89\x1aw\xdf" +
|
||||||
"\xe7N\x1d\xb5\xefT\xd8\xd8\xfd#\x88\x9d\xd2\xd1\x11t" +
|
"\xd24\xff\xf2q\xfbO\x87\x8d\x15k\x89\x9d\xd2\x84Z" +
|
||||||
"\xd2\xe2)'\xbf7\xe6\xce\x97O\xf5GA\xaci@" +
|
":i\xf1\xf4S\xff7\xe1\xeeWN\xf7G\xc1\xe6\xda" +
|
||||||
"I\xacq(X\xd3\x08\xf8\xc9\xcc\x1f\x1d\xac\x8b\xd5\x9d" +
|
"z\x94\xdak\x1d\x0a\xd2\xe2Og\x7f\xefp2\x9e<" +
|
||||||
".\x02\xb0\xc4\x09^M\x17J-5N\xf0j^!" +
|
"S\x06`\x85\x13\xbc\xda.\x946\xd6:\xc1\xab}\x95" +
|
||||||
"\x9a\xdd\xf2\xfeu\xcb\xde\xb9\xf9\xb3\xcf\x8bc\xed\xa8\x9e" +
|
"hv\xdb\xfb7,{\xe7\xd6\xcf\xbf(\x8f\xb5\xa3\xba" +
|
||||||
"\\\x9b@\xa9\xb5\x96T\xb7\xd4\x123\xee\x9b\xf7\x97U" +
|
"gL\x0a\xa5\xbb\xc6\x90\xea\x8dc\x88\x19\x0f,\xf8\xc3" +
|
||||||
"'\xb7\x8e\xf8\xa2\x8f\xc7\xc7k\xbbP:\xe3\xac\xfc\xbc" +
|
"\xaaS[G}\xd9\xc7\xe3\x89\xc9.\x94\xaeL\xd2\xca" +
|
||||||
"v\x834\xbf\x8e\x92\xdc\xdb\xc2#\x13\x9aW\xbd~&" +
|
"\xcb\x93\x1b\xa4\x07\xe9\x97\xfd\xb6\xf0\xe8\x94\xa6U\xaf\x7f" +
|
||||||
"t\x17\xae\xa8k#\x87\xef\x15\x1e<\xb6\xfa\x0f\xd7}" +
|
"\x15\xba\x0bk\x92\xad\xe4\xf0\xfd\xc2\xc3\xc7W\xff\xe6\x86" +
|
||||||
"\x19vxj\xdd\x87\xe4\xf0\xec:rx\xc5'\xf7_" +
|
"\xaf\xc3\x0e\xafL~D\x0eoJ\x92\xc3+>}\xf0" +
|
||||||
"u\xd7\xa2\xc7\xbf\x0e\x07\xb6n-m\xb5\xf2\x9a\xc62" +
|
"\xea{\x16=\xf9M8\xb0\xc9\xb5\xb4\xd5*j\x1a\xcb" +
|
||||||
"F.\x92\xba\xd4\xfb35>\xa5\xe4\xb4\xdc\xb4\xe9y" +
|
"\x19\x85H\xe6\x12\xefgfrF)h\x85\x193\x8b" +
|
||||||
"k\x09\xd3,5\xa5X,\xc1\x1a\xcd\x9c\xae\x99\xac\x1d" +
|
"\xd6\x12\xa6YjF\xb1X\x8a5\x98\x05]3Y\x1b" +
|
||||||
"Q\xae\xe6#\x00\x11\x04\x10\x95.\x00\xf9z\x1e\xe5\x0c" +
|
"\xa2\\\xc3G\x00\"\x08 *]\x00\xf2\x8d<\xca9" +
|
||||||
"\x87\"b\x9cB/\xaa$\\\xc2\xa3lq(r\\" +
|
"\x0eE\xc4\x04\x85^TI\xb8\x84G\xd9\xe2P\xe4\xb8" +
|
||||||
"\x9c2\x8f\xb8t\x14\x80\x9c\xe1Q^\xc6!\xf2q\xe4" +
|
"\x04e\x1eq\xe98\x009\xc7\xa3\xbc\x8cC\xe4\x13\xc8" +
|
||||||
"\x01\xc4\xfcf\x00y\x19\x8f\xf2:\x0e\xed\x1c3\xb2\x8a" +
|
"\x03\x88\xc5\xcd\x00\xf22\x1e\xe5u\x1c\xda\x05f\xe4\x15" +
|
||||||
"\xc64\x88Y-\x86\x81\x15\xc0a\x05\xa0m0\xcb\xe8" +
|
"\x8di\x10\xb7\x9a\x0d\x03\xab\x81\xc3j@\xdb`\x96\xd1" +
|
||||||
"Q:2\x10c!\xb1\xd0u\x83\x85\x95\xc0a%\xa0" +
|
"\xa3t\xe4 \xceBb\xa1\xeb&\x0bc\xc0a\x0c\xd0" +
|
||||||
"\xbdD\xcf\x1b\xe6|\xcdB5\x93`\x8b\x0df\xe2\x12" +
|
"^\xa2\x17\x0d\xb3]\xb3P\xcd\xa5\xd8b\x83\x99\xb8\x04" +
|
||||||
",\x01\x0eK\x00\x07r/\xc9LS\xd5\xb5\xd9\x8a\xa6" +
|
"+\x80\xc3\x0a\xc0\x81\xdcK3\xd3Tum\xae\xa2)" +
|
||||||
"t2\x03\x80<+\xe5\xa3\x00~\xd2F/\xbd\x8b\x13" +
|
"\x9d\xcc\x00 \xcf*\xf9(\x80\x9f\xb4\xd1K\xef\xe2\x94" +
|
||||||
"\xb6\x03'\x8e\x130\xc8\xc0\xe8\xd1O\xfc\xbf\xdd\xc0\x89" +
|
"\xed\xc0\x89\x93\x04\x0c20z\xf4\x13\xffc7p\xe2" +
|
||||||
"\x17\x0a\xb6\xc1:U\xd3b\x06\xceO\xe7\x1c\xdd\xbc\xae" +
|
"X\xc16X\xa7jZ\xcc\xc0\xf6l\xc1\xd1\xcd\xebZ" +
|
||||||
"5\xa1\x9d\xd7\xdc\x0f\xc8\x0c\xf7C\x8cNm\xc2v\x0c" +
|
"#\xdaE\xcd\xfd\x80\xccp?\xc4\xe9\xd4Fl\xc3\xc0" +
|
||||||
"\xac\xe3\xfbZweFe\x9a\x15k\xd5\x16\xebE\x90" +
|
":\xbe\xafuW\xe5T\xa6Y\xf1\x16m\xb1^\x06y" +
|
||||||
"\xb7\xf5\x07y[\x01\xf2u!\xc8\xd7\xcc\x00\x90W\xf0" +
|
"k\x7f\x90\xb7\x96 _\x17\x82|\xcd,\x00y\x05\x8f" +
|
||||||
"(\xdf\xca\xa1\xc8\x170_\xdf\x00 \xaf\xe6Q\xbe\x9d" +
|
"\xf2\xed\x1c\x8a|\x09\xf3\xf5\xf5\x00\xf2j\x1e\xe5;9" +
|
||||||
"C;\xe5\x1c\xd2\x9a\x06\x00\x1f\xcd\xc5L\xb1\xf2\x063" +
|
"\xb43\xce!-Y\x00\xf0\xd1\\\xcc\x14\xabh0\x93" +
|
||||||
"IV\x05\xd8\xce\xa3\x03z\x15\xe0\xaanf\x90\xed^" +
|
"d#\x00\xdbxt@\x1f\x01\xb8\xaa\x9b\x19d\xbb\x17" +
|
||||||
"\x10b\x8a\x91Z\xe2\x07j\x00\xa4[\x96\xa9\xa6\xa5j" +
|
"\x84\xb8bd\x96\xf8\x81\x1a\x00\xe9\xe6e\xaai\xa9Z" +
|
||||||
"\x9d\xf3\x1cyc\xbb\x9eQS=\xe4U\x85c\xe7\x85" +
|
"\xe7\x02G\xde\xd0\xa6\xe7\xd4L\x0fyU\xed\xd89v" +
|
||||||
"\xd3\x00\x10\xc5\xe1\x0b\x00\x90\x13\xc5\x19\x00\x8dj\xa7\xa6" +
|
"\x06\x00\xa28\xf2:\x00\xe4Dq\x16@\x83\xda\xa9\xe9" +
|
||||||
"\x1b\xccN\xabfJ\xd74\x06|\xcaZ\xd5\xa1d\x14" +
|
"\x06\xb3\xb3\xaa\x99\xd15\x8d\x01\x9f\xb1Vu(9E" +
|
||||||
"-\xc5\xfc\x83J\xfa\x1e\xe4\x1e\x90dF73\xc6+" +
|
"\xcb0\xff\xa0\x8a\xbe\x07\xb9\x07\xa4\x99\xd1\xcd\x8c\xc9J" +
|
||||||
"!\xfa\x8enW\x0c\x85\xcf\x9ar\x85\x8fc\xcb\x02\x00" +
|
"\x88\xbe\xe3\xdb\x14C\xe1\xf3\xa6\\\xed\xe3\xd8|\x1d\x80" +
|
||||||
"\xb9\x99G\xb9=\x84\xe3l\xc2q\x16\x8f\xf25!\x1c" +
|
"\xdc\xc4\xa3\xdc\x16\xc2q.\xe18\x87G\xf9\xda\x10\x8e" +
|
||||||
"\xe7\x13\x8e\xed<\xca\x8b8\xb4uC\xedT\xb5+\x19" +
|
"\xed\x84c\x1b\x8f\xf2\"\x0em\xddP;U\xed*\x06" +
|
||||||
"\xf0F\x98\x81\xa6\xa5)YF\x98\x15\xf0X\xa5\xe7," +
|
"\xbc\x11f\xa0iiJ\x9e\x11f%<V\xe9\x05K" +
|
||||||
"U\xd7L\xac\x0e\xf2? V\x87\x90\x12\x06\xe3\xe4x" +
|
"\xd55\x13k\x82\xfc\x0f\x885!\xa4\x84\xc189\xd9" +
|
||||||
"\x8fR\x1e\xa3tmt\x82\x99y!c\x99r\xc4\xf7" +
|
"\xa3\x94\xc7(]\x1b\x9fbfQ\xc8Y\xa6\x1c\xf1=" +
|
||||||
"\xa4r\x1a\x80\\\xca\xa3\x1c\xe7\xb0\xd1`f>ca" +
|
"\x89\xcd\x00\x90+y\x94\x13\x1c6\x18\xcc,\xe6,\xac" +
|
||||||
"uPf\xff\x13\xa7\xf6\x03_\xa2?\xf8&\x02\xc8W" +
|
"\x09\xca\xec?\xe3T\x0f\xbe\x10\x0dS\xfd\xd1p*\x80" +
|
||||||
"\xf1(\xcf\xe3\x10\x0b\xe8\xc93\x02Hm\xd3\xd5\xd7\x0a" +
|
"\x9c\xe5Q.p\x88%\xf4\xf2\xb3B\xd9\x80G\x97\x85" +
|
||||||
"\x98\xf6\xc0\xabO\x9bVk\xce\xfboU\xda\xb4\xdau" +
|
"K\xb7\x03\xc8\x16\x8f\xf2j\x0em\xd3=\xa4\x050\xeb" +
|
||||||
"\xc3B\x018\x14`\xc0+\xe2F;Fi\xca\xbd\xbb" +
|
"!Z\x975\xad\x96\x82\xf7\xd7\xaa\xaci\xb5\xe9\x86\x85" +
|
||||||
"\x9emc)\xb4\xff\xcf\xa3\xfc\xad\x90m\x13(+]" +
|
"\x02p(\x00\xf1V7\xd9\xcc\xc5t\xa7Z\xb29v" +
|
||||||
"\xc6\xa3|9\x87\xb6\x92J\xe9y\xcd\x9a\x07\xbc\xd2Y" +
|
"\xb5\xcak\x16F\x81\xc3(\x0cx\xa9\\~\xc4)\xb1" +
|
||||||
"\xc4\xe0$\x83X\xca`Ap\x87\x0e\x9cw\xd5\x8b\xa0" +
|
"\xb9\xb7\xdd\xf3f\"\x91\xe1?y\x94\xff+\xe4\xcd\x14" +
|
||||||
"\x8b\x19J\xb6W\xbc\x08\xba\x0a\x1e\xe5\x91\xfd#\xe2\x9f" +
|
"\xcac\x97\xf2(_\xc1\xa1\xadd2zQ\xb3\x16\x00" +
|
||||||
"\x18\xed'-\xd0uH\x11\xb3\x12\xccM\xc8\xe3\x0df" +
|
"\xaft\x96q>\xcd \x9e1X@\x87\xa1C\xed%" +
|
||||||
"\x0a\xf9\x8cE\xfeW\xd8\xb6\x0b\x00\xc5a4\x8f\xf2e" +
|
"\x872\xb0\xe3\x86\x92\xef\x15a\x02\xbb\x9aGyt\xff" +
|
||||||
"\x1cV\xe2\xd7\xb6\x8b\xc0\xb8\xcd\x01\x02\xf5\xcc0t\x03" +
|
"p\xf9'F\xfbI$t\x812\xc4\xc5\x14sS\xf8" +
|
||||||
"\xab\x83\x82U\xa0I\xaap\x00\xeaZ3\xb3\x145\x83" +
|
"d\x83\x99B1g\x91\xff\xd5\xb6\xed\x02@\x91\x1b\xcf" +
|
||||||
"Da\xbf{*\"\xd3`w0@\xc4\x15\x8fn$" +
|
"\xa3|)\x871\xfc\xc6v\x11\x98\xb49@\xa0\x8e\x19" +
|
||||||
"&\xf5Fc;\x80\\\xcd\xa3|\x01\x87v\xa7\xa1\xa4" +
|
"\x86n`MP\xe2J\xc4\xca\x94\x0e@]kb\x96" +
|
||||||
"X;3P\xd5\xd3s\x14MO\xf2,\x85Q\xe00" +
|
"\xa2\xe6\x90H\xef\xf7[e\xf4\x1b\xec\xd6\x06\x88\xb8\xe2" +
|
||||||
"\x1a:\xb4\xea\\\x0fM8w\xc2\x04\x7f\xd7\xc0\xfb\x0d" +
|
"\xf1\x0d\xc4\xbd\xdeh\x10yjx\x94/\xe0\xd0\xee4" +
|
||||||
"V\x00\xa1\xb0\xbd\xbd\xde\xb59\xee\xdb\xbcrTP\xb8" +
|
"\x94\x0ckc\x06\xaazv\x9e\xa2\xe9i\x9ee\xfaP" +
|
||||||
"|\x82\xad\xe9\x082\xab\x9f;6\x12\x15o\xe5Q\xde" +
|
"a\xc4\xb9\x1e\x9arn\x91\x09\xfe\xae\x81\xf7\x1b\xac\x04" +
|
||||||
"\x12\xca\xc1\x9b(\xcb\xdc\xcd\xa3\xfc \x87b$\x12\xc7" +
|
"Bi{[\x9dks\xc2\xb7y\xe5\xb8\xa0\xd4\xf9\x04" +
|
||||||
"\x08\x80x?\xdd\x93-<\xca;\xb9\xde\xe5\x8du3" +
|
"[\xd3\x11\xe4b?\xdbl$*\xde\xce\xa3\xbc%\x94" +
|
||||||
"\xcdjV;A`f %\x13\x9b\xd5N\x06\xbcy" +
|
"\xb57Q^\xba\x97G\xf9a\x0e\xc5H$\x81\x11\x00" +
|
||||||
"\xbey\xa8t\x10<\xf4\x0eS\xcf0\x8b5\xb3TF" +
|
"\xf1A\xbaY[x\x94wr\xbd\x0b\"\xebf\x9a\xd5" +
|
||||||
"1\x14K\xedf\xee\xf7\x02\x19\xbd\xa0\x0e\xc4\xdbD\x9f" +
|
"\xa4v\x82\xc0\xcc@J&6\xa9\x9d\x0cx\xf3|3" +
|
||||||
"\x8bA\xfc\x8dy\x1dE\x88\x0e\xa3\x82d&\xb0P#" +
|
"W\xe5 x\xe8\x1d\xa6\x9ec\x16kb\x99\x9cb(" +
|
||||||
"0\x80\xb5\xaer\xb2L\xd7\xfap \xb81\x05\x1e\xa0" +
|
"\x96\xda\xcd\xdc\xef%2zA\x1d\x88\xb7\xa9>\x17\x83" +
|
||||||
"9P\xad\x0a\x96\xcf\xcdY\xaa\xa0k&\xd9\x17\x0a\xfd" +
|
"\xf8\x1b\xf7z\x90\x10\x1d\xc6\x05\xe9O`\xa1\xd6a\x00" +
|
||||||
"\xb4\xfeBo\x04\xa1\xf7\xf2\xde\xc6\xb5\xe1\xc8c!\xf2" +
|
"k]\xe5d\x99\xae\xf5\xe1@pcJ<@s\xa0" +
|
||||||
"\xdb\x83 \x8b\x11\xce\x8d\xfc\x8e]\x00\xf2N\x1e\xe5\xc7" +
|
"\xea\x16,\x9f_\xb0TA\xd7L\xb2/\x14\xfa\x19\xfd" +
|
||||||
"9ltK2V\x07O\xdaB\xb4\xdc\xc23K\x87" +
|
"\x85\xde\x08B\xefe\xca\x8dk\xc3\x91/e\xcaM\xdb" +
|
||||||
"\xfa\x94\x92\x09\x12\xa6m\xb0\\FI\xb1\x16,\x14Y" +
|
"\x83 \x8b\x11\xce\x8d\xfc\x8e]\x00\xf2N\x1e\xe5'9" +
|
||||||
"@\x04\x0e\xd1\xa1H6g0\xd3DU\xd7\xe4\xbc\x92" +
|
"lp\x8b8\xd6\x04\x8f\xe0R\xb4\xdcR5G\x87\xba" +
|
||||||
"Qy\xab\xc7o\x8c\xb4|\xb6\xdd`\xdd*\xeays" +
|
"\x8c\x92\x0b\xb2\xa9m\xb0BN\xc9\xb0f,\x95e@" +
|
||||||
"\xbae\xb1\xac\x90\xb3\xcc\xa1\xb4M\x01@\x94\x1f\x045" +
|
"\x04\x0e\xd1\xa1H\xbe`0\xd3DU\xd7\xe4\xa2\x92S" +
|
||||||
"c\x16%\xdf\x86 \xf7\xf8\x00\x8d\xa3\xe4{\x09\x8f\xf2" +
|
"y\xab\xc7o\xa5\xb4b\xbe\xcd`\xdd*\xeaEs\xa6" +
|
||||||
"\x14\x0ec\xf9\xbc\x1a\xe4\xba\x8c\x9er\xe2\x06\xb19J" +
|
"e\xb1\xbcP\xb0\xcc\xa14Z\x01@\x94\x1f\x045g" +
|
||||||
"\x96\xf5\x89v\xc9\xa0w\xb5\xd7M\xf7\x92\xed\x7fS\x99" +
|
"\x96%\xdf\xfa \xf7\xf8\x00M\xa2\xe4{1\x8f\xf2t" +
|
||||||
"\x1f\xb8\xb3&\xd7\x9d\xd63d2]\x81&\x1e\xe5Y" +
|
"\x0e\xe3\xc5\xa2\x1a\xe4\xba\x9c\x9eq\xe2\x06\xf1yJ\x9e" +
|
||||||
"!\x93['\x86\xfc\xf0L\x9e\xdd\x11\xf8!\xfc\x90\xf5" +
|
"\xf5\x89v\xc5\xa0w\xb5\xd7M\xf7\x92\xed\xbfRc0" +
|
||||||
"xV\xd5\xb3,en\x0f\xcc\x823\xd3A\xb8:X" +
|
"p/N\xae;\xcdj\xc8d\xba\x02\x8d<\xcasB" +
|
||||||
"3\x90}\xe1\x0b57W\xefxH6N\xf1l\x94" +
|
"&\xb7L\x0d\xf9\xe1\x99<\xb7#\xf0C\xf8.\xeb\xf1" +
|
||||||
"z\xb0\x0d \xb9\x0cyL\xae\xc3\xc0Li\x0d\xce\x00" +
|
"\xac\xaacy\xca\xdc\x1e\x98%gf\x82pM\xb0f" +
|
||||||
"H\xae \xf9\xad\x18X*\xad\xc7:\x80\xe4j\x92\xdf" +
|
" \xfb\xc2\x17j~\xa1\xce\xf1\x90l\x9c\xee\xd9(\xf5" +
|
||||||
"\x8e\xfe\x0b@\xda\x88\xbb\x01\x92\xb7\x93x\x1b-\x8f\xf0" +
|
"`+@z\x19\xf2\x98^\x87\x81\x99\xd2\x1a\x9c\x05\x90" +
|
||||||
"\xce\x95\x90\xb6:\xea\xb7\x90|'\xc9\xa3\x918F\x01" +
|
"^A\xf2\xdb1\xb0TZ\x8fI\x80\xf4j\x92\xdf\x89" +
|
||||||
"\xa4\x1d\xd8\x00\x90\xdcF\xf2\xa7H^\xc2\xc5\xb1\x04@" +
|
"\xfe\x9bA\xda\x88\xbb\x01\xd2w\x92x\x1b-\x8f\xf0\xce" +
|
||||||
"\xda\x8b]\x00\xc9=$\x7f\x8e\xe4B4N\x8f \xe9" +
|
"\x95\x90\xb6:\xea\xb7\x90|'\xc9\xa3\x91\x04F\x01\xa4" +
|
||||||
"Y4\x00\x92\xbf\"\xf9\x8b$/\x1d\x19\xc7RzG" +
|
"\x1dX\x0f\x90\xdeF\xf2gH^\xc1%\xb0\x02@\xda" +
|
||||||
";\xf2\x17H\xfe\x06\xc9\xcbj\xe3XF\xafj\\\x0b" +
|
"\x8b]\x00\xe9=$\x7f\x9e\xe4B4A\xcf&i\x1f" +
|
||||||
"\x90|\x95\xe4\x07I>\x0c\xe38\x0c@:\x80\xdb\x01" +
|
"\x1a\x00\xe9\x9f\x91\xfc%\x92W\x8eN`%\x80\xb4\xdf" +
|
||||||
"\x92\x07I\xfeG\x92\x97\x97\xc4\xb1\x1c@\xfa\xc0\xb1\xe7" +
|
"\x91\xbfH\xf27H^5&\x81U\x00\xd2\x01\\\x0b" +
|
||||||
"0\xc9?\"yE$\x8e\x15\x00\xd2Q\xdc\x05\x90\xfc" +
|
"\x90~\x8d\xe4\x87I>\x0c\x138\x0c@:\x84\xdb\x01" +
|
||||||
"\x88\xe4\x7f'y\xa5\x10\xc7J\x00\xe9c\xc7\xaf\x93$" +
|
"\xd2\x87I\xfe[\x92\x0f\xafH\xe0p\x00\xe9\x03\xc7\x9e" +
|
||||||
"/\xe5\x8a\x1ap\x8fQE]6\xaf\x9b~\xc8X\xe1" +
|
"#$\xff\x98\xe4\xd5\x91\x04V\x03H\x1f\xe2.\x80\xf4" +
|
||||||
"\x8e\xa3K\xf7v=F\x9d4\xc6\x82\x91\x16 \xc6\x00" +
|
"\xc7$\xff3\xc9cB\x02c\x00\xd2'\x8e_\xa7H" +
|
||||||
"\xed\x9c\xaeg\xe6\xf4fj\xccR:M\xaf\xa3\xaf\x0e" +
|
"^\xc9\x95\xb5\xec\x1e\xa3\xca\xfar^7\xfd\x90\xb1\xd2" +
|
||||||
"\xa6\x0c\x80$\xf4\xeb>\xc4t\xad5\xed'\x82\xe2\xac" +
|
"\x1dG\x97\xeemz\x9czo\x8c\x07C0@\x8c\x03" +
|
||||||
"\xe3Y\xa2\x9a\xd3\xf3\x96\x9e\xcfA}Z\xb1X\xda\xcf" +
|
"\xda\x05]\xcf\xcd\xeb\xcd\xd4\xb8\xa5t\x9a\xde\x1b\xa0&" +
|
||||||
"9F^\x9bi\xe8\xd9y\xc8\x8c\xac\xaa)\x99A\xb2" +
|
"\x98K\x00\x92\xd0\xaf\xfb\x10\xd7\xb5\x96\xac\x9f\x08\xca\xb3" +
|
||||||
"Q\x19pX\x06\x85\x94\xe0\xe9\x1e85\x9d\xfd}\xe2" +
|
"\x8eg\x89j\xce,Zz\xb1\x00uY\xc5bY?" +
|
||||||
"3\x9a+ft}n\xda<\xa5s(yjb\xd0" +
|
"\xe7\x18Em\xb6\xa1\xe7\x17 3\xf2\xaa\xa6\xe4\x06\xc9" +
|
||||||
"9\xc6\xb4PB\xaa\xefV2\xf9o\x92\x9ez\xb7\x12" +
|
"FU\xc0a\x15\x94R\x82\xa7{\xe0\xd4t\xf6\x17\x8d" +
|
||||||
"\x89F\xb7\x15\x19\xac{\xf7\x86\x0e\x83\xa7\x92\xde\x0da" +
|
"\xcfh\xae\x9c\xd1u\x85\x19\x0b\x94\xce\xa1\xe4\xa9\xa9A" +
|
||||||
"\xef\x82\x8a\xa1y \x9d\xc3\x15\xf4\x0f\xd9\xfcNf\xb9" +
|
"\xe7\x18\xd7B\x09\xa9\xae[\xc9\x15\xbfMz\xea\xddJ" +
|
||||||
"\x7f\xd13\x94\x1e\x01B\xb8\xcc\x9f\xdb\xee\x043cC" +
|
"\xa4\x1a\xdcVd\xb0~\xdf\x1bS\x0c\x9eJz7\x84" +
|
||||||
"q=\x18\xce\x0c\xfep\xe9\xa7\xf0\xf7S\xf6\xbd\x9e3" +
|
"\xbd\x0b*\x86&\x88t\x0eW\xd2?d\xf3;\x99\xe5" +
|
||||||
"\xf4\x86\xa6\xd8/\xe2Q^\x12\x8a=\xa3\xa2\x90\xe6Q" +
|
"\xfe\xa2\x87+=\x1b\x84p\x99?\xb7\xdd)f\xc6\x87" +
|
||||||
"\xce\x05E<\x9b\x08\xa6\x16\"\xcf\x15\xc6\x16T(r" +
|
"\xe2z0\xce\x19\xfc\xa9\xd3O\xe1\xef\xa7\xec{=g" +
|
||||||
"<\xca+8\x8c\xd1+\x13\xab\x83)n/\xa3{\xbf" +
|
"\xe8\xb9C\xb1_\xc4\xa3\xbc$\x14{\xd6\xda\xcfs'" +
|
||||||
"\xac\x89\x0a\xadZ\x9a\x01.\xf3\xd8\x1c*\x1f\xfe<s" +
|
"\x15\xcc9D\x9e+\x0d:\xa8P\x14x\x94Wp\x18" +
|
||||||
"\xf0\xeelhn{]\xef\xa0\x80\xfb3\xc2\xa2\x93\xcf" +
|
"\xa7w)\xd6\x04s\xdf^F\xf7~\x8b\x13\x15Z\xb4" +
|
||||||
"\xfa\xe4jt\x0f%\x9e\x8dt\x06&\xde\xbc\x14\xbd\xc9" +
|
",\x03\\\xe6\xb19T>\xfc\x09\xe8\xe0\xdd\xd9\xd0\xdc" +
|
||||||
"\x9b\xb8w9p\xe2\xa3\x02\x063E\xf4F\x88\xe2\x0e" +
|
"\xf6\xba\xdeA\x01\xf7\xa7\x8ae'\x9f\xf5\xc9\xd5\xe0\x1e" +
|
||||||
"\x038q\xab\x80\x9c?\x81Fo\xd2,n\xbc\x0d8" +
|
"J<\x1b\xed\x8cX\xbc\x09+z\xb3:q\xefr\xe0" +
|
||||||
"q\xbd\x80\xbc?@Fov5\xa1g\x18\x02'\xae" +
|
"\xc4\xc7\x05\x0c\xa6\x90\xe8\x0d\x1d\xc5\x1d\x06p\xe2V\x01" +
|
||||||
"\x140\xe2\x0f\xe6\xd1\x9b|\x89K\xbb\x80\x13U\x01\xa3" +
|
"9\x7ff\x8d\xdelZ\xdcx\x07p\xe2z\x01y\x7f" +
|
||||||
"\xfel\x1a\xbd\xe1\xa8x\xedZ\xe0\xc4\xf9\xc1\x84\x06\x1a" +
|
"\xe4\x8c\xde\xb4kJ\xcf0\x04N\\)`\xc4\x1f\xe5" +
|
||||||
"]?\x9a\xd0\xf68\x0a\xf5\x0eK{\xcfk\xdcU\x00" +
|
"\xa37+\x13\x97v\x01'\xaa\x02F\xfdi6z\xe3" +
|
||||||
"Mh{=0\x7f\xb6&\xd8Y\xe5\x8d\x1c \x96R" +
|
"T\xf1\xfa\xb5\xc0\x89\xed\xc1L\x07\x1a\\?\x1a\xd1\xf6" +
|
||||||
",\xd6D\xcd\x99{\xff\xb1\x90\x00\xa0\x09\xe5\x08\x86\x06" +
|
"8\x0au\x0eK{Ox\xdcU\x00\x8dh{=0" +
|
||||||
"\x7f\x00\xe7\xfb\xbeL\xb0z'\xce\xdf\xb4e\xf2\xf6\x7f" +
|
"\x7f\xb6&\xd8Y\xe5\x0d) \x9eQ,\xd6H\xcd\x99" +
|
||||||
"\xc3\x94\xc4\xf7g5\x9d\xe3\x8f\xaeBz\xbbB\x0f\xdf" +
|
"{\xff\xb1\x94\x00\xa0\x11\xe5\x08\x86F\x85\x00\xe7\xfb\xbe" +
|
||||||
"A\x1a\xbf\xc8\xd9\xbc\xf0\xc8\x1f\xa3\xcd\xa4\xff\x7f}\xfd" +
|
"L\xb1:'\xce\xdf\xb6e\xf2\xf6\x7f\xcb\x94\xc4\xf7g" +
|
||||||
"\x07\xa8qz\x83G\xf9p\xe8Z\x1f\"\xe1\xdb<\xca" +
|
"5\x9d\xe3\x0f\xbbBz\xbbB\x0f\xdfA\x1a\xbf\xc8\xd9" +
|
||||||
"GB\x8d\xd3\xbbt\xd7\x0f\xf3(\x9f\x0e\xa6\x91\x9f\xde" +
|
"\xbc\xf0\xc8\x1f\xa7\xcd\xa4\xff\xdf}\xfd\x87\xa8qz\x83" +
|
||||||
"\x06 \x9f\xe61\x11jD\xc4\xafh\xe1\x97T\xae\x9d" +
|
"G\xf9H\xe8Z\xbfK\xc2\xb7y\x94\x8f\x85\x1a\xa7\xa3" +
|
||||||
"6\x04\xdd6$\x8a\x9b\x01\x92\xa5T\xc6\xe3N\x1b\x12" +
|
"t\xd7\x8f\xf0(\x9f\x09\xe6\x97\x9f\xdd\x01 \x9f\xe11" +
|
||||||
"q\xdb\x10\x11;\x00\x92\xd5$\xbf \xdc\x86\xd4\xe2\x02" +
|
"\x15jD\xc4\xbf\xd3\xc2\xaf\xa9\\;m\x08\xbamH" +
|
||||||
"\x80\xe4H\x92\x8f\xc6\xde\xef\x1a!o\x04\x8dZF\xef" +
|
"\x147\x03\xa4+\xa9\x8c'\x9c6$\xe2\xb6!\"v" +
|
||||||
"\x9c\xa5j\xfd\xd66o<\x8a\xd6LE\xcd\xe4\x0d\x06" +
|
"\x00\xa4kH~A\xb8\x0d\x19\x83\xd7\x01\xa4G\x93|" +
|
||||||
"Ai-$\x9b\xe6P\xb5w\xe7\xa6\xd3\x17\x13\x8d\x93" +
|
"<\xf6~\xd7\x08E#h\xd4rz\xe7\x1cU\xeb\xb7" +
|
||||||
"D\xc24\x9a\xfeL\xe5\x1c^\x94C\xaa<-\x86\xa1" +
|
"\xb6y\x03U\xb4f+j\xaeh0\x08Jk)\xd9" +
|
||||||
"\xa3Q\xd4\xc4N\x0c\x9aX\xbf\x87]\x10\x8c\x87D\xae" +
|
"4\x85\xaa\xbd;iu\x87*i\"a\x16M\x7f\xe0" +
|
||||||
"\xa90\x1f\xea\x08\xda\xee\xfa\x94\x927Y\x1f\x1f\x80g" +
|
"r\x0e/\xca!U\x9ef\xc3\xd0\xd1(kb\xa7\x06" +
|
||||||
"\x86?\x050\x97\xe8\xf9L:\xc1@\xb0\x8c\x9e\"\x08" +
|
"M\xac\xdf\xc3R/~5\x8f\xf2\x02\x0aE\xa3\x1b\x0a" +
|
||||||
"\x06mf\x93,\xe6e.w\xd4\xeb\xfdl\x81\xde\xaf" +
|
"\xb9#h\xbb\xeb2J\xd1d}|\x00\x9e\x19\xfe\x14" +
|
||||||
"\x13\xa1Q\xaf7oG\xefG\xa8\xbe\xa3^\x0f\x83>" +
|
"\xc0\\\xa2\x17s\xd9\x14\x03\xc12z\xca \x18\xb4\x99" +
|
||||||
"\xa3^\xf7\x83\xc3\xd1\xde\xa3\xde\xf3x\xbe\xbae,\x94" +
|
"M\xb3\xb8\x97\xb9\xdc\xe1\xb0\xf7\x8f\x0e\xf4\xfe\x9f\x11\x1a" +
|
||||||
"1\xcei\x02:\xe4\xc1\xa1\xff;m\xd1M/;\xdf" +
|
"\x0e{\x13z\xf4\xfem\xd5w8\xeca\xd0g8\xec" +
|
||||||
"1\x81W\x90\xfe\x1d\x00\x00\xff\xff#\xafZ\xc1"
|
"~p8\xda{8|\x1e\xcfW\xb7\x8c\x852\xc69" +
|
||||||
|
"\xcdL\x87<j\xf4\xff\xb3[v\xd3\xab\xcewL\xe0" +
|
||||||
|
"\x15\xa4\x7f\x04\x00\x00\xff\xff\xa5\x0ed\xc9"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
schemas.Register(schema_db8274f9144abc7e,
|
schemas.Register(schema_db8274f9144abc7e,
|
||||||
|
|
Loading…
Reference in New Issue