diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..a0182df --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,64 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + args: "--out-${NO_FUTURE}format colored-line-number" + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the all caching functionality will be complete disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... -gcflags=all=-l -coverprofile=coverage.txt -covermode=atomic -race + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index 3b46293..1cabe64 100644 --- a/.gitignore +++ b/.gitignore @@ -18,9 +18,8 @@ src/ pkg/ bin/ dist/ -go.sum delegated-apnic-latest cnip.txt -gfwlist*.txt +/gfwlist*.txt coverage.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a3d304f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: go - -go: - - 1.13.x - -before_install: - - go get -t -v ./... - -script: - - go build -o ts-dns github.com/wolf-joe/ts-dns/cmd - - go test -race ./... -gcflags=all=-l -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index 2f1a4f8..6e510eb 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,31 @@ # Telescope DNS [![GitHub release (latest by date)](https://img.shields.io/github/v/release/wolf-joe/ts-dns)](https://github.com/wolf-joe/ts-dns/releases) -[![Build Status](https://travis-ci.org/wolf-joe/ts-dns.svg?branch=master)](https://travis-ci.org/wolf-joe/ts-dns) +[![Go](https://github.com/wolf-joe/ts-dns/actions/workflows/go.yml/badge.svg)](https://github.com/wolf-joe/ts-dns/actions/workflows/go.yml) [![codecov](https://codecov.io/gh/wolf-joe/ts-dns/branch/master/graph/badge.svg)](https://codecov.io/gh/wolf-joe/ts-dns) [![Go Report Card](https://goreportcard.com/badge/github.com/wolf-joe/ts-dns)](https://goreportcard.com/report/github.com/wolf-joe/ts-dns) ![GitHub](https://img.shields.io/github/license/wolf-joe/ts-dns) -> 简单易用的DNS分组/转发器 +> 灵活快速的DNS分组转发器 -## 基本特性 +## 设计目标 +### 灵活解析 +* 支持按ABP风格规则/`GFWList`对DNS请求进行分组 +* 支持按CIDR对DNS请求进行重定向 +* 支持DNS over UDP/TCP/TLS/HTTPS、socks5代理、ECS +* 支持将查询结果中的IPv4地址添加至IPSet +### 快速解析 +* 支持并发请求上游DNS,选择最快响应 +* 选择ping值最低的IPv4地址(tcp/icmp ping) +* 支持hosts/DNS缓存/屏蔽指定查询类型 +* 支持热重载配置文件 -* 默认基于`CN IP列表` + `GFWList`进行域名分组; -* 支持DNS over UDP/TCP/TLS/HTTPS、非标准端口DNS; -* 支持选择ping值最低的IPv4地址(tcp/icmp ping); -* 支持并发请求/socks5代理请求上游DNS,支持附带指定ECS信息; -* 支持多Hosts文件 + 自定义Hosts、通配符Hosts; -* 支持配置文件自动重载,支持监听TCP/UDP端口; -* 支持DNS查询缓存(IP乱序、TTL倒计时、ECS); -* 支持屏蔽指定查询类型; -* 支持将查询结果中的IPv4地址添加至IPSet。 +## 解析流程 +![arch.drawio.svg](doc/arch.drawio.svg) -## DNS查询请求处理流程 - -1. 当域名匹配指定规则(配置文件里各组的`rules`)时,将请求转发至对应组上游DNS并直接返回; -2. 如未匹配规则,则假设域名为`clean`组,向`clean`组的上游DNS转发查询请求,并做如下判断: - * 如果查询结果中所有IPv4地址均为`CN IP`,则直接返回; - * 如果查询结果中出现非`CN IP`,进一步判断: - * 如果该域名匹配GFWList列表,则向`dirty`组的上游DNS转发查询请求并返回; - * 否则返回查询结果。 +``` +查找hosts -> 查找缓存 -> 匹配规则 -> 指定group处理 -> 重定向 -> 设置缓存 +``` ## 使用说明 @@ -36,8 +34,8 @@ ```shell # ./ts-dns -h # 显示命令行帮助信息 # ./ts-dns -c ts-dns.toml # 指定配置文件名 - # ./ts-dns -r # 自动重载配置文件 ./ts-dns + kill -SIGHUP # 重载配置文件 ``` ## 配置示例 @@ -47,8 +45,6 @@ 1. 默认配置(`ts-dns.toml`),开箱即用 ```toml listen = ":53" - gfwlist = "gfwlist.txt" - cnip = "cnip.txt" [groups] [groups.clean] @@ -56,7 +52,8 @@ concurrent = true [groups.dirty] - dns = [""] # 省略 + dns = [""] # 省略 + gfwlist_file = "gfwlist.txt" ``` 2. 选择ping值最低的IPv4地址(启用时建议以root权限运行本程序) @@ -112,9 +109,12 @@ ``` -## TODO +## 未来规划 -* 设置fallback DNS +- [ ] 支持定期拉取最新gfwlist +- [ ] 支持http接口管理 +- [ ] 降低gfwlist的匹配优先级 +- [ ] DoT/GFWList域名解析自闭环 ## 特别鸣谢 * [github.com/arloan/prdns](https://github.com/arloan/prdns) diff --git a/cache/dns.go b/cache/dns.go index 3c75ce9..1dfa75d 100644 --- a/cache/dns.go +++ b/cache/dns.go @@ -1,53 +1,120 @@ package cache import ( + "fmt" + "github.com/miekg/dns" + "github.com/valyala/fastrand" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/utils" "strconv" "strings" + "sync" "time" - - "github.com/miekg/dns" - "github.com/valyala/fastrand" - "github.com/wolf-joe/ts-dns/core/common" ) const ( - DefaultSize = 4096 // DefaultSize 默认dns缓存大小 DefaultMinTTL = time.Minute // DefaultMinTTL 默认dns缓存最小有效期 DefaultMaxTTL = 24 * time.Hour // DefaultMaxTTL 默认dns缓存最大有效期 ) -// DNSCache DNS响应缓存器 -type DNSCache struct { - ttlMap *TTLMap - size int - minTTL time.Duration - maxTTL time.Duration +// IDNSCache cache dns response for dns request +type IDNSCache interface { + // Get find cached response + Get(req *dns.Msg) *dns.Msg + // Set save response to cache + Set(req *dns.Msg, resp *dns.Msg) + // Start life cycle begin + Start(cleanTick ...time.Duration) + // Stop life cycle end + Stop() +} + +func NewDNSCache(conf config.Conf) (IDNSCache, error) { + minTTL, maxTTL := DefaultMinTTL, DefaultMaxTTL + if conf.Cache.MinTTL > 0 { + minTTL = time.Second * time.Duration(conf.Cache.MinTTL) + } + if conf.Cache.MaxTTL > 0 { + maxTTL = time.Second * time.Duration(conf.Cache.MaxTTL) + } + if minTTL > maxTTL { + return nil, fmt.Errorf("min ttl(%d) larger than max ttl(%d)", conf.Cache.MinTTL, conf.Cache.MaxTTL) + } + c := &dnsCache{ + items: map[string]cacheItem{}, + lock: new(sync.RWMutex), + stopCh: make(chan struct{}), + stopped: make(chan struct{}), + maxSize: conf.Cache.Size, + minTTL: minTTL, + maxTTL: maxTTL, + } + return c, nil +} + +var ( + _ IDNSCache = &dnsCache{} +) + +type cacheItem struct { + resp *dns.Msg + expiredAt int64 +} + +type dnsCache struct { + items map[string]cacheItem + lock *sync.RWMutex + stopCh chan struct{} + stopped chan struct{} + + maxSize int + minTTL time.Duration + maxTTL time.Duration } -// dns响应的包裹,用以实现动态ttl -type cacheEntry struct { - r *dns.Msg - expire time.Time +func (c *dnsCache) cacheKey(req *dns.Msg) string { + question := req.Question[0] + key := question.Name + strconv.FormatInt(int64(question.Qtype), 10) + if subnet := utils.FormatECS(req); subnet != "" { + key += "." + subnet + } + return strings.ToLower(key) } -func (entry *cacheEntry) Get() *dns.Msg { - var ttl int64 - if ttl = entry.expire.Unix() - time.Now().Unix(); ttl < 0 { +func (c *dnsCache) Get(req *dns.Msg) *dns.Msg { + if c.maxSize <= 0 { + return nil + } + // check cache + key := c.cacheKey(req) + c.lock.RLock() + item, exists := c.items[key] + c.lock.RUnlock() + if !exists { + return nil + } + // ttl countdown + ttl := item.expiredAt - time.Now().Unix() + if ttl <= 0 { + // remove expired item + c.lock.Lock() + delete(c.items, key) + c.lock.Unlock() return nil } - r := entry.r.Copy() - for i := 0; i < len(r.Answer); i++ { // 倒计时ttl + r := item.resp.Copy() + r.SetReply(req) + for i := 0; i < len(r.Answer); i++ { r.Answer[i].Header().Ttl = uint32(ttl) } - // 打乱ip响应顺序 + // shuffle ip first := uint32(len(r.Answer)) for ; first > 0; first-- { if t := r.Answer[first-1].Header().Rrtype; t != dns.TypeA && t != dns.TypeAAAA { break } } - ips := r.Answer[first:] // 切片不重新分配内存,修改ips相当于直接修改r.Answer - if len(ips) > 1 { + if ips := r.Answer[first:]; len(ips) > 1 { for i := uint32(len(ips) - 1); i > 0; i-- { j := fastrand.Uint32n(i + 1) ips[i], ips[j] = ips[j], ips[i] @@ -56,51 +123,68 @@ func (entry *cacheEntry) Get() *dns.Msg { return r } -// Get 获取DNS响应缓存,响应的ttl为倒计时形式 -func (cache *DNSCache) Get(request *dns.Msg) *dns.Msg { - question := request.Question[0] - cacheKey := question.Name + strconv.FormatInt(int64(question.Qtype), 10) - if subnet := common.FormatECS(request); subnet != "" { - cacheKey += "." + subnet - } - cacheKey = strings.ToLower(cacheKey) - if cacheHit, ok := cache.ttlMap.Get(cacheKey); ok { - r := cacheHit.(*cacheEntry).Get() - return r - } - return nil -} - -// Set 设置DNS响应缓存,缓存的ttl由minTTL、maxTTL、响应本身的ttl共同决定 -func (cache *DNSCache) Set(request *dns.Msg, r *dns.Msg) { - question := request.Question[0] - if cache.ttlMap.Len() >= cache.size || r == nil || len(r.Answer) <= 0 { +func (c *dnsCache) Set(req *dns.Msg, resp *dns.Msg) { + if c.maxSize <= 0 || resp == nil || len(resp.Answer) == 0 { return } - cacheKey := question.Name + strconv.FormatInt(int64(question.Qtype), 10) - if subnet := common.FormatECS(request); subnet != "" { - cacheKey += "." + subnet + // check size + c.lock.RLock() + length := len(c.items) + c.lock.RUnlock() + if length >= c.maxSize { + return } - cacheKey = strings.ToLower(cacheKey) - var ex = cache.maxTTL - for _, answer := range r.Answer { - if ttl := time.Duration(answer.Header().Ttl) * time.Second; ttl < ex { - ex = ttl + // reset ttl + key := c.cacheKey(req) + var expire = c.maxTTL + for _, answer := range resp.Answer { + if ttl := time.Duration(answer.Header().Ttl) * time.Second; ttl < expire { + expire = ttl } } - if ex < cache.minTTL { - ex = cache.minTTL + if expire < c.minTTL { + expire = c.minTTL } - for i := 0; i < len(r.Answer); i++ { - r.Answer[i].Header().Ttl = uint32(ex.Seconds()) + for i := 0; i < len(resp.Answer); i++ { + resp.Answer[i].Header().Ttl = uint32(expire.Seconds()) } - entry := &cacheEntry{r: r, expire: time.Now().Add(ex)} - cache.ttlMap.Set(cacheKey, entry, ex) + // set cache + expiredAt := time.Now().Add(expire).Unix() + c.lock.Lock() + c.items[key] = cacheItem{resp: resp, expiredAt: expiredAt} + c.lock.Unlock() +} + +func (c *dnsCache) Start(_cleanTick ...time.Duration) { + c.stopCh = make(chan struct{}) + c.stopped = make(chan struct{}) + go func() { + cleanTick := time.Minute + if len(_cleanTick) > 0 { + cleanTick = _cleanTick[0] + } + tk := time.NewTicker(cleanTick) + for { + select { + case <-tk.C: + // clean expired key + c.lock.Lock() + for key, item := range c.items { + if time.Now().Unix() >= item.expiredAt { + delete(c.items, key) + } + } + c.lock.Unlock() + case <-c.stopCh: + tk.Stop() + close(c.stopped) + return + } + } + }() } -// NewDNSCache 生成一个DNS响应缓存器实例。如果maxTTL为负数且minTTL也为负数,或size为负数,则缓存会立即失效 -func NewDNSCache(size int, minTTL, maxTTL time.Duration) (c *DNSCache) { - c = &DNSCache{size: size, minTTL: minTTL, maxTTL: maxTTL} - c.ttlMap = NewTTLMap(time.Minute) - return +func (c *dnsCache) Stop() { + close(c.stopCh) + <-c.stopped } diff --git a/cache/dns_test.go b/cache/dns_test.go index 69aafc2..9f97228 100644 --- a/cache/dns_test.go +++ b/cache/dns_test.go @@ -3,67 +3,66 @@ package cache import ( "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/wolf-joe/ts-dns/config" "testing" "time" ) -func TestCacheEntry(t *testing.T) { - resp := &dns.Msg{} - rr, _ := dns.NewRR("ip.cn. 0 IN A 1.1.1.1") // 实际ttl为-1 +func TestNewDNSCache(t *testing.T) { + req := new(dns.Msg) + req.SetQuestion("z.cn.", dns.TypeA) + c, err := NewDNSCache(config.Conf{Cache: config.CacheConf{ + Size: 0, MinTTL: 0, MaxTTL: 0, + }}) + assert.Nil(t, err) + + resp := new(dns.Msg) + rr, _ := dns.NewRR("z.cn. 0 IN A 1.1.1.1") + resp.Answer = append(resp.Answer, rr) + rr, _ = dns.NewRR("z.cn. 0 IN A 1.1.1.2") resp.Answer = append(resp.Answer, rr) - entry := cacheEntry{r: resp, expire: time.Now().Add(time.Second)} // ttl覆盖为1 - assert.True(t, entry.Get().Answer[0].Header().Ttl > 0) + c.Set(req, resp) + assert.Nil(t, c.Get(req)) + + c, err = NewDNSCache(config.Conf{Cache: config.CacheConf{ + Size: 1024, MinTTL: 1, MaxTTL: 3600, + }}) + assert.Nil(t, err) + + c.Start(time.Second) + defer c.Stop() + c.Set(req, resp) + assert.NotNil(t, c.Get(req)) + t.Log(c.Get(req)) + // expired by clean goroutine + time.Sleep(time.Second * 2) + assert.Nil(t, c.Get(req)) + + c.Stop() + c.Start(time.Minute) + // expired by get + c.Set(req, resp) + assert.NotNil(t, c.Get(req)) time.Sleep(time.Second * 2) - assert.Nil(t, entry.Get()) + assert.Nil(t, c.Get(req)) } -func TestGetDNSCache(t *testing.T) { - request1, request2, resp := &dns.Msg{}, &dns.Msg{}, &dns.Msg{} - rr, _ := dns.NewRR("ip.cn. 0 IN A 1.1.1.1") - resp.Answer = append(resp.Answer, rr) - request1.SetQuestion("ip.cn.", dns.TypeA) - request2.SetQuestion("ip.cn.", dns.TypeAAAA) - opt := &dns.EDNS0_SUBNET{Address: []byte("1.1.1.1"), SourceNetmask: 24} - request2.Extra = append(request2.Extra, &dns.OPT{Option: []dns.EDNS0{opt}}) +func BenchmarkNewDNSCache(b *testing.B) { + req := new(dns.Msg) + req.SetQuestion("z.cn.", dns.TypeA) + c, err := NewDNSCache(config.Conf{Cache: config.CacheConf{ + Size: 1024, MinTTL: 60, MaxTTL: 3600, + }}) + assert.Nil(b, err) - // 缓存立即失效 - cache := NewDNSCache(1, -100, -1) - cache.Set(request1, resp) - assert.True(t, cache.Get(request1) == nil) - cache = NewDNSCache(1, -1, -100) - cache.Set(request1, resp) - assert.True(t, cache.Get(request1) == nil) - cache = NewDNSCache(-1, DefaultMinTTL, DefaultMaxTTL) - cache.Set(request1, resp) - assert.True(t, cache.Get(request1) == nil) - // 缓存未立即失效 - cache = NewDNSCache(DefaultSize, DefaultMinTTL, DefaultMaxTTL) - cache.Set(request1, resp) - assert.True(t, cache.Get(request1) != nil) - cache = NewDNSCache(1, time.Second, time.Second) - cache.Set(request1, resp) - assert.True(t, cache.Get(request1) != nil) - // 插入失败 - cache.Set(request2, resp) - assert.True(t, cache.ttlMap.Len() == 1) - // 1秒钟后缓存失效 - time.Sleep(time.Second) - assert.True(t, cache.Get(request1) == nil) - assert.True(t, cache.ttlMap.Len() == 0) - cache.Set(request2, resp) - assert.True(t, cache.ttlMap.Len() == 1) - assert.True(t, cache.Get(request2) != nil) -} + resp := new(dns.Msg) + rr, _ := dns.NewRR("z.cn. 0 IN A 1.1.1.1") + resp.Answer = append(resp.Answer, rr) + rr, _ = dns.NewRR("z.cn. 0 IN A 1.1.1.2") + resp.Answer = append(resp.Answer, rr) -func TestTTLRewrite(t *testing.T) { - rr0, _ := dns.NewRR("ip.cn. 0 IN CNAME xxx") - rr1, _ := dns.NewRR("ip.cn. 0 IN A 1.1.1.1") - rr2, _ := dns.NewRR("ip.cn. 0 IN A 1.1.1.2") - req, resp := &dns.Msg{}, &dns.Msg{Answer: []dns.RR{rr0, rr1, rr2}} - req.SetQuestion("ip.cn.", dns.TypeA) - cache := NewDNSCache(1, time.Minute, time.Hour*24) - cache.Set(req, resp) - assert.NotEqual(t, resp.Answer[0].Header().Ttl, uint32(0)) - // 顺便测试random record order - cache.Get(req) + for i := 0; i < b.N; i++ { + c.Set(req, resp) + assert.NotNil(b, c.Get(req)) + } } diff --git a/cache/ramset.go b/cache/ramset.go deleted file mode 100644 index ddb26ed..0000000 --- a/cache/ramset.go +++ /dev/null @@ -1,55 +0,0 @@ -package cache - -import ( - "io/ioutil" - "net" - "regexp" - "strings" -) - -// RamSet 在go内存中的ipset -type RamSet struct { - subnet []*net.IPNet - ipMap map[string]bool -} - -// Contain 判断目标ip是否在范围内 -func (s *RamSet) Contain(target net.IP) bool { - if _, ok := s.ipMap[target.String()]; ok { - return true - } - for _, subnet := range s.subnet { - if subnet.Contains(target) { - return true - } - } - return false -} - -// NewRamSetByText 用文本内容初始化一个RamSet,每行一个ip/网段 -func NewRamSetByText(text string) (s *RamSet) { - s = &RamSet{subnet: []*net.IPNet{}, ipMap: map[string]bool{}} - v4reg := regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`) - cidr4reg := regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$`) - for _, line := range strings.Split(text, "\n") { - line = strings.Trim(line, " \t\n\r") - if v4reg.MatchString(line) { - s.ipMap[net.ParseIP(line).String()] = true - } - if cidr4reg.MatchString(line) { - if _, subnet, err := net.ParseCIDR(line); err == nil { - s.subnet = append(s.subnet, subnet) - } - } - } - return s -} - -// NewRamSetByFile 用文件内容初始化一个RamSet,每行一个ip/网段 -func NewRamSetByFile(filename string) (matcher *RamSet, err error) { - var raw []byte - if raw, err = ioutil.ReadFile(filename); err != nil { - return nil, err - } - return NewRamSetByText(string(raw)), nil -} diff --git a/cache/ramset_test.go b/cache/ramset_test.go deleted file mode 100644 index 3053b0e..0000000 --- a/cache/ramset_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package cache - -import ( - "github.com/stretchr/testify/assert" - "io/ioutil" - "net" - "os" - "testing" -) - -func TestRamSet(t *testing.T) { - text := " 1.2.4.8 \n 8.8.8.8 \n 1.0.0.0/8 \n 192.168.1.1/33 \n ::1 \n ::2 " - filename := "go_test_ips_file" - _ = ioutil.WriteFile(filename, []byte(text), 0644) - // 读取失败 - matcher, err := NewRamSetByFile(filename + "_ne") - assert.True(t, err != nil) - // 读取成功 - matcher, err = NewRamSetByFile(filename) - assert.True(t, matcher != nil) - assert.True(t, err == nil) - if matcher != nil { - assert.False(t, matcher.Contain(nil)) - assert.False(t, matcher.Contain(net.ParseIP("999.9.9.9"))) - assert.True(t, matcher.Contain(net.ParseIP("8.8.8.8"))) - assert.True(t, matcher.Contain(net.ParseIP("1.254.254.254"))) - assert.False(t, matcher.Contain(net.ParseIP("192.168.1.1"))) - } - _ = os.Remove(filename) -} diff --git a/cache/ttlmap.go b/cache/ttlmap.go deleted file mode 100644 index b940d5c..0000000 --- a/cache/ttlmap.go +++ /dev/null @@ -1,71 +0,0 @@ -package cache - -import ( - "sync" - "time" -) - -const ( - minCleanTick = time.Second -) - -type item struct { - value interface{} - expire int64 -} - -// TTLMap 类似redis的超时map -type TTLMap struct { - itemMap map[string]*item - mux *sync.RWMutex -} - -// Set 放入一个指定有效期的对象 -func (m *TTLMap) Set(key string, value interface{}, ex time.Duration) { - m.mux.Lock() - defer m.mux.Unlock() - m.itemMap[key] = &item{value: value, expire: time.Now().Add(ex).UnixNano()} -} - -// Get 取出对象,当后一个返回值为false时代表对象已过期或对象不存在 -func (m *TTLMap) Get(key string) (interface{}, bool) { - // get item, using read lock - m.mux.RLock() - value, ok := m.itemMap[key] - m.mux.RUnlock() - if !ok || time.Now().UnixNano() >= value.expire { - // delete item, use write lock - m.mux.Lock() - delete(m.itemMap, key) - m.mux.Unlock() - return nil, false - } - return value.value, true -} - -// Len 统计map中存在多少对象(包括已过期对象) -func (m TTLMap) Len() int { - m.mux.RLock() - defer m.mux.RUnlock() - return len(m.itemMap) -} - -// NewTTLMap 新建一个超时map,cleanTick为清除过期对象的频率 -func NewTTLMap(cleanTick time.Duration) (m *TTLMap) { - if cleanTick < minCleanTick { - cleanTick = minCleanTick - } - m = &TTLMap{itemMap: map[string]*item{}, mux: new(sync.RWMutex)} - go func() { - for range time.Tick(cleanTick) { - m.mux.Lock() - for key, item := range m.itemMap { - if time.Now().UnixNano() >= item.expire { - delete(m.itemMap, key) - } - } - m.mux.Unlock() - } - }() - return -} diff --git a/cache/ttlmap_test.go b/cache/ttlmap_test.go deleted file mode 100644 index f33ae7a..0000000 --- a/cache/ttlmap_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package cache - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func TestNewTTLMap(t *testing.T) { - ttlMap := NewTTLMap(time.Millisecond * 500) - ttlMap.Set("key1", "value1", time.Millisecond*500) - ttlMap.Set("key2", "value2", time.Millisecond*500) - val, ok := ttlMap.Get("key1") - assert.Equal(t, val, "value1") - assert.Equal(t, ok, true) - - time.Sleep(time.Millisecond * 600) - val, ok = ttlMap.Get("key1") - assert.Equal(t, val, nil) - assert.Equal(t, ok, false) - // key1在主动访问时被发现失效,从而删除,但key2仍然存在 - assert.Equal(t, ttlMap.Len(), 1) - - // key2被定时clean机制判断失效,从而删除 - time.Sleep(time.Millisecond * 500) - assert.Equal(t, ttlMap.Len(), 0) // -} diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..a3545ba --- /dev/null +++ b/changelog.md @@ -0,0 +1,8 @@ +# v1.0.0 + +- [x] 从配置中移除`query_log`、`gfwlist`、`gfwlist_b64`项 +- [x] 移除针对`dirty`、`clean`组的特殊逻辑 +- [x] 支持为特定组指定`gfwlist`匹配策略、兜底匹配策略 +- [x] 收到`SIGNUP`信号时重载配置文件 +- [x] 支持非CNIP转发到指定组策略 +- [X] 完全重构代码 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index f00bf9e..f28f481 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,29 +1,28 @@ package main import ( - "context" + "bytes" "flag" "fmt" - "os" - "sync" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/fsnotify/fsnotify" + "github.com/BurntSushi/toml" "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/core/model" - "github.com/wolf-joe/ts-dns/core/utils" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/ts-dns/config" "github.com/wolf-joe/ts-dns/inbound" + "os" + "os/signal" + "strings" + "sync" + "syscall" ) // VERSION 程序版本号 var VERSION = "dev" func main() { - ctx := utils.NewCtx(nil, 0xffff) // 读取命令行参数 filename := flag.String("c", "ts-dns.toml", "config file path") - reload := flag.Bool("r", false, "auto reload config file") + listen := flag.String("listen", "", "listen address/port/protocol") showVer := flag.Bool("v", false, "show version and exit") debugMode := flag.Bool("vv", false, "show debug log") flag.Parse() @@ -32,77 +31,74 @@ func main() { os.Exit(0) } if *debugMode { - log.SetLevel(log.DebugLevel) + logrus.SetLevel(logrus.DebugLevel) } // 读取配置文件 - handler, err := model.NewHandler(ctx, *filename) - if err != nil { - os.Exit(1) + conf := config.Conf{} + if _, err := toml.DecodeFile(*filename, &conf); err != nil { + logrus.Fatalf("load config file %q failed: %+v", *filename, err) + } + buf := bytes.NewBuffer(nil) + _ = toml.NewEncoder(buf).Encode(conf) + logrus.Debugf("load config success: %s", buf) + // 解析监听地址 + if *listen == "" { + listen = &conf.Listen + } + addr, network := *listen, "" + if parts := strings.SplitN(*listen, "/", 2); len(parts) == 2 { + addr, network = parts[0], strings.ToLower(parts[1]) } - if *reload { // 自动重载配置文件 - utils.CtxWarn(ctx, "auto reload "+*filename) - go autoReload(ctx, handler, *filename) + if network != "" && network != "udp" && network != "tcp" { + logrus.Fatalf("unknown network: %q", network) } - // 启动dns服务,因为可能会同时监听TCP/UDP,所以封装个函数 + // 构建handler + handler, err := inbound.NewHandler(conf) + if err != nil { + logrus.Fatalf("build handler failed: %+v", err) + } + // 监听SIGNUP命令 + signCh := make(chan os.Signal, 1) + signal.Notify(signCh, syscall.SIGHUP) + go reloadConf(signCh, filename, handler) + + // 启动服务 wg := sync.WaitGroup{} runSrv := func(net string) { defer wg.Done() - srv := &dns.Server{Addr: handler.Listen, Net: net, Handler: handler} - utils.CtxWarn(ctx, "listen on %s/%s", handler.Listen, net) - if err := srv.ListenAndServe(); err != nil { - utils.CtxError(ctx, err.Error()) + srv := &dns.Server{Addr: addr, Net: net, Handler: handler} + logrus.Infof("listen on %s/%s", addr, net) + if err = srv.ListenAndServe(); err != nil { + logrus.Errorf("service stopped: %+v", err) } } - // 判断是否在配置文件里指定了监听协议 - if handler.Network != "" { + if network != "" { wg.Add(1) - go runSrv(handler.Network) + go runSrv(network) } else { wg.Add(2) go runSrv("udp") go runSrv("tcp") } wg.Wait() - utils.CtxInfo(ctx, "ts-dns exited.") + logrus.Infof("ts-dns exists") } -// 持续监测目标配置文件,如文件发生变动则尝试载入,载入成功后更新现有handler的配置 -func autoReload(ctx context.Context, handle *inbound.Handler, filename string) { - ctx = utils.WithFields(ctx, log.Fields{"file": filename}) - // 创建监测器 - watcher, err := fsnotify.NewWatcher() - if err != nil { - utils.CtxError(ctx, "create watcher error: "+err.Error()) - return - } - defer func() { - _ = watcher.Close() - utils.CtxError(ctx, "file watcher closed") - }() - // 指定监测文件 - if err = watcher.Add(filename); err != nil { - utils.CtxError(ctx, "watch file error: "+err.Error()) - return - } - // 接收文件事件 +func reloadConf(ch chan os.Signal, filename *string, handler inbound.IHandler) { for { - select { - case event, ok := <-watcher.Events: // 出现文件事件 - if !ok { - return - } - if event.Op&fsnotify.Write == fsnotify.Write { // 文件变动事件 - utils.CtxWarn(ctx, "file changed, reloading") - if newHandler, err := model.NewHandler(ctx, filename); err == nil { - handle.Refresh(newHandler) - } - } - case err, ok := <-watcher.Errors: // 出现错误 - if !ok { - return - } - utils.CtxError(ctx, "watch error: "+err.Error()) + <-ch + conf := config.Conf{} + if _, err := toml.DecodeFile(*filename, &conf); err != nil { + logrus.Warnf("load config file %q failed: %+v", *filename, err) + continue + } + buf := bytes.NewBuffer(nil) + _ = toml.NewEncoder(buf).Encode(conf) + logrus.Debugf("reload config: %s", buf) + if err := handler.ReloadConfig(conf); err != nil { + logrus.Warnf("reload config failed: %+v", err) + continue } - time.Sleep(time.Second) + logrus.Infof("reload config success") } } diff --git a/config/config.go b/config/config.go index 9c53be3..f1b925e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,15 +1,63 @@ package config type Conf struct { - Listen string - GFWList string - GFWb64 bool `toml:"gfwlist_b64"` - CNIP string - //Logger *QueryLog `toml:"query_log"` - HostsFiles []string `toml:"hosts_files"` - Hosts map[string]string - //Cache CacheConf - //Groups map[string]*Group - DisableIPv6 bool `toml:"disable_ipv6"` - DisableQTypes []string `toml:"disable_qtypes"` + HostsFiles []string `toml:"hosts_files"` + Hosts map[string]string `toml:"hosts"` + Cache CacheConf `toml:"cache"` + + Groups map[string]Group `toml:"groups"` + DisableIPv6 bool `toml:"disable_ipv6"` + DisableQTypes []string `toml:"disable_qtypes"` + Redirectors map[string]RedirectorConf `toml:"redirectors"` + + Listen string `toml:"listen"` +} + +// CacheConf 配置文件中cache section对应的结构 +type CacheConf struct { + Size int `toml:"size"` + MinTTL int `toml:"min_ttl"` + MaxTTL int `toml:"max_ttl"` +} + +// Group 配置文件中每个groups section对应的结构 +type Group struct { + ECS string `toml:"ecs"` + NoCookie bool `toml:"no_cookie"` + + Rules []string `toml:"rules"` + RulesFile string `toml:"rules_file"` + GFWListFile string `toml:"gfwlist_file"` + GFWListURL string `toml:"gfwlist_url"` + Fallback bool `toml:"fallback"` + + Socks5 string `toml:"socks5"` + DNS []string `toml:"dns"` + DoT []string `toml:"dot"` + DoH []string `toml:"doh"` + + Concurrent bool `toml:"concurrent"` + FastestV4 bool `toml:"fastest_v4"` + TCPPingPort int `toml:"tcp_ping_port"` + + IPSet string `toml:"ipset"` + IPSetTTL int `toml:"ipset_ttl"` + + Redirector string `toml:"redirector"` +} + +func (g Group) IsSetGFWList() bool { + return g.GFWListFile != "" || g.GFWListURL != "" +} + +func (g Group) IsEmptyRule() bool { + return len(g.Rules) == 0 && g.RulesFile == "" && !g.IsSetGFWList() +} + +// RedirectorConf 重定向器配置 +type RedirectorConf struct { + Type string `toml:"type"` + Rules []string `toml:"rules"` + RulesFile string `toml:"rules_file"` + DstGroup string `toml:"dst_group"` } diff --git a/core/inbound/base.go b/core/inbound/base.go deleted file mode 100644 index b92182d..0000000 --- a/core/inbound/base.go +++ /dev/null @@ -1,30 +0,0 @@ -package inbound - -import ( - "context" - - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/core/utils" -) - -// Handler DNS请求处理器,用于将请求转发至上游DNS/其它DNS请求处理器 -type Handler interface { - Handle(ctx context.Context, req, resp *dns.Msg) *dns.Msg - String() string -} - -// 判断是否出现递归处理 -func recursiveDetect(ctx context.Context, handler Handler) (context.Context, bool) { - var history map[Handler]bool - if val, ok := ctx.Value(utils.RecHandleKey).(map[Handler]bool); ok { - history = val - } else { - history = make(map[Handler]bool) - ctx = context.WithValue(ctx, utils.RecHandleKey, history) - } - if history[handler] { - return ctx, true - } - history[handler] = true - return ctx, false -} diff --git a/core/inbound/group.go b/core/inbound/group.go deleted file mode 100644 index c1b0755..0000000 --- a/core/inbound/group.go +++ /dev/null @@ -1,215 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "time" - - "github.com/janeczku/go-ipset/ipset" - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/core/common" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" -) - -// Group 域名解析组,负责将DNS请求转发至上游DNS -type Group struct { - name string - Priority int // Priority 匹配优先级 - matcher matcher.DomainMatcher // 域名匹配规则 - - NoCookie bool // NoCookie 是否删除请求中的cookie - WithECS *dns.EDNS0_SUBNET // WithECS 是否在请求中附加ECS信息 - - callers []outbound.Caller // 上游DNS服务器 - Concurrent bool // Concurrent 是否需要并发请求 - - fastestIP bool // 是否对响应中的IP地址进行测速,找出ping值最低的IP地址 - tcpPingPort int // 是否使用tcp ping - - IPSet *ipset.IPSet // IPSet 将响应中的IP地址加入ipset - Next Handler // Next 下一个DNS请求处理器 -} - -// NewGroup 初始化一个解析组。需要匹配规则、上游DNS -func NewGroup(name string, matcher matcher.DomainMatcher, callers []outbound.Caller) *Group { - return &Group{name: name, matcher: matcher, callers: callers} -} - -// WithFastestIP 处理DNS请求时只返回响应里ping值最低的IP地址。当tcpPingPort大于0时使用tcp ping -func (g *Group) WithFastestIP(tcpPingPort int) { - g.fastestIP = true - g.tcpPingPort = tcpPingPort -} - -// Handle 处理DNS请求 -func (g *Group) Handle(ctx context.Context, req, _ *dns.Msg) (resp *dns.Msg) { - utils.CtxDebug(ctx, "handle by "+g.String()) - var recursive bool // 检测是否存在回环处理 - if ctx, recursive = recursiveDetect(ctx, g); recursive { - utils.CtxError(ctx, "handle recursive") - return resp - } - defer func(req *dns.Msg) { - go g.add2IPSet(ctx, resp) - if g.Next != nil { - resp = g.Next.Handle(ctx, req, resp) - } - }(req) - - if g.NoCookie || g.WithECS != nil { - // 预处理请求 - req = req.Copy() - if g.NoCookie { - common.RemoveEDNSCookie(req) - } - if g.WithECS != nil { - common.SetDefaultECS(req, g.WithECS) - } - } - - if !g.Concurrent && !g.fastestIP { - // 依次请求上游DNS - for _, caller := range g.callers { - resp, err := caller.Call(req) - if err == nil { - return resp - } - utils.CtxWarn(ctx, "query dns error: "+err.Error()) - } - return nil - } - // 并发请求上游DNS - chLen := len(g.callers) - respCh := make(chan *dns.Msg, chLen) - for _, caller := range g.callers { - go func(c outbound.Caller) { - resp, err := c.Call(req) - if err == nil { - respCh <- resp - } else { - utils.CtxWarn(ctx, "query dns error: "+err.Error()) - respCh <- nil - } - }(caller) - } - - // 处理响应 - var qType uint16 - if len(req.Question) > 0 { - qType = req.Question[0].Qtype - } - if !g.fastestIP || (qType != dns.TypeA && qType != dns.TypeAAAA) { - // 无需测速,只需返回第一个不为nil的DNS响应 - for i := 0; i < chLen; i++ { - if resp := <-respCh; resp != nil { - return resp - } - } - return nil - } - return g.fastestResp(ctx, qType, respCh, chLen) -} - -// 寻找响应里ping值最低的IP地址 -func (g *Group) fastestResp(ctx context.Context, qType uint16, respCh chan *dns.Msg, chLen int) *dns.Msg { - const ( - maxGoNum = 15 // 最大并发量 - pingTimeout = 500 * time.Millisecond - ) - // 从resp ch中提取所有IP地址,并建立IP地址到resp的映射 - allIP := make([]string, 0, maxGoNum) - respMap := make(map[string]*dns.Msg, maxGoNum) - var firstResp *dns.Msg // 最早抵达的msg,当测速失败时返回该响应 - for i := 0; i < chLen; i++ { - resp := <-respCh - if resp == nil { - continue - } - if firstResp == nil { - firstResp = resp - } - for _, answer := range resp.Answer { - var ip string - switch rr := answer.(type) { - case *dns.A: - if qType == dns.TypeA { - ip = rr.A.String() - } - case *dns.AAAA: - if qType == dns.TypeAAAA { - ip = rr.AAAA.String() - } - } - if ip != "" { - allIP = append(allIP, ip) - if _, exists := respMap[ip]; !exists { - respMap[ip] = resp - if len(respMap) >= maxGoNum { - goto doPing - } - } - } - } - } -doPing: - switch len(respMap) { - case 0: // 没有任何IP地址 - return firstResp - case 1: // 只有一个IPv4地址 - for _, resp := range respMap { - return resp - } - } - fastestIP, cost, err := utils.FastestPingIP(ctx, allIP, g.tcpPingPort, pingTimeout) - if err != nil { - return firstResp - } - utils.CtxDebug(ctx, "fastest ip of %s: %s(%dms)", allIP, fastestIP, cost) - msg := respMap[fastestIP] - // 删除msg内除fastestIP之外的其它IP记录 - for i := 0; i < len(msg.Answer); i++ { - switch rr := msg.Answer[i].(type) { - case *dns.A: - if qType == dns.TypeA && rr.A.String() != fastestIP { - goto delThis - } - case *dns.AAAA: - if qType == dns.TypeAAAA && rr.AAAA.String() != fastestIP { - goto delThis - } - } - continue - delThis: - msg.Answer = append(msg.Answer[:i], msg.Answer[i+1:]...) - i-- - } - return msg -} - -// 将响应中的IP地址加入ipset -func (g *Group) add2IPSet(ctx context.Context, resp *dns.Msg) { - if resp == nil || g.IPSet == nil { - return - } - for _, answer := range resp.Answer { - if a, ok := answer.(*dns.A); ok { - if err := g.IPSet.Add(a.A.String(), g.IPSet.Timeout); err != nil { - utils.CtxWarn(ctx, "add %s to ipset<%s> error: %s", a.A, g.IPSet.Name, err) - } - } - } -} - -// String 描述自身 -func (g *Group) String() string { - return fmt.Sprintf("Group<%s,%d>", g.name, len(g.callers)) -} - -// Exit 停止服务 -func (g *Group) Exit() { - for _, caller := range g.callers { - caller.Exit() - } -} diff --git a/core/inbound/group_test.go b/core/inbound/group_test.go deleted file mode 100644 index 3713b7e..0000000 --- a/core/inbound/group_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package inbound - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/Sirupsen/logrus" - "github.com/janeczku/go-ipset/ipset" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/core/utils/mock" - "github.com/wolf-joe/ts-dns/outbound" -) - -type fakeCaller struct { - latestReq *dns.Msg - sleep time.Duration - resp *dns.Msg - err error -} - -func (f *fakeCaller) Call(req *dns.Msg) (r *dns.Msg, err error) { - f.latestReq = req - time.Sleep(f.sleep) - if f.err != nil { - return nil, f.err - } - return f.resp, nil -} - -func (f *fakeCaller) Exit() { -} - -func (f *fakeCaller) String() string { - return fmt.Sprintf("fakeCaller<%s,%p,%p>", f.sleep, f.resp, f.err) -} - -func newFakeCaller(sleep time.Duration, resp *dns.Msg, err error) *fakeCaller { - return &fakeCaller{sleep: sleep, resp: resp, err: err} -} - -func TestGroup(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) - req := &dns.Msg{Question: []dns.Question{{Qtype: dns.TypeA}}} - mockResp := &dns.Msg{Answer: []dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}}} - cErr := newFakeCaller(50*time.Millisecond, nil, errors.New("err by mock")) - cMock := newFakeCaller(100*time.Millisecond, mockResp, nil) - cNil := newFakeCaller(100*time.Millisecond, nil, nil) - - group := NewGroup("test", nil, nil) - fmt.Println(group) - assert.Nil(t, group.Handle(ctx, req, nil)) - - group = NewGroup("test", nil, []outbound.Caller{cErr, cMock}) - assert.Equal(t, mockResp, group.Handle(ctx, req, nil)) - - // 0. test next - fmt.Println("test Next") - group.Next = ©RespHandler{} - assert.NotEqual(t, mockResp, group.Handle(ctx, req, nil)) - group.Next = &toNextHandler{next: group} - assert.NotEqual(t, mockResp, group.Handle(ctx, req, nil)) - - // 1. test Concurrent - fmt.Println("test Concurrent") - group.Next = nil - group.Concurrent = true - assert.Equal(t, mockResp, group.Handle(ctx, req, nil)) - - group = NewGroup("test", nil, []outbound.Caller{cErr, cNil}) - group.Concurrent = true - assert.Nil(t, group.Handle(ctx, req, nil)) - - // 2. test WithFastestIP - fmt.Println("test WithFastestIP") - group = NewGroup("test", nil, []outbound.Caller{cErr, cNil}) - group.WithFastestIP(0) - assert.Nil(t, group.Handle(ctx, req, nil)) - - group = NewGroup("test", nil, []outbound.Caller{cErr, cMock}) - group.WithFastestIP(0) - assert.Equal(t, mockResp, group.Handle(ctx, req, nil)) - - mocker := new(mock.Mocker) - defer mocker.Reset() - mockPing := func(ip string, err error) { - mocker.Func(utils.FastestPingIP, func(_ context.Context, _ []string, _ int, _ time.Duration, - ) (string, int64, error) { - return ip, 233, err - }) - } - buildAnswer := func(v6 bool) { - mockResp.Answer = nil - for i := byte(1); i < 20; i++ { - var rr dns.RR - rr = &dns.A{A: []byte{1, 1, 1, i}} - if v6 { - rr = &dns.AAAA{AAAA: append(make([]byte, 10), []byte{0xff, 0xff, 1, 1, 1, i}...)} - } - mockResp.Answer = append(mockResp.Answer, rr) - } - } - buildAnswer(false) - mockPing("", errors.New("timeout")) - assert.Equal(t, mockResp, group.Handle(ctx, req, nil)) - - buildAnswer(false) - mockPing("1.1.1.1", nil) - resp := group.Handle(ctx, req, nil) - assert.NotNil(t, resp) - assert.Equal(t, 1, len(mockResp.Answer)) - - req.Question[0].Qtype = dns.TypeAAAA - buildAnswer(true) - assert.Equal(t, mockResp, group.Handle(ctx, req, nil)) -} - -func TestGroup2(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) - req := &dns.Msg{Question: []dns.Question{{Qtype: dns.TypeA}}} - mockResp := &dns.Msg{Answer: []dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}}} - c := newFakeCaller(100*time.Millisecond, mockResp, nil) - - group := NewGroup("test", nil, []outbound.Caller{c}) - next := ©RespHandler{} - group.Next = next - - c.latestReq, next.latestReq = nil, nil - _ = group.Handle(ctx, req, nil) - assert.NotNil(t, c.latestReq) - assert.NotNil(t, next.latestReq) - assert.Equal(t, c.latestReq, next.latestReq) - - group.NoCookie = true - _ = group.Handle(ctx, req, nil) - assert.NotNil(t, c.latestReq) - assert.NotNil(t, next.latestReq) - assert.NotEqual(t, c.latestReq, next.latestReq) - - group.NoCookie = false - group.WithECS = &dns.EDNS0_SUBNET{} - _ = group.Handle(ctx, req, nil) - assert.NotNil(t, c.latestReq) - assert.NotNil(t, next.latestReq) - assert.NotEqual(t, c.latestReq, next.latestReq) - - // test with ipset - mockResp.Answer = append(mockResp.Answer, &dns.A{A: []byte{1, 1, 1, 2}}) - group = NewGroup("test", nil, []outbound.Caller{c}) - group.IPSet = &ipset.IPSet{} - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Method(group.IPSet, "Add", func(_ *ipset.IPSet, entry string, _ int) error { - if entry == "1.1.1.1" { - return nil - } - return errors.New("err by mock") - }) - _ = group.Handle(ctx, req, nil) - time.Sleep(10 * time.Millisecond) // wait ipset goroutine done -} diff --git a/core/inbound/redirector.go b/core/inbound/redirector.go deleted file mode 100644 index bb63bf6..0000000 --- a/core/inbound/redirector.go +++ /dev/null @@ -1,129 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "net" - - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/matcher" -) - -// IPRedRule IP重定向器规则 -type IPRedRule int - -const ( - // IPRedRuleIfFind 如果响应里出现匹配指定范围的ip地址 - IPRedRuleIfFind IPRedRule = iota - // IPRedRuleIfNotFind 如果响应里未出现匹配指定范围的ip地址 - IPRedRuleIfNotFind -) - -// IPRedirector 基于DNS响应中IP地址的重定向器 -type IPRedirector struct { - ramSet *cache.RamSet - rule IPRedRule - next Handler -} - -// NewIPRedirector 创建一个重定向器 -func NewIPRedirector(ramSet *cache.RamSet, rule IPRedRule, next Handler) *IPRedirector { - return &IPRedirector{ramSet: ramSet, rule: rule, next: next} -} - -// String 描述 -func (red *IPRedirector) String() string { - return fmt.Sprintf("IPRedirector<%d>", red.rule) -} - -// Handle 根据ip地址范围和规则决定是否转发至其它处理器 -func (red *IPRedirector) Handle(ctx context.Context, req, resp *dns.Msg) *dns.Msg { - utils.CtxDebug(ctx, "handle by "+red.String()) - if red.next == nil { - utils.CtxError(ctx, "next not set") - return resp - } - var recursive bool // 检测是否存在回环处理 - if ctx, recursive = recursiveDetect(ctx, red); recursive { - utils.CtxError(ctx, "handle recursive") - return resp - } - var find bool - for _, ans := range resp.Answer { - var ip net.IP - switch rr := ans.(type) { - case *dns.A: - ip = rr.A.To4() - case *dns.AAAA: - ip = rr.AAAA.To16() - default: - continue - } - if red.ramSet.Contain(ip) { - find = true - if red.rule == IPRedRuleIfFind { - return red.next.Handle(ctx, req, resp) - } - } - } - if !find && red.rule == IPRedRuleIfNotFind { - return red.next.Handle(ctx, req, resp) - } - return resp -} - -// DomainRedRule 重定向器规则 -type DomainRedRule int - -const ( - // DomainRedRuleIfMatch 如果请求的域名匹配指定规则 - DomainRedRuleIfMatch DomainRedRule = iota - // DomainRedRuleIfNotMatch 如果请求的域名匹配指定规则 - DomainRedRuleIfNotMatch -) - -// DomainRedirector 基于DNS请求中目标域名的重定向器 -type DomainRedirector struct { - matcher matcher.DomainMatcher - rule DomainRedRule - next Handler -} - -// NewDomainRedirector 创建一个重定向器 -func NewDomainRedirector(matcher matcher.DomainMatcher, rule DomainRedRule, next Handler) *DomainRedirector { - return &DomainRedirector{matcher: matcher, rule: rule, next: next} -} - -// Handle 根据请求域名和规则决定是否转发至其它处理器 -func (red *DomainRedirector) Handle(ctx context.Context, req, resp *dns.Msg) *dns.Msg { - utils.CtxDebug(ctx, "handle by "+red.String()) - if red.next == nil { - utils.CtxError(ctx, "next not set") - return resp - } - var recursive bool // 检测是否存在回环处理 - if ctx, recursive = recursiveDetect(ctx, red); recursive { - utils.CtxError(ctx, "handle recursive") - return resp - } - for _, question := range req.Question { - if match, ok := red.matcher.Match(question.Name); ok && match { - if red.rule == DomainRedRuleIfMatch { - return red.next.Handle(ctx, req, resp) - } - } else { - if red.rule == DomainRedRuleIfNotMatch { - return red.next.Handle(ctx, req, resp) - } - } - break // only care about the first question - } - return resp -} - -// String 描述 -func (red *DomainRedirector) String() string { - return fmt.Sprintf("DomainRedirector<%d>", red.rule) -} diff --git a/core/inbound/redirector_test.go b/core/inbound/redirector_test.go deleted file mode 100644 index b866372..0000000 --- a/core/inbound/redirector_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package inbound - -import ( - "context" - "testing" - - "github.com/Sirupsen/logrus" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/matcher" -) - -type copyRespHandler struct{ latestReq *dns.Msg } - -func (h *copyRespHandler) Handle(_ context.Context, req, resp *dns.Msg) *dns.Msg { - h.latestReq = req - return resp.Copy() -} -func (*copyRespHandler) String() string { return "copyRespHandler" } - -type toNextHandler struct{ next Handler } - -func (h *toNextHandler) Handle(ctx context.Context, req, resp *dns.Msg) *dns.Msg { - return h.next.Handle(ctx, req, resp) -} -func (*toNextHandler) String() string { return "toNextHandler" } - -func TestIPSetRedirector(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) - ramSet := cache.NewRamSetByText("1.1.1.1") - resp := &dns.Msg{} - - redirector := NewIPRedirector(ramSet, IPRedRuleIfFind, nil) - assert.Equal(t, resp, redirector.Handle(ctx, nil, resp)) // next not set - - redirector = NewIPRedirector(ramSet, IPRedRuleIfFind, ©RespHandler{}) - assert.Equal(t, resp, redirector.Handle(ctx, nil, resp)) // not find ip match ramSet - - resp.Answer = append(resp.Answer, &dns.A{A: []byte{1, 1, 1, 1}}) - assert.NotEqual(t, resp, redirector.Handle(ctx, nil, resp)) // find ip, return copy of resp - - resp.Answer = []dns.RR{&dns.AAAA{AAAA: []byte{1, 1, 1, 1, 1, 1, 1, 1}}} - resp.Answer = append(resp.Answer, &dns.DNAME{}) - redirector = NewIPRedirector(ramSet, IPRedRuleIfNotFind, ©RespHandler{}) - assert.NotEqual(t, resp, redirector.Handle(ctx, nil, resp)) // not find ip, return copy of resp - - // test recursive - redirector.next = &toNextHandler{next: redirector} - assert.Equal(t, resp, redirector.Handle(ctx, nil, resp)) -} - -func TestDomainRedirector(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) - rules := matcher.NewABPByText("a.com") - req, resp := &dns.Msg{}, &dns.Msg{} - - redirector := NewDomainRedirector(rules, DomainRedRuleIfMatch, nil) - assert.Equal(t, resp, redirector.Handle(ctx, req, resp)) // next not set - - req.Question = []dns.Question{{Name: "A.COM."}} - redirector = NewDomainRedirector(rules, DomainRedRuleIfMatch, ©RespHandler{}) - assert.NotEqual(t, resp, redirector.Handle(ctx, req, resp)) // matched, return copy of resp - - req.Question = []dns.Question{{Name: "B.COM."}} - assert.Equal(t, resp, redirector.Handle(ctx, req, resp)) // not matched, return resp - - redirector = NewDomainRedirector(rules, DomainRedRuleIfNotMatch, ©RespHandler{}) - assert.NotEqual(t, resp, redirector.Handle(ctx, req, resp)) // not matched, return copy of resp - - // test recursive - redirector.next = &toNextHandler{next: redirector} - assert.Equal(t, resp, redirector.Handle(ctx, req, resp)) -} diff --git a/core/inbound/server.go b/core/inbound/server.go deleted file mode 100644 index 92a6d8a..0000000 --- a/core/inbound/server.go +++ /dev/null @@ -1,258 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - - "github.com/Sirupsen/logrus" - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/hosts" -) - -// DNSServer 程序主体,负责Hosts/缓存/请求分发 -type DNSServer struct { - addr string // 监听地址 - network string // 监听协议 - stopSign chan interface{} // 服务停止信号 - stopped chan interface{} // 服务是否停止 - - disableQTypes map[uint16]bool // 禁用的DNS查询类型 - Hosts []hosts.Reader // Hosts hosts列表 - Cache *cache.DNSCache // Cache DNS响应缓存 - logCfg *LogConfig - - names []string - groups map[string]*Group -} - -// NewDNSServer 创建一个DNS Server -func NewDNSServer(addr, network string, groups map[string]*Group, logCfg *LogConfig) *DNSServer { - dnsCache := cache.NewDNSCache(cache.DefaultSize, cache.DefaultMinTTL, cache.DefaultMaxTTL) - names := make([]string, 0, len(groups)) - for name := range groups { - names = append(names, name) - } - // 按优先级排序 - sort.Slice(names, func(i, j int) bool { - gi, gj := groups[names[i]], groups[names[j]] - if gi.Priority != gj.Priority { - return gi.Priority < gi.Priority - } - return gi.name < gj.name - }) - return &DNSServer{addr: addr, network: network, logCfg: logCfg, - Cache: dnsCache, names: names, groups: groups} -} - -// GetGroup 通过group名称获取group -func (s *DNSServer) GetGroup(name string) *Group { - return s.groups[name] -} - -// SetDisableQTypes 设置禁止查询的类型 -func (s *DNSServer) SetDisableQTypes(qTypes []string) { - s.disableQTypes = make(map[uint16]bool, len(qTypes)) - for _, qTypeStr := range qTypes { - if qType, exists := dns.StringToType[strings.ToUpper(qTypeStr)]; exists { - s.disableQTypes[qType] = true - } - } -} - -// Run 以阻塞形式启动DNS Server -func (s *DNSServer) Run(ctx context.Context) { - s.stopSign = make(chan interface{}, 0) - s.stopped = make(chan interface{}, 0) - errCh := make(chan error, 2) - newSrv := func(net string) *dns.Server { return &dns.Server{Addr: s.addr, Net: net, Handler: s} } - servers := make([]*dns.Server, 0, 2) - if s.network != "" { - servers = append(servers, newSrv(s.network)) - } else { - servers = append(servers, newSrv("tcp")) - servers = append(servers, newSrv("udp")) - } - go s.wait(ctx, servers, errCh) -} - -func (s *DNSServer) wait(ctx context.Context, servers []*dns.Server, errCh chan error) { - utils.CtxDebug(ctx, "%s is running", s) - for _, srv := range servers { - go func(srv *dns.Server) { - select { - case <-s.stopSign: - return - default: - // continue - } - utils.CtxWarn(ctx, "listen on %s/%s", srv.Addr, srv.Net) - if err := srv.ListenAndServe(); err != nil { - utils.CtxError(ctx, err.Error()) - errCh <- err - } - }(srv) - } - // 阻塞运行 - for alive := len(servers); alive > 0; { - select { - case <-errCh: - alive-- - case <-s.stopSign: - for _, svr := range servers { - if err := svr.ShutdownContext(ctx); err != nil { - utils.CtxError(ctx, err.Error()) - } - } - alive = 0 - } - } - for _, group := range s.groups { - group.Exit() - } - s.logCfg.Exit(ctx) - utils.CtxDebug(ctx, "%s is stopped", s) - close(s.stopped) -} - -// StopAndWait 以阻塞形式停止DNS Server -func (s *DNSServer) StopAndWait() { - close(s.stopSign) - <-s.stopped -} - -// String 描述DNS Server -func (s *DNSServer) String() string { - return fmt.Sprintf("DNSServer<%s/%s>", s.addr, s.network) -} - -// ServeDNS 核心函数,处理DNS查询请求 -func (s *DNSServer) ServeDNS(writer dns.ResponseWriter, req *dns.Msg) { - ctx := utils.NewCtx(s.logCfg.logger, req.Id) - ctx = utils.WithFields(ctx, s.logCfg.getFields(writer, req)) - utils.CtxDebug(ctx, "extra: %s", req.Extra) - - var resp *dns.Msg - var hitHosts, hitCache bool - defer func() { // 返回响应 - if resp == nil { - resp = &dns.Msg{} - } - s.logCfg.logFunc(req, hitHosts, hitCache)(ctx, "response: %s", resp.Answer) - resp.SetReply(req) - _ = writer.WriteMsg(resp) - _ = writer.Close() - }() - - if len(req.Question) == 0 { - return - } - question := req.Question[0] - if s.disableQTypes[question.Qtype] { // 判断是否阻止查询 - return - } - if resp = s.tryHosts(ctx, question); resp != nil { // 判断是否命中hosts - hitHosts = true - return - } - if resp = s.Cache.Get(req); resp != nil { // 判断是否命中缓存 - hitCache = true - return - } - defer func() { s.Cache.Set(req, resp) }() // 将结果加入缓存 - - for _, name := range s.names { - group := s.groups[name] - if match, ok := group.matcher.Match(question.Name); ok && match { - resp = group.Handle(ctx, req, nil) - break - } - } -} - -// tryHosts 如DNS查询请求匹配hosts,则生成对应dns记录并返回。否则返回nil -func (s *DNSServer) tryHosts(ctx context.Context, question dns.Question) *dns.Msg { - if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { - ipv6 := question.Qtype == dns.TypeAAAA - for _, reader := range s.Hosts { - record, hostname := "", question.Name - if record = reader.Record(hostname, ipv6); record == "" { - // 去掉末尾的根域名再找一次 - record = reader.Record(hostname[:len(hostname)-1], ipv6) - } - if record != "" { - if ret, err := dns.NewRR(record); err != nil { - utils.CtxError(ctx, fmt.Sprintf("make dns.RR for %q: %s", record, err)) - } else { - r := new(dns.Msg) - r.Answer = append(r.Answer, ret) - return r - } - } - } - } - return nil -} - -// NewLogConfig 初始化一个请求日志配置 -func NewLogConfig(closer io.WriteCloser, ignoreQTypes []string, - ignoreHosts, ignoreCache bool) *LogConfig { - logger := logrus.New() - logger.SetLevel(logrus.StandardLogger().Level) - if closer != nil { - logger.SetOutput(closer) - } - qTypes := make(map[uint16]bool, len(ignoreQTypes)) - for _, qTypeStr := range ignoreQTypes { - if qType, exists := dns.StringToType[strings.ToUpper(qTypeStr)]; exists { - qTypes[qType] = true - } - } - return &LogConfig{closer: closer, logger: logger, ignoreQTypes: qTypes, - ignoreHosts: ignoreHosts, ignoreCache: ignoreCache} -} - -// LogConfig DNSServer专用的请求日志配置 -type LogConfig struct { - closer io.WriteCloser - logger *logrus.Logger - ignoreQTypes map[uint16]bool - ignoreHosts bool - ignoreCache bool -} - -func (l *LogConfig) getFields(writer dns.ResponseWriter, req *dns.Msg) logrus.Fields { - fields := logrus.Fields{"SRC": writer.RemoteAddr().String()} - for _, question := range req.Question { - fields["QUESTION"] = question.Name - fields["Q_TYPE"] = dns.Type(question.Qtype).String() - break - } - return fields -} - -func (l *LogConfig) logFunc(req *dns.Msg, hitHosts, hitCache bool, -) func(ctx context.Context, format string, args ...interface{}) { - if hitHosts && l.ignoreHosts || hitCache && l.ignoreCache { - return utils.CtxDebug - } - for _, question := range req.Question { - if ignore, ok := l.ignoreQTypes[question.Qtype]; ok && ignore { - return utils.CtxDebug - } - } - return utils.CtxInfo -} - -// Exit 关闭closer -func (l *LogConfig) Exit(ctx context.Context) { - if l.closer != nil { - if err := l.closer.Close(); err != nil { - utils.CtxWarn(ctx, err.Error()) - } - } -} diff --git a/core/inbound/server_test.go b/core/inbound/server_test.go deleted file mode 100644 index 46b42e9..0000000 --- a/core/inbound/server_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package inbound - -import ( - "context" - "errors" - "fmt" - "sync/atomic" - "testing" - "time" - - "github.com/Sirupsen/logrus" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/valyala/fastrand" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/core/utils/mock" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" -) - -func mockListenAndServe(mocker *mock.Mocker, sleep time.Duration, err string) { - target := &dns.Server{} - mocker.Method(target, "ListenAndServe", func(s *dns.Server) error { - time.Sleep(sleep) - if err != "" { - return fmt.Errorf("listen %s/%s error: %s", s.Addr, s.Net, err) - } - return nil - }) -} - -func mockShutdownContext(mocker *mock.Mocker, err string) { - target := &dns.Server{} - mocker.Method(target, "ShutdownContext", func(s *dns.Server, ctx context.Context) error { - if err != "" { - return fmt.Errorf("shutdown %s/%s error: %s", s.Addr, s.Net, err) - } - return nil - }) -} - -func TestDNSServer(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - mocker := new(mock.Mocker) - defer mocker.Reset() - - ctx := utils.NewCtx(nil, 0xffff) - allM := matcher.NewABPByText("*") - cErr := newFakeCaller(0, nil, errors.New("err")) - groups := map[string]*Group{"test": NewGroup("test", allM, []outbound.Caller{cErr})} - logCfg := NewLogConfig(nil, nil, false, false) - server := NewDNSServer("127.0.0.1:5353", "", groups, logCfg) - - utils.CtxInfo(ctx, "---- test listen error ----") - mockListenAndServe(mocker, 10*time.Millisecond, "unavailable now") - server.Run(ctx) - time.Sleep(20 * time.Millisecond) - server.StopAndWait() - - utils.CtxInfo(ctx, "---- test immediate shutdown ----") - mockListenAndServe(mocker, time.Hour, "") - server.Run(ctx) - server.StopAndWait() - - utils.CtxInfo(ctx, "---- test shutdown error ----") - server = NewDNSServer("127.0.0.1:5353", "udp", groups, logCfg) - mockShutdownContext(mocker, "system is busy") - server.Run(ctx) - time.Sleep(20 * time.Millisecond) - server.StopAndWait() - - utils.CtxInfo(ctx, "---- test priority ----") - groups = map[string]*Group{"test": NewGroup("test", allM, []outbound.Caller{cErr}), - "test2": NewGroup("test2", allM, []outbound.Caller{cErr})} - server = NewDNSServer("127.0.0.1:5353", "udp", groups, logCfg) - assert.Equal(t, []string{"test", "test2"}, server.names) - groups["test"].Priority = 100 - groups["test2"].Priority = 0 - server = NewDNSServer("127.0.0.1:5353", "udp", groups, logCfg) - assert.Equal(t, []string{"test2", "test"}, server.names) - - assert.Equal(t, groups["test"], server.GetGroup("test")) -} - -// 初始化一个一次性caller。首次调用Call时返回指定数据,否则panic -func newOneTimeCaller(resp *dns.Msg) *oneTimeCaller { - return &oneTimeCaller{resp: resp} -} - -type oneTimeCaller struct { - resp *dns.Msg - times int32 -} - -func (o *oneTimeCaller) Call(_ *dns.Msg) (*dns.Msg, error) { - if atomic.AddInt32(&o.times, 1) == 1 { - return o.resp, nil - } - panic("not first time") -} -func (o *oneTimeCaller) Exit() {} -func (o *oneTimeCaller) String() string { return fmt.Sprintf("oneTimeCaller<%p>", o) } - -func TestDNSServer_ServeDNS(t *testing.T) { - // region init - logrus.SetLevel(logrus.DebugLevel) - mocker := new(mock.Mocker) - defer mocker.Reset() - writer := utils.NewFakeRespWriter() - newReq := func(name string, qType uint16) *dns.Msg { - msg := &dns.Msg{Question: []dns.Question{{ - Name: name, Qtype: qType, - }}} - msg.Id = uint16(fastrand.Uint32()) - return msg - } - newResp := func(rr []dns.RR) *dns.Msg { - return &dns.Msg{Answer: rr} - } - ctx := utils.NewCtx(nil, 0xffff) - allMatcher := matcher.NewABPByText("*") - errCaller := newFakeCaller(0, nil, errors.New("call error")) - logCfg := NewLogConfig(nil, nil, false, false) - groups := make(map[string]*Group) - // endregion - - utils.CtxInfo(ctx, "---- test begin ----") - server := NewDNSServer("", "", nil, logCfg) - server.SetDisableQTypes([]string{"NS"}) - - writer.Msg = nil - server.ServeDNS(writer, &dns.Msg{Question: []dns.Question{}}) - assert.Empty(t, writer.Msg.Answer) - - writer.Msg = nil - server.ServeDNS(writer, newReq("abc", dns.TypeNS)) - assert.Empty(t, writer.Msg.Answer) - - writer.Msg = nil - server.ServeDNS(writer, newReq("abc", dns.TypeA)) - assert.Empty(t, writer.Msg.Answer) - - utils.CtxInfo(ctx, "---- test call err ----") - groups["all"] = NewGroup("all", allMatcher, []outbound.Caller{errCaller}) - server = NewDNSServer("", "", groups, logCfg) - - writer.Msg = nil - server.ServeDNS(writer, newReq("abc", dns.TypeA)) - assert.Empty(t, writer.Msg.Answer) - - utils.CtxInfo(ctx, "---- test hit hosts ----") - groups["all"] = NewGroup("all", allMatcher, []outbound.Caller{errCaller}) - hostsReader := hosts.NewReaderByText("127.0.0.1 baidu.com") - server = NewDNSServer("", "", groups, logCfg) - server.Hosts = []hosts.Reader{hostsReader} - - writer.Msg = nil - server.ServeDNS(writer, newReq("BAIDU.COM.", dns.TypeA)) - assert.NotEmpty(t, writer.Msg.Answer) - - mocker.Method(hostsReader, "Record", func(*hosts.TextReader, string, bool) string { - return "invalid string" - }) - writer.Msg = nil - server.ServeDNS(writer, newReq("BAIDU.COM.", dns.TypeA)) - assert.Empty(t, writer.Msg.Answer) - - utils.CtxInfo(ctx, "---- test hit cache ----") - caller := newOneTimeCaller(newResp([]dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}})) - groups["all"] = NewGroup("all", allMatcher, []outbound.Caller{caller}) - server = NewDNSServer("", "", groups, logCfg) - server.Cache = cache.NewDNSCache(100, time.Minute, time.Hour) - - writer.Msg = nil - server.ServeDNS(writer, newReq("BAIDU.COM.", dns.TypeA)) - assert.NotEmpty(t, writer.Msg.Answer) - writer.Msg = nil - server.ServeDNS(writer, newReq("BAIDU.COM.", dns.TypeA)) - assert.NotEmpty(t, writer.Msg.Answer) -} - -type bufCloser struct { - buf []byte - closeErr error -} - -func newBufCloser(closeErr error) *bufCloser { - return &bufCloser{buf: make([]byte, 0, 4096), closeErr: closeErr} -} -func (b *bufCloser) Write(p []byte) (n int, err error) { - b.buf = append(b.buf, p...) - return len(p), nil -} -func (b *bufCloser) Close() error { - return b.closeErr -} - -func TestNewLogConfig(t *testing.T) { - // region init - logrus.SetLevel(logrus.InfoLevel) - logBuf := newBufCloser(errors.New("system busy")) - logCfg := NewLogConfig(logBuf, []string{"A"}, true, true) - writer := utils.NewFakeRespWriter() - newReq := func(name string, qType uint16) *dns.Msg { - msg := &dns.Msg{Question: []dns.Question{{ - Name: name, Qtype: qType, - }}} - msg.Id = uint16(fastrand.Uint32()) - return msg - } - ctx := utils.NewCtx(nil, 0xffff) - matchAll := matcher.NewABPByText("*") - resp := &dns.Msg{Answer: []dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}}} - callers := []outbound.Caller{newFakeCaller(0, resp, nil)} - groups := map[string]*Group{"all": NewGroup("all", matchAll, callers)} - svc := NewDNSServer("", "", groups, logCfg) - // endregion - - svc.Hosts = []hosts.Reader{hosts.NewReaderByText("1.2.3.4 baidu.com")} - req := newReq("baidu.com", dns.TypeA) // hit hosts - writer.Msg = nil - logBuf.buf = nil - svc.ServeDNS(writer, req) - assert.Equal(t, "1.2.3.4", writer.Msg.Answer[0].(*dns.A).A.String()) - assert.Empty(t, logBuf.buf) // hit hosts, empty log - - req = newReq("ip.cn", dns.TypeA) - writer.Msg = nil - logBuf.buf = nil - svc.ServeDNS(writer, req) - assert.Equal(t, "1.1.1.1", writer.Msg.Answer[0].(*dns.A).A.String()) - assert.Empty(t, logBuf.buf) // ignore qTypes, empty log - - req = newReq("ip.cn", dns.TypeAAAA) - writer.Msg = nil - logBuf.buf = nil - svc.ServeDNS(writer, req) - assert.Equal(t, "1.1.1.1", writer.Msg.Answer[0].(*dns.A).A.String()) - assert.NotEmpty(t, logBuf.buf) // log info - - writer.Msg = nil - logBuf.buf = nil - svc.ServeDNS(writer, req) - assert.Equal(t, "1.1.1.1", writer.Msg.Answer[0].(*dns.A).A.String()) - assert.Empty(t, logBuf.buf) // hit cache, empty log - - logCfg.Exit(ctx) -} diff --git a/core/model/conf.go b/core/model/conf.go deleted file mode 100644 index 3f47eff..0000000 --- a/core/model/conf.go +++ /dev/null @@ -1,281 +0,0 @@ -package model - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "strings" - "sync" - "time" - - "github.com/BurntSushi/toml" - "github.com/Sirupsen/logrus" - "github.com/janeczku/go-ipset/ipset" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/common" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/inbound" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" - "golang.org/x/net/proxy" -) - -// Group 配置文件中每个groups section对应的结构 -type Group struct { - ECS string - NoCookie bool `toml:"no_cookie"` - Socks5 string - IPSet string - IPSetTTL int `toml:"ipset_ttl"` - DNS []string - DoT []string - DoH []string - Concurrent bool - FastestV4 bool `toml:"fastest_v4"` - TCPPingPort int `toml:"tcp_ping_port"` - Rules []string - RulesFile string `toml:"rules_file"` -} - -// GenIPSet 读取ipset配置并打包成IPSet对象 -func (conf *Group) GenIPSet() (ipSet *ipset.IPSet, err error) { - if conf.IPSet != "" { - param := &ipset.Params{Timeout: conf.IPSetTTL} - ipSet, err = ipset.New(conf.IPSet, "hash:ip", param) - if err != nil { - return nil, err - } - return ipSet, nil - } - return nil, nil -} - -// GenCallers 读取dns配置并打包成Caller对象 -func (conf *Group) GenCallers(ctx context.Context) (callers []outbound.Caller) { - // 读取socks5代理地址 - var dialer proxy.Dialer - if conf.Socks5 != "" { - dialer, _ = proxy.SOCKS5("tcp", conf.Socks5, nil, proxy.Direct) - } - // 为每个出站dns服务器创建对应Caller对象 - for _, addr := range conf.DNS { // TCP/UDP服务器 - network := "udp" - if strings.HasSuffix(addr, "/tcp") { - addr, network = addr[:len(addr)-4], "tcp" - } - if addr != "" { - if !strings.Contains(addr, ":") { - addr += ":53" - } - callers = append(callers, outbound.NewDNSCaller(addr, network, dialer)) - } - } - for _, addr := range conf.DoT { // dns over tls服务器,格式为ip:port@serverName - var serverName string - if arr := strings.Split(addr, "@"); len(arr) != 2 { - continue - } else { - addr, serverName = arr[0], arr[1] - } - if addr != "" && serverName != "" { - if !strings.Contains(addr, ":") { - addr += ":853" - } - callers = append(callers, outbound.NewDoTCaller(addr, serverName, dialer)) - } - } - for _, addr := range conf.DoH { // dns over https服务器 - if caller, err := outbound.NewDoHCallerV2(ctx, addr, dialer); err != nil { - utils.CtxError(ctx, "parse doh server error: "+err.Error()) - } else { - callers = append(callers, caller) - } - } - return -} - -// CacheConf 配置文件中cache section对应的结构 -type CacheConf struct { - Size int - MinTTL int `toml:"min_ttl"` - MaxTTL int `toml:"max_ttl"` -} - -func (conf CacheConf) GenCache() *cache.DNSCache { - minTTL := time.Duration(conf.MinTTL) * time.Second - maxTTL := time.Duration(conf.MaxTTL) * time.Second - return cache.NewDNSCache(conf.Size, minTTL, maxTTL) -} - -// QueryLog 配置文件中query_log section对应的结构 -type QueryLog struct { - File string - IgnoreQTypes []string `toml:"ignore_qtypes"` - IgnoreHosts bool `toml:"ignore_hosts"` - IgnoreCache bool `toml:"ignore_cache"` -} - -// GenLogger 读取logger配置并打包成Logger对象 -func (conf *QueryLog) GenLogger() (*inbound.QueryLogger, error) { - logger := logrus.New() - logger.SetLevel(logrus.StandardLogger().Level) - if conf.File == "/dev/null" { - logger.SetOutput(ioutil.Discard) - } else if conf.File != "" { - file, err := os.OpenFile(conf.File, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - logger.SetOutput(file) - } - return inbound.NewQueryLogger(logger, conf.IgnoreQTypes, conf.IgnoreHosts, conf.IgnoreCache), nil -} - -// Conf 配置文件总体结构 -type Conf struct { - Listen string - GFWList string - GFWb64 bool `toml:"gfwlist_b64"` - CNIP string - Logger *QueryLog `toml:"query_log"` - HostsFiles []string `toml:"hosts_files"` - Hosts map[string]string - Cache CacheConf - Groups map[string]*Group - DisableIPv6 bool `toml:"disable_ipv6"` - DisableQTypes []string `toml:"disable_qtypes"` -} - -// SetDefault 为部分字段默认配置 -func (conf *Conf) SetDefault() { - if conf.Listen == "" { - conf.Listen = ":53" - } - if conf.GFWList == "" { - conf.GFWList = "gfwlist.txt" - } - if conf.CNIP == "" { - conf.CNIP = "cnip.txt" - } -} - -// GenHostsReader 读取hosts section里的hosts记录、hosts_files里的hosts文件路径,生成hosts实例列表 -func (conf *Conf) GenHostsReader(ctx context.Context) (readers []hosts.Reader) { - // 读取Hosts列表 - var lines []string - for hostname, ip := range conf.Hosts { - lines = append(lines, ip+" "+hostname) - } - if len(lines) > 0 { - text := strings.Join(lines, "\n") - readers = append(readers, hosts.NewReaderByText(text)) - } - // 读取Hosts文件列表。reloadTick为0代表不自动重载hosts文件 - for _, filename := range conf.HostsFiles { - if reader, err := hosts.NewReaderByFile(filename, 0); err != nil { - utils.CtxWarn(ctx, "read hosts file %s error: %s", filename, err) - } else { - readers = append(readers, reader) - } - } - return -} - -// GenGroups 读取groups section里的配置,生成inbound.Group map -func (conf *Conf) GenGroups(ctx context.Context) (groups map[string]*inbound.Group, err error) { - groups = map[string]*inbound.Group{} - // 读取每个域名组的配置信息 - for name, group := range conf.Groups { - inboundGroup := &inbound.Group{ - Callers: group.GenCallers(ctx), Concurrent: group.Concurrent, - FastestV4: group.FastestV4, TCPPingPort: group.TCPPingPort, - NoCookie: group.NoCookie, - } - if inboundGroup.Concurrent { - utils.CtxWarn(ctx, "enable concurrent for group "+name) - } - if inboundGroup.FastestV4 { - utils.CtxWarn(ctx, "find fastest ipv4 for group "+name) - } - if inboundGroup.ECS, err = common.ParseECS(group.ECS); err != nil { - return nil, err - } - if group.ECS != "" { - utils.CtxWarn(ctx, "enable ecs %s for group %s", group.ECS, name) - } - // 读取匹配规则 - inboundGroup.Matcher, err = matcher.NewABPByFile(group.RulesFile, false) - if err != nil { - return nil, err - } - inboundGroup.Matcher.Extend(matcher.NewABPByText(strings.Join(group.Rules, "\n"))) - // 读取IPSet配置 - if inboundGroup.IPSet, err = group.GenIPSet(); err != nil { - return nil, err - } - groups[name] = inboundGroup - } - return groups, nil -} - -// NewHandler 从toml文件里读取ts-dns的配置并打包为Handler。如err不为空,则在返回前会输出相应错误信息 -func NewHandler(ctx context.Context, filename string) (handler *inbound.Handler, err error) { - config := Conf{Logger: &QueryLog{}, GFWb64: true} - if _, err = toml.DecodeFile(filename, &config); err != nil { - utils.CtxError(ctx, "read config %s error: %s", filename, err) - return nil, err - } - config.SetDefault() - // 初始化handler - handler = &inbound.Handler{Mux: new(sync.RWMutex), Listen: config.Listen} - // 从listen中分离监听地址和协议 - if i := strings.Index(config.Listen, "/"); i != -1 { - handler.Listen, handler.Network = config.Listen[:i], config.Listen[i+1:] - } - handler.DisableIPv6 = config.DisableIPv6 - if handler.DisableIPv6 { - utils.CtxWarn(ctx, "disable ipv6 resolve") - } - handler.DisableQTypes = map[string]bool{} - for _, qType := range config.DisableQTypes { - if qType = strings.TrimSpace(qType); qType != "" { - handler.DisableQTypes[strings.ToUpper(qType)] = true - } - } - // 读取gfwlist - if handler.GFWMatcher, err = matcher.NewABPByFile(config.GFWList, config.GFWb64); err != nil { - utils.CtxError(ctx, "read gfwlist %s error: %s", config.GFWList, err) - return nil, err - } - // 读取cnip - if handler.CNIP, err = cache.NewRamSetByFile(config.CNIP); err != nil { - utils.CtxError(ctx, "read cnip %s error: %s", config.CNIP, err) - return nil, err - } - // 读取groups - if handler.Groups, err = config.GenGroups(ctx); err != nil { - utils.CtxError(ctx, "read group config error: "+err.Error()) - return nil, err - } - for _, group := range handler.Groups { - for _, caller := range group.Callers { - if doh, ok := caller.(*outbound.DoHCallerV2); ok { - doh.SetResolver(handler) - } - } - } - handler.HostsReaders = config.GenHostsReader(ctx) - handler.Cache = config.Cache.GenCache() - // 读取Logger - if handler.QLogger, err = config.Logger.GenLogger(); err != nil { - utils.CtxError(ctx, "create query logger error: "+err.Error()) - return nil, err - } - // 检测配置有效性 - if !handler.IsValid() { - return nil, fmt.Errorf("") - } - return -} diff --git a/core/model/conf_test.go b/core/model/conf_test.go deleted file mode 100644 index 4c88d3c..0000000 --- a/core/model/conf_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package model - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/BurntSushi/toml" - "github.com/agiledragon/gomonkey" - "github.com/janeczku/go-ipset/ipset" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils/mock" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/inbound" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" -) - -func TestQueryLog(t *testing.T) { - logConf := QueryLog{File: "/dev/null"} - logger, err := logConf.GenLogger() - assert.NotNil(t, logger) - assert.Nil(t, err) - - mocker := mock.Mocker{} - defer mocker.Reset() - - logConf.File = "aaa" - mocker.FuncSeq(os.OpenFile, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {&os.File{}, nil}, - }) - logger, err = logConf.GenLogger() - assert.Nil(t, logger) - assert.NotNil(t, err) - - logger, err = logConf.GenLogger() - assert.NotNil(t, logger) - assert.Nil(t, err) -} - -func TestGroup(t *testing.T) { - mocker := mock.Mocker{} - defer mocker.Reset() - - group := Group{} - // 测试GenIPSet - mocker.FuncSeq(ipset.New, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {&ipset.IPSet{}, nil}, - }) - s, err := group.GenIPSet() // ipset名称为空,直接返回nil - assert.Nil(t, s) - assert.Nil(t, err) - group.IPSet = "test" - s, err = group.GenIPSet() //ipset.New返回异常结果 - assert.Nil(t, s) - assert.NotNil(t, err) - s, err = group.GenIPSet() // ipset.New返回正常结果 - assert.NotNil(t, s) - assert.Nil(t, err) - - // 测试GenCallers - callers := group.GenCallers(context.Background()) - assert.Empty(t, callers) - group.Socks5 = "1.1.1.1" - group.DNS = []string{"1.1.1.1", "8.8.8.8:53/tcp"} // 两个都有效 - group.DoT = []string{"1.1.1.1", "1.1.1.1@name"} // 后一个有效 - group.DoH = []string{"not exists", "https://domain/dns-query"} // 后一个有效 - callers = group.GenCallers(context.Background()) - assert.Equal(t, len(callers), 4) - -} - -func TestConf(t *testing.T) { - mocker := mock.Mocker{} - defer mocker.Reset() - - conf := &Conf{} - // 测试SetDefault - conf.SetDefault() - assert.NotEmpty(t, conf.Listen) - assert.NotEmpty(t, conf.GFWList) - assert.NotEmpty(t, conf.CNIP) - // 测试GenCache - conf.Cache = CacheConf{} - c := conf.Cache.GenCache() - assert.NotNil(t, c) - // 测试GenHostsReader - conf.Hosts = map[string]string{"host": "1.1.1.1", "ne": "ne"} - conf.HostsFiles = []string{"aaa", "bbb"} // 后一个NewReaderByFile正常 - mocker.FuncSeq(hosts.NewReaderByFile, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {&hosts.FileReader{}, nil}, - }) - ctx := context.Background() - readers := conf.GenHostsReader(ctx) - assert.Equal(t, len(readers), 2) - assert.NotNil(t, readers[0].IP("host", false)) - // 测试GenGroups - conf.Groups = map[string]*Group{"test": {Concurrent: true, FastestV4: true}} - mocker.MethodSeq(&Group{}, "GenCallers", []gomonkey.Params{ - {nil}, {nil}, {nil}, {nil}, - }) - mocker.MethodSeq(&Group{}, "GenIPSet", []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {nil, nil}, - }) - conf.Groups["test"].ECS = "1.1.1." - groups, err := conf.GenGroups(ctx) // genECS失败 - assert.NotNil(t, err) - assert.Nil(t, groups) - conf.Groups["test"].ECS = "1.1.1.1" - conf.Groups["test"].RulesFile = "???not_exists" // NewABPByFile失败 - groups, err = conf.GenGroups(ctx) - assert.NotNil(t, err) - assert.Nil(t, groups) - conf.Groups["test"].RulesFile = "" // NewABPByFile成功 - groups, err = conf.GenGroups(ctx) // GenIPSet失败 - assert.NotNil(t, err) - assert.Nil(t, groups) - groups, err = conf.GenGroups(ctx) // GenIPSet成功 - assert.Nil(t, err) - assert.NotNil(t, groups) -} - -func TestNewHandler(t *testing.T) { - mocker := mock.Mocker{} - defer mocker.Reset() - - mocker.FuncSeq(toml.DecodeFile, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, - }) - ctx := context.Background() - handler, err := NewHandler(ctx, "") // DecodeFile失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - - p := gomonkey.ApplyFunc(toml.DecodeFile, - func(fn string, conf interface{}) (toml.MetaData, error) { - conf.(*Conf).DisableIPv6 = true - conf.(*Conf).DisableQTypes = []string{"HTTPS"} - conf.(*Conf).Listen = ":53/tcp" - return toml.MetaData{}, nil - }) - defer p.Reset() - - mocker.FuncSeq(matcher.NewABPByFile, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {nil, nil}, {nil, nil}, {nil, nil}, - {nil, nil}, {nil, nil}, - }) - handler, err = NewHandler(ctx, "") // NewABPByFile失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - mocker.FuncSeq(cache.NewRamSetByFile, []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {nil, nil}, {nil, nil}, {nil, nil}, - {nil, nil}, - }) - handler, err = NewHandler(ctx, "") // NewRamSetByFile失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - - caller, _ := outbound.NewDoHCallerV2(ctx, "https://abc/", nil) - caller.Exit() - groups := map[string]*inbound.Group{"abc": { - Callers: []outbound.Caller{caller}, - }} - mocker.MethodSeq(&Conf{}, "GenGroups", []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {groups, nil}, {nil, nil}, {nil, nil}, - }) - handler, err = NewHandler(ctx, "") // GenGroups失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - mocker.MethodSeq(&QueryLog{}, "GenLogger", []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {nil, nil}, {nil, nil}, - }) - handler, err = NewHandler(ctx, "") // GenLogger失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - mocker.MethodSeq(handler, "IsValid", []gomonkey.Params{{false}, {true}}) - handler, err = NewHandler(ctx, "") // 验证配置失败 - assert.Nil(t, handler) - assert.NotNil(t, err) - handler, err = NewHandler(ctx, "") // 验证配置成功 - assert.NotNil(t, handler) - assert.Nil(t, err) -} diff --git a/core/model/reader.go b/core/model/reader.go deleted file mode 100644 index c889d67..0000000 --- a/core/model/reader.go +++ /dev/null @@ -1,318 +0,0 @@ -package model - -import ( - "context" - "errors" - "fmt" - "io" - "io/ioutil" - "math" - "os" - "strconv" - "strings" - "time" - - "github.com/BurntSushi/toml" - "github.com/janeczku/go-ipset/ipset" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/common" - "github.com/wolf-joe/ts-dns/core/inbound" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" - "golang.org/x/net/proxy" -) - -func newDNSCache(conf CacheConf) *cache.DNSCache { - size := cache.DefaultSize - minTTL := cache.DefaultMinTTL - maxTTL := cache.DefaultMaxTTL - if conf.Size != 0 { - size = conf.Size - } - if conf.MinTTL != 0 { - minTTL = time.Duration(conf.MinTTL) * time.Second - } - if conf.MaxTTL != 0 { - maxTTL = time.Duration(conf.MaxTTL) * time.Second - } - return cache.NewDNSCache(size, minTTL, maxTTL) -} - -func newLogCfg(ctx context.Context, conf *QueryLog) (*inbound.LogConfig, error) { - var closer io.WriteCloser - if conf.File == "/dev/null" { // 丢弃查询日志 - closer = &wrapCloser{Writer: ioutil.Discard} - } else if conf.File != "" { // 查询日志写入到文件 - file, err := os.OpenFile(conf.File, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - utils.CtxError(ctx, "open file %q error: %s", conf.File, err) - return nil, err - } - closer = file - } - return inbound.NewLogConfig(closer, conf.IgnoreQTypes, conf.IgnoreHosts, conf.IgnoreCache), nil -} - -func newIPSet(ctx context.Context, name string, ttl int) (*ipset.IPSet, error) { - if name != "" { - param := &ipset.Params{Timeout: ttl} - val, err := ipset.New(name, "hash:ip", param) - if err != nil { - utils.CtxError(ctx, "new ipset %q error: %s", name, err) - return nil, err - } - return val, nil - } - return nil, nil -} - -// 接受类似"1.1.1.1:53/udp"的配置 -func newDNSCaller(server string, dialer proxy.Dialer) (*outbound.DNSCaller, error) { - port, network := 53, "udp" - if strings.Contains(server, "/") { - parts := strings.Split(server, "/") - server = parts[0] - if len(parts) > 1 { - network = strings.ToLower(parts[1]) - } - } - if network != "udp" && network != "tcp" { - return nil, errors.New("unknown network: " + network) - } - var err error - if strings.Contains(server, ":") { - parts := strings.Split(server, ":") - server = parts[0] - if len(parts) > 1 { - port, err = strconv.Atoi(parts[1]) - } - } - if err != nil { - return nil, err - } - if server == "" { - return nil, errors.New("empty server") - } - server += fmt.Sprintf(":%d", port) - return outbound.NewDNSCaller(server, network, dialer), nil -} - -// 接受类似"1.1.1.1:853@domain.com"的配置 -func newDoTCaller(server string, dialer proxy.Dialer) (*outbound.DNSCaller, error) { - port, domain := 853, "" - if strings.Contains(server, "@") { - parts := strings.Split(server, "@") - server = parts[0] - if len(parts) > 1 { - domain = strings.ToLower(parts[1]) - } - } - if domain == "" { - return nil, errors.New("empty domain") - } - var err error - if strings.Contains(server, ":") { - parts := strings.Split(server, ":") - server = parts[0] - if len(parts) > 1 { - port, err = strconv.Atoi(parts[1]) - } - } - if err != nil { - return nil, err - } - if server == "" { - return nil, errors.New("empty server") - } - server += fmt.Sprintf(":%d", port) - return outbound.NewDoTCaller(server, domain, dialer), nil -} - -func newCallers(ctx context.Context, socks5 string, dns, dot, doh []string) ([]outbound.Caller, error) { - // 初始化socks5代理 - var dialer proxy.Dialer - var err error - if socks5 != "" { - dialer, _ = proxy.SOCKS5("tcp", socks5, nil, proxy.Direct) - } - var caller outbound.Caller - var ans []outbound.Caller - for _, server := range dns { - caller, err = newDNSCaller(server, dialer) - if err != nil { - utils.CtxError(ctx, "parse dns %q error: %s", server, err) - return nil, err - } - ans = append(ans, caller) - } - for _, server := range dot { - caller, err = newDoTCaller(server, dialer) - if err != nil { - utils.CtxError(ctx, "parse dot %q error: %s", server, err) - return nil, err - } - ans = append(ans, caller) - } - for _, server := range doh { - caller, err = outbound.NewDoHCallerV2(ctx, server, dialer) - if err != nil { - utils.CtxError(ctx, "parse doh %q error: %s", server, err) - return nil, err - } - ans = append(ans, caller) - } - return ans, nil -} - -func newGroup(ctx context.Context, name string, conf *Group) (*inbound.Group, error) { - priority := 255 - // 读取域名分组配置 - rule, err := matcher.NewABPByFile(conf.RulesFile, false) - if err != nil { - utils.CtxError(ctx, "read rules file %q error: %s", conf.RulesFile, err) - return nil, err - } - rule.Extend(matcher.NewABPByText(strings.Join(conf.Rules, "\n"))) - if conf.RulesFile == "" && len(conf.RulesFile) == 0 { - rule = matcher.NewABPByText("*") - priority = math.MaxInt32 - } - // 读取dns服务器配置 - callers, err := newCallers(ctx, conf.Socks5, conf.DNS, conf.DoT, conf.DoH) - if err != nil { - return nil, err - } - // 读取group配置 - group := inbound.NewGroup(name, rule, callers) - group.Priority = priority - group.IPSet, err = newIPSet(ctx, conf.IPSet, conf.IPSetTTL) - if err != nil { - return nil, err - } - group.WithECS, err = common.ParseECS(conf.ECS) - if err != nil { - utils.CtxError(ctx, "parse ecs %q error: %s", conf.ECS, err) - return nil, err - } - group.NoCookie = conf.NoCookie - group.Concurrent = conf.Concurrent - if conf.FastestV4 { - group.WithFastestIP(conf.TCPPingPort) - } - return group, nil -} - -// 将listen配置(如":53/udp")拆分成ip+port和network两部分 -func parseListen(listen string) (string, string) { - network := "" - if i := strings.Index(listen, "/"); i != -1 { - listen, network = listen[:i], listen[i+1:] - } - ip, port := listen, ":53" - if i := strings.Index(listen, ":"); i != -1 { - ip, port = listen[:i], listen[i:] - } - - return ip + port, network -} - -// 读取配置,创建一个DNSServer -func newDNSServer(ctx context.Context, conf Conf) (*inbound.DNSServer, error) { - // 读取域名组配置 - groups := make(map[string]*inbound.Group, len(conf.Groups)) - for name, groupConf := range conf.Groups { - group, err := newGroup(ctx, name, groupConf) - if err != nil { - return nil, err - } - groups[name] = group - } - // 读取日志配置 - logCfg, err := newLogCfg(ctx, conf.Logger) - if err != nil { - return nil, err - } - // 读取服务器配置 - addr, network := parseListen(conf.Listen) - svc := inbound.NewDNSServer(addr, network, groups, logCfg) - svc.Cache = newDNSCache(conf.Cache) - if conf.DisableIPv6 { - conf.DisableQTypes = append(conf.DisableQTypes, "AAAA") - } - svc.SetDisableQTypes(conf.DisableQTypes) - // 读取hosts - svc.Hosts = make([]hosts.Reader, 0, len(conf.HostsFiles)+1) - for _, file := range conf.HostsFiles { - var reader hosts.Reader - if reader, err = hosts.NewReaderByFile(file, 0); err != nil { - utils.CtxWarn(ctx, "read hosts %s: %s", file, err) - } else { - svc.Hosts = append(svc.Hosts, reader) - } - } - if len(conf.Hosts) > 0 { - lines := make([]string, 0, len(conf.Hosts)) - for hostname, ip := range conf.Hosts { - lines = append(lines, ip+" "+hostname) - } - text := strings.Join(lines, "\n") - svc.Hosts = append(svc.Hosts, hosts.NewReaderByText(text)) - } - - if err = compatibleOld(ctx, conf, svc); err != nil { - return nil, err - } - return svc, nil -} - -// NewDNSServerFromText 从文本中读取配置 -func NewDNSServerFromText(ctx context.Context, text string) (*inbound.DNSServer, error) { - conf := Conf{Logger: &QueryLog{}} - if _, err := toml.Decode(text, &conf); err != nil { - utils.CtxError(ctx, "decode toml error: %s", err) - return nil, err - } - return newDNSServer(ctx, conf) -} - -// NewDNSServerFromFile 从文件中读取配置 -func NewDNSServerFromFile(ctx context.Context, file string) (*inbound.DNSServer, error) { - data, err := ioutil.ReadFile(file) - if err != nil { - utils.CtxError(ctx, "read file %q error: %s", file, err) - return nil, err - } - return NewDNSServerFromText(ctx, string(data)) -} - -// 兼容老逻辑 -func compatibleOld(ctx context.Context, conf Conf, svc *inbound.DNSServer) error { - if conf.CNIP == "" && conf.GFWList == "" { - return nil // 不走老逻辑 - } - groupClean, groupDirty := svc.GetGroup("clean"), svc.GetGroup("dirty") - if groupClean == nil || groupDirty == nil { - return errors.New("group clean/dirty not found") - } - gfw, err := matcher.NewABPByFile(conf.GFWList, conf.GFWb64) - if err != nil { - utils.CtxError(ctx, "read gfwlist %q: %s", conf.GFWList, err) - return err - } - gfwRed := inbound.NewDomainRedirector(gfw, inbound.DomainRedRuleIfMatch, groupDirty) - cnIP, err := cache.NewRamSetByFile(conf.CNIP) - if err != nil { - utils.CtxError(ctx, "read cnip %q: %s", conf.CNIP, err) - return err - } - cnIPRed := inbound.NewIPRedirector(cnIP, inbound.IPRedRuleIfFind, gfwRed) - groupClean.Next = cnIPRed - return nil -} - -type wrapCloser struct{ io.Writer } - -// Close do nothing -func (c *wrapCloser) Close() error { return nil } diff --git a/core/model/reader_test.go b/core/model/reader_test.go deleted file mode 100644 index 0286ea7..0000000 --- a/core/model/reader_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package model - -import ( - "errors" - "io/ioutil" - "os" - "testing" - "time" - - "github.com/BurntSushi/toml" - "github.com/janeczku/go-ipset/ipset" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/core/utils/mock" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/matcher" -) - -func TestNewDNSCache(t *testing.T) { - assert.NotNil(t, newDNSCache(CacheConf{Size: 1, MinTTL: 60, MaxTTL: 86400})) -} - -func TestNewLogCfg(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(os.OpenFile, func(name string, _ int, _ os.FileMode) (*os.File, error) { - if name == "error" { - return nil, errors.New("mock error") - } - return &os.File{}, nil - }) - mocker.Method(&os.File{}, "Close", func(*os.File) error { - return errors.New("close file mock error") - }) - - cfg := &QueryLog{File: "", IgnoreQTypes: nil, - IgnoreHosts: false, IgnoreCache: false} - logCfg, err := newLogCfg(ctx, cfg) - assert.Nil(t, err) - assert.NotNil(t, logCfg) - logCfg.Exit(ctx) - - cfg.File = "error" - logCfg, err = newLogCfg(ctx, cfg) - assert.NotNil(t, err) - assert.Nil(t, logCfg) - - cfg.File = "abc" - logCfg, err = newLogCfg(ctx, cfg) - assert.Nil(t, err) - assert.NotNil(t, logCfg) - logCfg.Exit(ctx) - - cfg.File = "/dev/null" - logCfg, err = newLogCfg(ctx, cfg) - assert.Nil(t, err) - assert.NotNil(t, logCfg) - logCfg.Exit(ctx) -} - -func TestParseListen(t *testing.T) { - addr, network := parseListen("") - assert.Equal(t, ":53", addr) - assert.Equal(t, "", network) - - addr, network = parseListen(":5353") - assert.Equal(t, ":5353", addr) - assert.Equal(t, "", network) - - addr, network = parseListen("/udp") - assert.Equal(t, ":53", addr) - assert.Equal(t, "udp", network) - - addr, network = parseListen("127.0.0.1:5353/tcp") - assert.Equal(t, "127.0.0.1:5353", addr) - assert.Equal(t, "tcp", network) - - addr, network = parseListen("???:::???///???") - assert.Equal(t, "???:::???", addr) - assert.Equal(t, "//???", network) -} - -func TestNewDNSCaller(t *testing.T) { - _, err := newDNSCaller("", nil) - assert.NotNil(t, err) - _, err = newDNSCaller("1.1.1.1:abc", nil) - assert.NotNil(t, err) - _, err = newDNSCaller("1.1.1.1:53/???", nil) - assert.NotNil(t, err) - _, err = newDNSCaller("1.1.1.1:53/udp", nil) - assert.Nil(t, err) -} - -func TestNewDoTCaller(t *testing.T) { - _, err := newDoTCaller("", nil) - assert.NotNil(t, err) - _, err = newDoTCaller("1.1.1.1", nil) - assert.NotNil(t, err) - _, err = newDoTCaller("1.1.1.1:853", nil) - assert.NotNil(t, err) - _, err = newDoTCaller("1.1.1.1:853@", nil) - assert.NotNil(t, err) - _, err = newDoTCaller("1.1.1.1:???@abc", nil) - assert.NotNil(t, err) - _, err = newDoTCaller("1.1.1.1:853@abc", nil) - assert.Nil(t, err) - _, err = newDoTCaller(":853@abc", nil) - assert.NotNil(t, err) -} - -func TestNewIPSet(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(ipset.New, func(name string, _ string, _ *ipset.Params) (*ipset.IPSet, error) { - if name == "error" { - return nil, errors.New("error") - } - return &ipset.IPSet{}, nil - }) - - val, err := newIPSet(ctx, "", 100) - assert.Nil(t, err) - assert.Nil(t, val) - val, err = newIPSet(ctx, "error", 100) - assert.NotNil(t, err) - assert.Nil(t, val) - val, err = newIPSet(ctx, "abc", 100) - assert.Nil(t, err) - assert.NotNil(t, val) -} - -func TestNewCallers(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - socks5 := "abc" - - var dns, dot, doh []string - - callers, err := newCallers(ctx, socks5, dns, dot, doh) - assert.Empty(t, callers) - assert.Nil(t, err) - - dns = []string{":abc"} - _, err = newCallers(ctx, socks5, dns, dot, doh) - assert.NotNil(t, err) - - dns = []string{"1.1.1.1"} - dot = []string{":abc"} - _, err = newCallers(ctx, socks5, dns, dot, doh) - assert.NotNil(t, err) - - dot = []string{"8.8.8.8@dns.google"} - doh = []string{":abc"} - _, err = newCallers(ctx, socks5, dns, dot, doh) - assert.NotNil(t, err) - - doh = []string{"https://dns.google/"} - callers, err = newCallers(ctx, socks5, dns, dot, doh) - assert.Nil(t, err) - assert.NotNil(t, callers) -} - -func TestNewGroup(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(ipset.New, func(name string, _ string, _ *ipset.Params) (*ipset.IPSet, error) { - if name == "error" { - return nil, errors.New("error") - } - return &ipset.IPSet{}, nil - }) - name := "test" - conf := &Group{} - - group, err := newGroup(ctx, name, conf) - assert.Nil(t, err) - assert.NotNil(t, group) - - conf.RulesFile = "???" - _, err = newGroup(ctx, name, conf) - assert.NotNil(t, err) - - conf.RulesFile = "" - conf.DNS = []string{":???"} - _, err = newGroup(ctx, name, conf) - assert.NotNil(t, err) - - conf.DNS = nil - conf.IPSet = "error" - _, err = newGroup(ctx, name, conf) - assert.NotNil(t, err) - - conf.IPSet = "" - conf.ECS = "???" - _, err = newGroup(ctx, name, conf) - assert.NotNil(t, err) - - conf.ECS = "" - conf.FastestV4 = true - group, err = newGroup(ctx, name, conf) - assert.Nil(t, err) - assert.NotNil(t, group) -} - -func TestNewDNSServer(t *testing.T) { - conf := Conf{Logger: &QueryLog{}, DisableIPv6: true} - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(os.OpenFile, func(name string, _ int, _ os.FileMode) (*os.File, error) { - if name == "error" { - return nil, errors.New("mock error") - } - return &os.File{}, nil - }) - mocker.Func(hosts.NewReaderByFile, func(fn string, _ time.Duration) (*hosts.FileReader, error) { - if fn == "error" { - return nil, errors.New("mock error") - } - return &hosts.FileReader{}, nil - }) - - conf.Groups = map[string]*Group{"test": { - ECS: "123", - }} - _, err := newDNSServer(ctx, conf) - assert.NotNil(t, err) - conf.Groups["test"].ECS = "" - _, err = newDNSServer(ctx, conf) - assert.Nil(t, err) - - conf.Logger.File = "error" - _, err = newDNSServer(ctx, conf) - assert.NotNil(t, err) - conf.Logger.File = "" - _, err = newDNSServer(ctx, conf) - assert.Nil(t, err) - - conf.HostsFiles = []string{"error", "test"} - conf.Hosts = map[string]string{"taobao.com": "1.1.1.1"} - _, err = newDNSServer(ctx, conf) - assert.Nil(t, err) - - conf.HostsFiles = nil - conf.GFWList = "gfw.txt" - _, err = newDNSServer(ctx, conf) - assert.NotNil(t, err) -} - -func TestNewDNSServerFromFile(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(ioutil.ReadFile, func(filename string) ([]byte, error) { - if filename == "error" { - return nil, errors.New("file not exists") - } - return nil, nil - }) - - _, err := NewDNSServerFromFile(ctx, "error") - assert.NotNil(t, err) - - serv, err := NewDNSServerFromFile(ctx, "abc") - assert.Nil(t, err) - assert.NotNil(t, serv) -} - -func TestNewDNSServerFromText(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - mocker.Func(ioutil.ReadFile, func(filename string) ([]byte, error) { - if filename == "error" { - return nil, errors.New("file not exists") - } - return nil, nil - }) - - _, err := NewDNSServerFromText(ctx, "???") - assert.NotNil(t, err) - _, err = NewDNSServerFromText(ctx, "") - assert.Nil(t, err) -} - -func TestCompatibleOld(t *testing.T) { - ctx := utils.NewCtx(nil, 0xffff) - mocker := new(mock.Mocker) - defer mocker.Reset() - text := `listen = ":53" -gfwlist = "error" -cnip = "error" - -[groups] - [groups.clean] - dns = ["223.5.5.5", "114.114.114.114"] - concurrent = true - - [groups.dirty] - dns = ["208.67.222.222:5353", "176.103.130.130:5353"]` - var conf = Conf{Logger: &QueryLog{}} - if _, err := toml.Decode(text, &conf); err != nil { - panic(err) - } - mocker.Func(matcher.NewABPByFile, func(fn string, _ bool) (*matcher.ABPlus, error) { - if fn == "error" { - return nil, errors.New("mock by error") - } - return &matcher.ABPlus{}, nil - }) - mocker.Func(cache.NewRamSetByFile, func(fn string) (*cache.RamSet, error) { - if fn == "error" { - return nil, errors.New("mock by error") - } - return &cache.RamSet{}, nil - }) - - serv, err := newDNSServer(ctx, conf) - assert.NotNil(t, err) - assert.Nil(t, serv) - - conf.GFWList = "gfwlist.txt" - serv, err = newDNSServer(ctx, conf) - assert.NotNil(t, err) - assert.Nil(t, serv) - - conf.CNIP = "cnip.txt" - serv, err = newDNSServer(ctx, conf) - assert.Nil(t, err) - assert.NotNil(t, serv) -} diff --git a/core/utils/resp_writer.go b/core/utils/resp_writer.go deleted file mode 100644 index 47973b0..0000000 --- a/core/utils/resp_writer.go +++ /dev/null @@ -1,51 +0,0 @@ -package utils - -import ( - "net" - - "github.com/miekg/dns" -) - -type fakeRespWriter struct { - Msg *dns.Msg - Bytes []byte -} - -// NewFakeRespWriter 创建一个FakeRespWriter,用于手动请求dns.Handler时获取DNS响应 -func NewFakeRespWriter() *fakeRespWriter { - return &fakeRespWriter{} -} - -func (w *fakeRespWriter) LocalAddr() net.Addr { - return &net.IPAddr{IP: []byte{127, 0, 0, 1}} -} - -func (w *fakeRespWriter) RemoteAddr() net.Addr { - return &net.IPAddr{IP: []byte{127, 0, 0, 1}} -} - -func (w *fakeRespWriter) WriteMsg(msg *dns.Msg) error { - w.Msg = msg - return nil -} - -func (w *fakeRespWriter) Write(bytes []byte) (int, error) { - w.Bytes = bytes - return len(bytes), nil -} - -func (w *fakeRespWriter) Close() error { - return nil -} - -func (w *fakeRespWriter) TsigStatus() error { - return nil -} - -func (w *fakeRespWriter) TsigTimersOnly(bool) { - return -} - -func (w *fakeRespWriter) Hijack() { - return -} diff --git a/doc/.gitkeep b/doc/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/doc/arch.drawio.svg b/doc/arch.drawio.svg new file mode 100644 index 0000000..9d9d932 --- /dev/null +++ b/doc/arch.drawio.svg @@ -0,0 +1,4 @@ + + + +
1
1
2
2
3
3
4
4
5
5
6
6
7 response
7 response
ts-dns
ts-dns
0 request
0 request
hosts
hosts
rules
rules
group
group
redirect
redire...
cache
cache
cache
cache
dns server
dns server
Text is not SVG - cannot display
\ No newline at end of file diff --git a/doc/arch.drawio.xml b/doc/arch.drawio.xml new file mode 100644 index 0000000..6942a3b --- /dev/null +++ b/doc/arch.drawio.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/go.mod b/go.mod index ee1ae7c..ce36346 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,27 @@ module github.com/wolf-joe/ts-dns -go 1.13 - -replace github.com/Sirupsen/logrus v1.4.2 => github.com/sirupsen/logrus v1.4.2 +go 1.17 require ( - github.com/BurntSushi/toml v0.3.1 - github.com/Sirupsen/logrus v1.4.2 + github.com/BurntSushi/toml v1.2.1 github.com/agiledragon/gomonkey v2.0.1+incompatible - github.com/coreos/go-semver v0.3.0 // indirect - github.com/fsnotify/fsnotify v1.4.9 - github.com/janeczku/go-ipset v0.0.0-20170206212442-499ed3217c4b - github.com/miekg/dns v1.1.32 - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/miekg/dns v1.1.50 + github.com/sirupsen/logrus v1.9.0 github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/stretchr/testify v1.7.0 github.com/valyala/fastrand v1.0.0 - golang.org/x/net v0.0.0-20200301022130-244492dfa37a - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/wolf-joe/go-ipset v0.0.0-20221126092954-3bc3b2576989 + github.com/yl2chen/cidranger v1.0.2 + golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 +) + +require ( + github.com/coreos/go-semver v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2f7e97 --- /dev/null +++ b/go.sum @@ -0,0 +1,68 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/agiledragon/gomonkey v2.0.1+incompatible h1:DIQT3ZshgGz9pTwBddRSZWDutIRPx2d7UzmjzgWo9q0= +github.com/agiledragon/gomonkey v2.0.1+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw= +github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI= +github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/wolf-joe/go-ipset v0.0.0-20221126092954-3bc3b2576989 h1:erlfrFfwhErnPgShdzD5u9qOYhv3CJgGEOcoMx4lDjE= +github.com/wolf-joe/go-ipset v0.0.0-20221126092954-3bc3b2576989/go.mod h1:cyMlufZQUvgEvNs4UPZvx8WXMTWGzbed4dI1QuAg7Dg= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hosts/hosts.go b/hosts/hosts.go index de74035..b96ca8c 100644 --- a/hosts/hosts.go +++ b/hosts/hosts.go @@ -1,143 +1,162 @@ package hosts import ( + "bufio" "fmt" - "io/ioutil" + "github.com/miekg/dns" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/ts-dns/config" "net" + "os" "regexp" "strings" - "sync" - "time" + "unicode" ) -const ( - minReloadTick = time.Second // 当reloadTick低于该值时不自动重载hosts -) - -// Reader Hosts读取器 -type Reader interface { - IP(hostname string, ipv6 bool) string - Record(hostname string, ipv6 bool) string -} +// region interface -// TextReader 基于文本的读取器 -type TextReader struct { - v4Map map[string]string - v4Reg map[*regexp.Regexp]string - v6Map map[string]string - v6Reg map[*regexp.Regexp]string +type IDNSHosts interface { + Get(req *dns.Msg) *dns.Msg } -// IP 获取hostname对应的ip地址,如不存在则返回空串 -func (r *TextReader) IP(hostname string, ipv6 bool) string { - hostname = strings.ToLower(hostname) - hostMap, regMap := r.v4Map, r.v4Reg - if ipv6 { - hostMap, regMap = r.v6Map, r.v6Reg - } - if val, ok := hostMap[hostname]; ok { - return val - } - for regex, val := range regMap { - if regex.MatchString(hostname) { - return val +func NewDNSHosts(conf config.Conf) (IDNSHosts, error) { + domainMap := make(map[string]ipInfo, len(conf.Hosts)) + regexMap := make(map[*regexp.Regexp]ipInfo, len(conf.Hosts)) + load := func(host, ipStr string) error { + ip := buildIPInfo(ipStr) + if ip == zeroIP { + return fmt.Errorf("parse %q to ip failed", ipStr) } + if !strings.ContainsAny(host, "*?") { + domainMap[host] = ip + return nil + } + // wildcard to regexp + host = strings.Replace(host, ".", "\\.", -1) + host = strings.Replace(host, "*", ".*", -1) + host = strings.Replace(host, "?", ".", -1) + reg, err := regexp.Compile("^" + host + "$") + if err != nil { + return fmt.Errorf("build host regexp %q failed: %w", host, err) + } + regexMap[reg] = ip + return nil } - return "" -} - -// Record 生成hostname对应的dns记录,格式为"hostname ttl IN A ip",如不存在则返回空串 -func (r *TextReader) Record(hostname string, ipv6 bool) (record string) { - ip, t := r.IP(hostname, ipv6), "A" - if ipv6 { - t = "AAAA" - } - if ip == "" { - return "" + // parse hosts + for host, ipStr := range conf.Hosts { + if err := load(host, ipStr); err != nil { + return nil, err + } } - return fmt.Sprintf("%s 0 IN %s %s", hostname, t, ip) -} - -// NewReaderByText 解析文本内容中的Hosts -func NewReaderByText(text string) (r *TextReader) { - r = &TextReader{v4Map: map[string]string{}, v4Reg: map[*regexp.Regexp]string{}, - v6Map: map[string]string{}, v6Reg: map[*regexp.Regexp]string{}} - for _, line := range strings.Split(text, "\n") { - line = strings.Trim(line, " \t\r") - if line == "" || strings.HasPrefix(line, "#") { - continue + // parse hosts files + files := make([]*os.File, 0, len(conf.HostsFiles)) + defer func() { + for _, f := range files { + _ = f.Close() + } + }() + for _, filename := range conf.HostsFiles { + logrus.Debugf("load hosts file %q", filename) + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("load hosts file %q error: %w", filename, err) } - splitter := func(r rune) bool { return r == ' ' || r == '\t' } - if arr := strings.FieldsFunc(line, splitter); len(arr) >= 2 { - ip, hostname := net.ParseIP(arr[0]), arr[1] - var regex *regexp.Regexp - if strings.ContainsRune(hostname, '*') { - hostname = strings.Replace(hostname, ".", "\\.", -1) - hostname = strings.Replace(hostname, "*", ".*", -1) - regex = regexp.MustCompile("^" + hostname + "$") + files = append(files, file) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + // parse each line + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue // ignore comment + } + parts := strings.FieldsFunc(line, unicode.IsSpace) + if len(parts) < 2 { + continue } - if ip.To4() != nil { - if regex != nil { - r.v4Reg[regex] = ip.To4().String() - } else { - r.v4Map[hostname] = ip.To4().String() - } - } else if ip.To16() != nil { - if regex != nil { - r.v6Reg[regex] = ip.To16().String() - } else { - r.v6Map[hostname] = ip.To16().String() - } + if err = load(parts[0], parts[1]); err != nil { + return nil, fmt.Errorf("load hosts file %q error: %w", filename, err) } } } - return + return &HostReader{ + domainMap: domainMap, + regexMap: regexMap, + }, nil } -// FileReader 基于文件的读取器 -type FileReader struct { - mux *sync.Mutex - filename string - timestamp time.Time - reloadTick time.Duration - reader *TextReader +// endregion + +// region impl +var ( + zeroIP = ipInfo{} + _ IDNSHosts = &HostReader{} +) + +type ipInfo struct { + val string + _type uint16 } -func (r *FileReader) reload() { - r.mux.Lock() - defer r.mux.Unlock() - if r.reloadTick < minReloadTick || time.Now().Before(r.timestamp.Add(r.reloadTick)) { - return - } - // read host file again - nr, err := NewReaderByFile(r.filename, r.reloadTick) - // 当hosts文件读取失败时不更新内存中已有hosts记录 - if err == nil { - r.reader = nr.reader +func (i ipInfo) Record(host string) string { + if i._type == dns.TypeA { + return host + " 0 IN A " + i.val } - r.timestamp = time.Now() + return host + " 0 IN AAAA " + i.val } -// IP 获取hostname对应的ip地址,如不存在则返回空串 -func (r *FileReader) IP(hostname string, ipv6 bool) string { - r.reload() - return r.reader.IP(hostname, ipv6) +func buildIPInfo(val string) ipInfo { + ip := net.ParseIP(val) + if ip.To4() != nil { + return ipInfo{val: val, _type: dns.TypeA} + } else if ip.To16() != nil { + return ipInfo{val: val, _type: dns.TypeAAAA} + } + return zeroIP } -// Record 生成hostname对应的dns记录,格式为"hostname ttl IN A ip",如不存在则返回空串 -func (r *FileReader) Record(hostname string, ipv6 bool) string { - r.reload() - return r.reader.Record(hostname, ipv6) +// HostReader 管理hosts +type HostReader struct { + domainMap map[string]ipInfo + regexMap map[*regexp.Regexp]ipInfo } -// NewReaderByFile 解析目标文件内容中的Hosts -func NewReaderByFile(filename string, reloadTick time.Duration) (r *FileReader, err error) { - var raw []byte - if raw, err = ioutil.ReadFile(filename); err != nil { - return +func (h *HostReader) Get(req *dns.Msg) *dns.Msg { + if len(req.Question) == 0 { + return nil + } + // todo dns大小写不敏感 + host, qType := strings.ToLower(req.Question[0].Name), req.Question[0].Qtype + if qType != dns.TypeA && qType != dns.TypeAAAA { + return nil } - r = &FileReader{mux: new(sync.Mutex), filename: filename, reloadTick: reloadTick} - r.reader = NewReaderByText(string(raw)) - r.timestamp = time.Now() - return + + getIP := func(host string) (ipInfo, bool) { + if res, exists := h.domainMap[host]; exists { + return res, true + } + for reg, res := range h.regexMap { + if reg.MatchString(host) { + return res, true + } + } + return zeroIP, false + } + ip, exists := getIP(host) + if !exists && strings.HasSuffix(host, ".") { + ip, exists = getIP(host[:len(host)-1]) + } + if !exists || ip._type != qType { + return nil + } + rr, err := dns.NewRR(ip.Record(host)) + if err != nil { + logrus.Errorf("build dns rr failed: %+v", err) + return nil + } + resp := new(dns.Msg) + resp.SetReply(req) + resp.Answer = append(resp.Answer, rr) + return resp } + +// endregion diff --git a/hosts/hosts_test.go b/hosts/hosts_test.go index 145408f..22786fa 100644 --- a/hosts/hosts_test.go +++ b/hosts/hosts_test.go @@ -1,60 +1,101 @@ package hosts import ( + "github.com/miekg/dns" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "io/ioutil" - "os" + "github.com/wolf-joe/ts-dns/config" "testing" - "time" ) -func TestNewTextReader(t *testing.T) { - content := "# comment\n\n 256.0.0.0 ne\n" + - " 127.0.0.1 localhost \n \n gggg::0 ip6-ne \n ::1 ip6-localhost " - reader := NewReaderByText(content) - assert.Equal(t, reader.IP("ne", false), "") - assert.Equal(t, reader.IP("localhost", false), "127.0.0.1") - assert.Equal(t, reader.IP("ip6-ne", true), "") - assert.Equal(t, reader.IP("ip6-localhost", true), "::1") - assert.Equal(t, reader.Record("ne", false), "") - expect := "localhost 0 IN A 127.0.0.1" - assert.Equal(t, reader.Record("localhost", false), expect) - expect = "ip6-localhost 0 IN AAAA ::1" - assert.Equal(t, reader.Record("ip6-localhost", true), expect) - - // 通配符hosts - content = "1.1.1.1 www.*.org \n ::1 *.cn" - reader = NewReaderByText(content) - assert.Equal(t, reader.IP("www.test.org", false), "1.1.1.1") - assert.Equal(t, reader.IP("m.test.org", false), "") - assert.Equal(t, reader.IP("test.cn", true), "::1") - assert.Equal(t, reader.IP("test.cn", false), "") +func buildReq(host string, qType uint16) *dns.Msg { + msg := new(dns.Msg) + msg.Question = append(msg.Question, dns.Question{ + Name: host, + Qtype: qType, + Qclass: 0, + }) + return msg } -func TestNewFileReader(t *testing.T) { - filename := "go_test_hosts_file" - reader, err := NewReaderByFile(filename, 0) - assert.True(t, reader == nil) - assert.NotEqual(t, err, nil) - - // 写入测试文件 - content := "127.0.0.1 localhost\n::1 ip6-localhost" - _ = ioutil.WriteFile(filename, []byte(content), 0644) - reader, err = NewReaderByFile(filename, time.Second) - assert.Equal(t, err, nil) - assert.Equal(t, reader.IP("localhost", false), "127.0.0.1") - assert.Equal(t, reader.IP("ip6-localhost", true), "::1") - expect := "localhost 0 IN A 127.0.0.1" - assert.Equal(t, reader.Record("localhost", false), expect) - - content = "127.0.1.1 localhost\n::2 ip6-localhost" - _ = ioutil.WriteFile(filename, []byte(content), 0644) - // 1秒之后自动重载hosts - time.Sleep(time.Second) - assert.Equal(t, reader.IP("localhost", false), "127.0.1.1") - assert.Equal(t, reader.IP("ip6-localhost", true), "::2") - expect = "ip6-localhost 0 IN AAAA ::2" - assert.Equal(t, reader.Record("ip6-localhost", true), expect) - - _ = os.Remove(filename) +func TestNewHostReader(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + cfg := config.Conf{Hosts: map[string]string{ + "z.cn": "1.1.1.1", + }, HostsFiles: []string{ + "testdata/test.txt", + }} + r, err := NewDNSHosts(cfg) + assert.Nil(t, err) + assert.NotNil(t, r) + + resp := r.Get(buildReq("z.cn.", dns.TypeA)) + assert.NotNil(t, resp) + assert.Equal(t, "z.cn.\t0\tIN\tA\t1.1.1.1", resp.Answer[0].String()) + + cases := []struct { + host string + query uint16 + isNil bool + }{ + {"z.cn", dns.TypeA, false}, + {"z.cn", dns.TypeAAAA, true}, + {"comment1.com", dns.TypeA, true}, + {"comment2.com", dns.TypeA, true}, + {"space_suffix.com", dns.TypeA, false}, + {"hello.wildcard1.com", dns.TypeA, false}, + {"a.wildcard2.com", dns.TypeA, false}, + {"v6.com", dns.TypeA, true}, + {"v6.com", dns.TypeAAAA, false}, + } + for _, c := range cases { + t.Log(c) + resp = r.Get(buildReq(c.host, c.query)) + assert.Nil(t, err) + if c.isNil { + assert.Nil(t, resp) + } else { + assert.NotNil(t, resp) + } + } + + cfg = config.Conf{HostsFiles: []string{ + "testdata/invalid.txt", + }} + _, err = NewDNSHosts(cfg) + t.Logf("%+v", err) + assert.NotNil(t, err) + + cfg = config.Conf{HostsFiles: []string{ + "testdata/not_exists.txt", + }} + _, err = NewDNSHosts(cfg) + t.Logf("%+v", err) + assert.NotNil(t, err) +} + +func BenchmarkHostReader_Regexp(b *testing.B) { + hosts, err := NewDNSHosts(config.Conf{Hosts: map[string]string{ + "z.cn": "1.1.1.1", + "*.wd.cn": "1.1.1.1", + }}) + assert.Nil(b, err) + req := buildReq("test.wd.cn", dns.TypeA) + for i := 0; i < b.N; i++ { + resp := hosts.Get(req) + assert.NotNil(b, resp) + } +} + +func BenchmarkHostReader_Domain(b *testing.B) { + r, err := NewDNSHosts(config.Conf{Hosts: map[string]string{ + "z.cn": "1.1.1.1", + "*.wd.cn": "1.1.1.1", + }}) + assert.Nil(b, err) + req := buildReq("z.cn", dns.TypeA) + for i := 0; i < b.N; i++ { + resp := r.Get(req) + assert.NotNil(b, resp) + } } diff --git a/hosts/hosts_v2.go b/hosts/hosts_v2.go deleted file mode 100644 index 427b415..0000000 --- a/hosts/hosts_v2.go +++ /dev/null @@ -1,149 +0,0 @@ -package hosts - -import ( - "bufio" - "errors" - "fmt" - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/config" - "net" - "os" - "regexp" - "strings" - "sync/atomic" - "unicode" -) - -var ( - ErrUnknownQueryType = errors.New("unknown query type") - ErrInvalidIP = errors.New("invalid IP addr") - zeroIP = ipInfo{} -) - -type ipInfo struct { - val string - dt uint16 -} - -func (i ipInfo) Record(host string) string { - if i.dt == dns.TypeA { - return host + " 0 IN A " + i.val - } - return host + " 0 IN AAAA " + i.val -} - -func buildIPInfo(val string) (ipInfo, error) { - ip := net.ParseIP(val) - if ip.To4() != nil { - return ipInfo{val: val, dt: dns.TypeA}, nil - } else if ip.To16() != nil { - return ipInfo{val: val, dt: dns.TypeAAAA}, nil - } - return zeroIP, ErrInvalidIP -} - -// HostReader 管理hosts -type HostReader struct { - domainMap *atomic.Value // type: map[string]ipInfo - regexMap *atomic.Value // type: map[*regexp.Regexp]ipInfo -} - -func (h *HostReader) getIP(host string) (ipInfo, bool) { - if res, exists := h.domainMap.Load().(map[string]ipInfo)[host]; exists { - return res, true - } - for reg, res := range h.regexMap.Load().(map[*regexp.Regexp]ipInfo) { - if reg.MatchString(host) { - return res, true - } - } - return zeroIP, false -} - -func (h *HostReader) Record(host string, query uint16) (dns.RR, error) { - if query != dns.TypeA && query != dns.TypeAAAA { - return nil, ErrUnknownQueryType - } - ip, exists := h.getIP(host) - if !exists && strings.HasSuffix(host, ".") { - ip, exists = h.getIP(host[:len(host)-1]) - } - if !exists || ip.dt != query { - return nil, nil - } - return dns.NewRR(ip.Record(host)) -} - -func (h *HostReader) ReloadConfig(conf *config.Conf) error { - domainMap := make(map[string]ipInfo, len(conf.Hosts)) - regexMap := make(map[*regexp.Regexp]ipInfo, len(conf.Hosts)) - load := func(host, ipStr string) error { - ip, err := buildIPInfo(ipStr) - if err != nil { - return fmt.Errorf("parse %q to host failed: %w", ipStr, err) - } - if !strings.ContainsAny(host, "*?") { - domainMap[host] = ip - return nil - } - // wildcard to regexp - host = strings.Replace(host, ".", "\\.", -1) - host = strings.Replace(host, "*", ".*", -1) - host = strings.Replace(host, "?", ".", -1) - reg, err := regexp.Compile("^" + host + "$") - if err != nil { - return fmt.Errorf("build host regexp %q failed: %w", host, err) - } - regexMap[reg] = ip - return nil - } - // parse hosts - for host, ipStr := range conf.Hosts { - if err := load(host, ipStr); err != nil { - return err - } - } - // parse hosts files - files := make([]*os.File, 0, len(conf.HostsFiles)) - defer func() { - for _, f := range files { - _ = f.Close() - } - }() - for _, filename := range conf.HostsFiles { - file, err := os.Open(filename) - if err != nil { - return fmt.Errorf("load hosts file %q error: %w", filename, err) - } - files = append(files, file) - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { - continue - } - parts := strings.FieldsFunc(line, unicode.IsSpace) - if len(parts) < 2 { - continue - } - if err = load(parts[0], parts[1]); err != nil { - return fmt.Errorf("load hosts file %q error: %w", filename, err) - } - } - } - // reload - h.domainMap.Store(domainMap) - h.regexMap.Store(regexMap) - return nil -} - -func NewHostReader(conf *config.Conf) (*HostReader, error) { - r := &HostReader{ - domainMap: new(atomic.Value), - regexMap: new(atomic.Value), - } - if err := r.ReloadConfig(conf); err != nil { - return nil, err - } - return r, nil -} diff --git a/hosts/hosts_v2_test.go b/hosts/hosts_v2_test.go deleted file mode 100644 index 1b042cc..0000000 --- a/hosts/hosts_v2_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package hosts - -import ( - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/config" - "testing" -) - -func TestNewHostReader(t *testing.T) { - cfg := &config.Conf{Hosts: map[string]string{ - "z.cn": "1.1.1.1", - }, HostsFiles: []string{ - "testdata/test.txt", - }} - r, err := NewHostReader(cfg) - assert.Nil(t, err) - assert.NotNil(t, r) - - rr, err := r.Record("z.cn.", dns.TypeA) - assert.Nil(t, err) - assert.NotNil(t, rr) - assert.Equal(t, "z.cn.\t0\tIN\tA\t1.1.1.1", rr.String()) - - rr, err = r.Record("z.cn.", dns.TypeANY) - t.Logf("%+v", err) - assert.NotNil(t, err) - - cases := []struct { - host string - query uint16 - isNil bool - }{ - {"z.cn", dns.TypeA, false}, - {"z.cn", dns.TypeAAAA, true}, - {"comment1.com", dns.TypeA, true}, - {"comment2.com", dns.TypeA, true}, - {"space_suffix.com", dns.TypeA, false}, - {"hello.wildcard1.com", dns.TypeA, false}, - {"a.wildcard2.com", dns.TypeA, false}, - {"v6.com", dns.TypeA, true}, - {"v6.com", dns.TypeAAAA, false}, - } - for _, c := range cases { - t.Log(c) - rr, err = r.Record(c.host, c.query) - assert.Nil(t, err) - if c.isNil { - assert.Nil(t, rr) - } else { - assert.NotNil(t, rr) - } - } - - cfg = &config.Conf{HostsFiles: []string{ - "testdata/invalid.txt", - }} - r, err = NewHostReader(cfg) - t.Logf("%+v", err) - assert.NotNil(t, err) - - cfg = &config.Conf{HostsFiles: []string{ - "testdata/not_exists.txt", - }} - r, err = NewHostReader(cfg) - t.Logf("%+v", err) - assert.NotNil(t, err) -} diff --git a/inbound/handler.go b/inbound/handler.go new file mode 100644 index 0000000..0b40302 --- /dev/null +++ b/inbound/handler.go @@ -0,0 +1,274 @@ +package inbound + +import ( + "errors" + "fmt" + "github.com/miekg/dns" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/ts-dns/cache" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/hosts" + "github.com/wolf-joe/ts-dns/outbound" + "github.com/wolf-joe/ts-dns/redirector" + "strconv" + "strings" + "sync/atomic" + "time" + "unsafe" +) + +// region interface + +// IHandler ts-dns service handler +type IHandler interface { + dns.Handler + ReloadConfig(conf config.Conf) error + Stop() +} + +// NewHandler Build a service can handle dns request, life cycle start immediately +func NewHandler(conf config.Conf) (IHandler, error) { + h := new(handlerWrapper) + if err := h.ReloadConfig(conf); err != nil { + return nil, err + } + return h, nil +} + +// endregion + +// region wrapper +var ( + _ IHandler = &handlerWrapper{} +) + +// todo: add unittest +type handlerWrapper struct { + handlerPtr unsafe.Pointer // type: *handlerImpl +} + +func (w *handlerWrapper) ReloadConfig(conf config.Conf) error { + // create & start new handler + h, err := newHandle(conf) + if err != nil { + return fmt.Errorf("make new handler failed: %w", err) + } + h.start() + // swap handler + for { + old := atomic.LoadPointer(&w.handlerPtr) + if atomic.CompareAndSwapPointer(&w.handlerPtr, old, unsafe.Pointer(h)) { + if old != nil { + (*handlerImpl)(old).stop() + } + break + } + } + return nil +} + +func (w *handlerWrapper) ServeDNS(writer dns.ResponseWriter, req *dns.Msg) { + (*handlerImpl)(atomic.LoadPointer(&w.handlerPtr)).ServeDNS(writer, req) +} + +func (w *handlerWrapper) Stop() { + for { + old := atomic.LoadPointer(&w.handlerPtr) + if old == nil { + return + } + if atomic.CompareAndSwapPointer(&w.handlerPtr, old, nil) { + (*handlerImpl)(old).stop() + return + } + } +} + +// endregion + +func newHandle(conf config.Conf) (*handlerImpl, error) { + var err error + h := &handlerImpl{ + disableQTypes: map[uint16]bool{}, + cache: nil, + hosts: nil, + groups: nil, + redirector: nil, + } + // disable query types + if conf.DisableIPv6 { + h.disableQTypes[dns.TypeAAAA] = true + } + for _, qTypeStr := range conf.DisableQTypes { + qTypeStr = strings.ToUpper(qTypeStr) + if _, exists := dns.StringToType[qTypeStr]; !exists { + return nil, fmt.Errorf("unknown query type: %q", qTypeStr) + } + h.disableQTypes[dns.StringToType[qTypeStr]] = true + } + + // hosts & cache + h.hosts, err = hosts.NewDNSHosts(conf) + if err != nil { + return nil, fmt.Errorf("build hosts failed: %w", err) + } + h.cache, err = cache.NewDNSCache(conf) + if err != nil { + return nil, fmt.Errorf("build cache failed: %w", err) + } + h.groups, err = outbound.BuildGroups(conf) + if err != nil { + return nil, fmt.Errorf("build groups failed: %w", err) + } + for _, group := range h.groups { + if group.IsFallback() { + h.fallbackGroup = group + } + } + if h.fallbackGroup == nil { + return nil, errors.New("fallback group not found") + } + h.redirector, err = redirector.NewRedirector(conf, h.groups) + if err != nil { + return nil, fmt.Errorf("build redirector failed: %w", err) + } + return h, nil +} + +// region impl +type handlerImpl struct { + disableQTypes map[uint16]bool + cache cache.IDNSCache + hosts hosts.IDNSHosts + groups map[string]outbound.IGroup + fallbackGroup outbound.IGroup + redirector redirector.Redirector +} + +func (h *handlerImpl) ServeDNS(writer dns.ResponseWriter, req *dns.Msg) { + resp := h.handle(writer, req) + if resp == nil { + resp = new(dns.Msg) + } + if !resp.Response { + resp.SetReply(req) + } + _ = writer.WriteMsg(resp) + _ = writer.Close() +} + +func (h *handlerImpl) handle(writer dns.ResponseWriter, req *dns.Msg) (resp *dns.Msg) { + // region log + _info := struct { + blocked bool + hitHosts bool + hitCache bool + matched outbound.IGroup + fallback bool + redirect outbound.IGroup + }{} + begin := time.Now() + defer func() { + fields := logrus.Fields{ + "cost": strconv.FormatInt(time.Since(begin).Milliseconds(), 10) + "ms", + "remote": writer.RemoteAddr().String(), + } + if _info.blocked { + fields["blocked"] = true + } + if _info.hitHosts { + fields["hit_hosts"] = true + } + if _info.hitCache { + fields["hit_cache"] = true + } + if len(req.Question) > 0 { + fields["question"] = req.Question[0].Name + fields["q_type"] = dns.TypeToString[req.Question[0].Qtype] + } + if _info.matched != nil { + if _info.fallback { + fields["group"] = "_" + _info.matched.Name() + } else { + fields["group"] = _info.matched.Name() + } + } + if _info.redirect != nil { + fields["redir"] = _info.redirect.Name() + } + if resp == nil { + fields["answer"] = "nil" + } else { + fields["answer"] = len(resp.Answer) + } + if _info.blocked || _info.hitCache || _info.hitHosts { + logrus.WithFields(fields).Debug() + } else { + logrus.WithFields(fields).Info() + } + }() + // endregion + for _, question := range req.Question { + if h.disableQTypes[question.Qtype] { + _info.blocked = true + return nil // disabled + } + } + if resp = h.hosts.Get(req); resp != nil { + _info.hitHosts = true + return resp + } + if resp = h.cache.Get(req); resp != nil { + _info.hitCache = true + return resp + } + + // handle by matched group + var matched outbound.IGroup + for _, group := range h.groups { + if group.Match(req) { + matched = group + resp = group.Handle(req) + break + } + } + if matched == nil { + matched = h.fallbackGroup + resp = h.fallbackGroup.Handle(req) + _info.fallback = true + } + _info.matched = matched + + // redirect + if h.redirector != nil { + if group := h.redirector(matched, req, resp); group != nil { + matched = group + resp = group.Handle(req) + _info.redirect = group + } + } + + // finally + matched.PostProcess(req, resp) + h.cache.Set(req, resp) + return resp +} + +func (h *handlerImpl) start() { + for _, group := range h.groups { + group.Start(h) + } + h.cache.Start() + logrus.Debugf("start handler success") +} + +func (h *handlerImpl) stop() { + logrus.Debugf("stop handler") + for _, group := range h.groups { + group.Stop() + } + h.cache.Stop() + logrus.Debugf("stop handler success") +} + +// endregion diff --git a/inbound/handler_test.go b/inbound/handler_test.go new file mode 100644 index 0000000..47cc1da --- /dev/null +++ b/inbound/handler_test.go @@ -0,0 +1,156 @@ +package inbound + +import ( + "github.com/miekg/dns" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/outbound" + "github.com/wolf-joe/ts-dns/utils" + "testing" +) + +func buildReq(name string, qType uint16) *dns.Msg { + return &dns.Msg{Question: []dns.Question{{ + Name: name, Qtype: qType, + }}} +} + +func Test_handlerImpl_ServeDNS(t *testing.T) { +} + +func TestNewHandler(t *testing.T) { + h, err := NewHandler(config.Conf{ + HostsFiles: nil, + Hosts: nil, + Cache: config.CacheConf{}, + Groups: map[string]config.Group{"default": {}}, + DisableIPv6: false, + DisableQTypes: nil, + Redirectors: nil, + Listen: "", + }) + assert.Nil(t, err) + assert.NotNil(t, h) + + err = h.ReloadConfig(config.Conf{ + HostsFiles: nil, + Hosts: nil, + Cache: config.CacheConf{}, + Groups: map[string]config.Group{"default": {}}, + DisableIPv6: false, + DisableQTypes: nil, + Redirectors: nil, + Listen: "", + }) + assert.Nil(t, err) + rw := utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("ip.cn", dns.TypeA)) + assert.NotNil(t, rw.Msg) + h.Stop() + h.Stop() + + _, err = NewHandler(config.Conf{ + HostsFiles: []string{"not_exists.txt"}, + Hosts: nil, + Cache: config.CacheConf{}, + Groups: nil, + DisableIPv6: false, + DisableQTypes: nil, + Redirectors: nil, + Listen: "", + }) + assert.NotNil(t, err) + t.Log(err) +} + +func Test_newHandle(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + defaultConf := config.Conf{ + HostsFiles: nil, + Hosts: map[string]string{ + "z.cn": "1.1.1.1", "v6.cn": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + }, + Cache: config.CacheConf{}, + Groups: map[string]config.Group{"fallback": {}}, + DisableIPv6: false, + DisableQTypes: nil, + Redirectors: nil, + Listen: "", + } + t.Run("hosts", func(t *testing.T) { + conf := defaultConf + h, err := newHandle(conf) + assert.Nil(t, err) + assert.NotNil(t, h) + + rw := utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("z.cn", dns.TypeA)) + assert.NotNil(t, rw.Msg) + assert.NotNil(t, rw.Msg.Answer) + + rw = utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("v6.cn", dns.TypeAAAA)) + t.Log(rw.Msg.String()) + assert.NotNil(t, rw.Msg) + assert.NotNil(t, rw.Msg.Answer) + }) + t.Run("disable", func(t *testing.T) { + conf := defaultConf + conf.DisableQTypes = []string{"???"} + _, err := newHandle(conf) + assert.NotNil(t, err) + t.Log(err) + + conf.DisableQTypes = []string{"A"} + conf.DisableIPv6 = true + h, err := newHandle(conf) + assert.Nil(t, err) + assert.NotNil(t, h) + rw := utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("z.cn", dns.TypeA)) + assert.NotNil(t, rw.Msg) + assert.Nil(t, rw.Msg.Answer) + + rw = utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("v6.cn", dns.TypeAAAA)) + assert.NotNil(t, rw.Msg) + assert.Nil(t, rw.Msg.Answer) + }) + t.Run("cache", func(t *testing.T) { + conf := defaultConf + conf.Cache.Size = 10 + h, err := newHandle(conf) + assert.Nil(t, err) + assert.NotNil(t, h) + + req := buildReq("a.cn", dns.TypeA) + h.cache.Set(req, &dns.Msg{ + Answer: []dns.RR{&dns.A{}, &dns.AAAA{}}, + }) + rw := utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("a.cn", dns.TypeA)) + assert.NotNil(t, rw.Msg) + assert.Equal(t, 2, len(rw.Msg.Answer)) + }) + t.Run("group", func(t *testing.T) { + conf := defaultConf + conf.Groups["a"] = config.Group{ + Rules: []string{"a.cn"}, + } + h, err := newHandle(conf) + assert.Nil(t, err) + assert.NotNil(t, h) + + var srcGroup outbound.IGroup + h.redirector = func(src outbound.IGroup, req, resp *dns.Msg) outbound.IGroup { + srcGroup = src + return src + } + + rw := utils.NewFakeRespWriter() + h.ServeDNS(rw, buildReq("a.cn", dns.TypeA)) + assert.NotNil(t, srcGroup) + assert.Equal(t, "a", srcGroup.Name()) + }) +} diff --git a/inbound/server.go b/inbound/server.go deleted file mode 100644 index 0ea55ba..0000000 --- a/inbound/server.go +++ /dev/null @@ -1,294 +0,0 @@ -package inbound - -import ( - "context" - "strings" - "sync" - - log "github.com/Sirupsen/logrus" - "github.com/janeczku/go-ipset/ipset" - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/common" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" -) - -// Group 各域名组相关配置 -type Group struct { - Callers []outbound.Caller - Matcher *matcher.ABPlus - IPSet *ipset.IPSet - Concurrent bool - FastestV4 bool - TCPPingPort int - ECS *dns.EDNS0_SUBNET - NoCookie bool -} - -// CallDNS 向组内的dns服务器转发请求,可能返回nil -func (group *Group) CallDNS(ctx context.Context, request *dns.Msg) *dns.Msg { - if len(group.Callers) == 0 || request == nil { - return nil - } - request = request.Copy() - common.SetDefaultECS(request, group.ECS) - if group.NoCookie { - common.RemoveEDNSCookie(request) - } - // 并发用的channel - ch := make(chan *dns.Msg, len(group.Callers)) - // 包裹Caller.Call,方便实现并发 - call := func(caller outbound.Caller, request *dns.Msg) *dns.Msg { - r, err := caller.Call(request) - if err != nil { - utils.CtxError(ctx, "query dns error: "+err.Error()) - } - ch <- r - return r - } - // 遍历DNS服务器 - for _, caller := range group.Callers { - if group.Concurrent || group.FastestV4 { - go call(caller, request) - } else if r := call(caller, request); r != nil { - return r - } - } - // 并发情况下依次提取channel中的返回值 - if group.Concurrent && !group.FastestV4 { - for i := 0; i < len(group.Callers); i++ { - if r := <-ch; r != nil { - return r - } - } - } else if group.FastestV4 { // 选择ping值最低的IPv4地址作为返回值 - return fastestA(ctx, ch, len(group.Callers), group.TCPPingPort) - } - return nil -} - -// AddIPSet 将dns响应中所有的ipv4地址加入group指定的ipset -func (group *Group) AddIPSet(ctx context.Context, r *dns.Msg) { - if group.IPSet == nil || r == nil { - return - } - for _, a := range common.ExtractA(r) { - if err := group.IPSet.Add(a.A.String(), group.IPSet.Timeout); err != nil { - utils.CtxError(ctx, "add ipset error: "+err.Error()) - } - } - return -} - -// QueryLogger 打印请求日志的配置 -type QueryLogger struct { - logger *log.Logger - ignoreQTypes map[uint16]bool - ignoreHosts bool - ignoreCache bool -} - -// ShouldIgnore 判断该次请求是否应该打印请求日志 -func (logger *QueryLogger) ShouldIgnore(request *dns.Msg, hitHosts, hitCache bool) bool { - if logger.logger.Level == log.DebugLevel { - return false - } - if hitHosts && logger.ignoreHosts || hitCache && logger.ignoreCache { - return true - } - for _, question := range request.Question { - if ignore, ok := logger.ignoreQTypes[question.Qtype]; ok && ignore { - return true - } - } - return false -} - -// GetFields 从dns请求中获取用于打印日志的fields -func (logger *QueryLogger) GetFields(writer dns.ResponseWriter, request *dns.Msg) log.Fields { - fields := log.Fields{"SRC": writer.RemoteAddr().String()} - for _, question := range request.Question { - fields["QUESTION"] = question.Name - fields["Q_TYPE"] = dns.Type(question.Qtype).String() - break - } - return fields -} - -// NewQueryLogger 创建一个QueryLogger -func NewQueryLogger(logger *log.Logger, ignoreQTypes []string, - ignoreHosts, ignoreCache bool) *QueryLogger { - queryLogger := &QueryLogger{ - logger: logger, ignoreHosts: ignoreHosts, ignoreCache: ignoreCache, - ignoreQTypes: make(map[uint16]bool, len(ignoreQTypes)), - } - for _, qType := range ignoreQTypes { - if t, ok := dns.StringToType[strings.ToUpper(qType)]; ok { - queryLogger.ignoreQTypes[t] = true - } - } - return queryLogger -} - -// Handler 存储主要配置的dns请求处理器,程序核心 -type Handler struct { - Mux *sync.RWMutex - Listen string - Network string - DisableIPv6 bool - Cache *cache.DNSCache - GFWMatcher *matcher.ABPlus - CNIP *cache.RamSet - HostsReaders []hosts.Reader - Groups map[string]*Group - QLogger *QueryLogger - DisableQTypes map[string]bool -} - -// HitHosts 如dns请求匹配hosts,则生成对应dns记录并返回。否则返回nil -func (handler *Handler) HitHosts(ctx context.Context, request *dns.Msg) *dns.Msg { - question := request.Question[0] - if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { - ipv6 := question.Qtype == dns.TypeAAAA - for _, reader := range handler.HostsReaders { - record, hostname := "", question.Name - if record = reader.Record(hostname, ipv6); record == "" { - // 去掉末尾的根域名再找一次 - record = reader.Record(hostname[:len(hostname)-1], ipv6) - } - if record != "" { - if ret, err := dns.NewRR(record); err != nil { - utils.CtxError(ctx, "make DNS.RR error: "+err.Error()) - } else { - r := new(dns.Msg) - r.Answer = append(r.Answer, ret) - return r - } - } - } - } - return nil -} - -// ServeDNS 处理dns请求,程序核心函数 -func (handler *Handler) ServeDNS(writer dns.ResponseWriter, request *dns.Msg) { - handler.Mux.RLock() // 申请读锁,持续整个请求 - ctx := utils.NewCtx(handler.QLogger.logger, request.Id) - ctx = utils.WithFields(ctx, handler.QLogger.GetFields(writer, request)) - var r *dns.Msg - var group *Group - defer func() { - if r == nil { - r = &dns.Msg{} - } - r.SetReply(request) // 写入响应 - utils.CtxDebug(ctx, "response: %q", r.Answer) - _ = writer.WriteMsg(r) - if group != nil { - group.AddIPSet(ctx, r) // 写入IPSet - } - handler.Mux.RUnlock() // 读锁解除 - _ = writer.Close() // 结束连接 - }() - - question := request.Question[0] - utils.CtxDebug(ctx, "question: %q, extra: %q", request.Question, request.Extra) - if handler.DisableIPv6 && question.Qtype == dns.TypeAAAA { - r = &dns.Msg{} - return // 禁用IPv6时直接返回 - } - if qType := dns.TypeToString[question.Qtype]; handler.DisableQTypes[qType] { - r = &dns.Msg{} - return // 禁用指定查询类型 - } - // 检测是否命中hosts - if r = handler.HitHosts(ctx, request); r != nil { - if !handler.QLogger.ShouldIgnore(request, true, false) { - utils.CtxInfo(ctx, "hit hosts") - } - return - } - // 检测是否命中dns缓存 - if r = handler.Cache.Get(request); r != nil { - if !handler.QLogger.ShouldIgnore(request, false, true) { - utils.CtxInfo(ctx, "hit cache") - } - return - } - - // 判断域名是否匹配指定规则 - var name string - for name, group = range handler.Groups { - if match, ok := group.Matcher.Match(question.Name); ok && match { - utils.CtxInfo(ctx, "match by rules, group: "+name) - r = group.CallDNS(ctx, request) - // 设置dns缓存 - handler.Cache.Set(request, r) - return - } - } - // 先用clean组dns解析 - group = handler.Groups["clean"] // 设置group变量以在defer里添加ipset - r = group.CallDNS(ctx, request) - if allInRange(r, handler.CNIP) { - // 未出现非cn ip,流程结束 - utils.CtxInfo(ctx, "cn/empty ipv4, group: clean") - } else if blocked, ok := handler.GFWMatcher.Match(question.Name); !ok || !blocked { - // 出现非cn ip但域名不匹配gfwlist,流程结束 - utils.CtxInfo(ctx, "not match gfwlist, group: clean") - } else { - // 出现非cn ip且域名匹配gfwlist,用dirty组dns再次解析 - utils.CtxInfo(ctx, "match gfwlist, group: dirty") - group = handler.Groups["dirty"] // 设置group变量以在defer里添加ipset - r = group.CallDNS(ctx, request) - } - // 设置dns缓存 - handler.Cache.Set(request, r) -} - -// Refresh 刷新配置,复制target中除Mux、Listen之外的值 -func (handler *Handler) Refresh(target *Handler) { - handler.Mux.Lock() - defer handler.Mux.Unlock() - - if target.Cache != nil { - handler.Cache = target.Cache - } - if target.GFWMatcher != nil { - handler.GFWMatcher = target.GFWMatcher - } - if target.CNIP != nil { - handler.CNIP = target.CNIP - } - if target.HostsReaders != nil { - handler.HostsReaders = target.HostsReaders - } - if target.Groups != nil { - for _, group := range target.Groups { - for _, caller := range group.Callers { - caller.Exit() - } - } - handler.Groups = target.Groups - } - if target.QLogger != nil { - handler.QLogger = target.QLogger - } - handler.DisableIPv6 = target.DisableIPv6 -} - -// IsValid 判断Handler是否符合运行条件 -func (handler *Handler) IsValid() bool { - if handler.Groups == nil { - return false - } - clean, dirty := handler.Groups["clean"], handler.Groups["dirty"] - if clean == nil || len(clean.Callers) <= 0 || dirty == nil || len(dirty.Callers) <= 0 { - log.Errorf("dns of clean/dirty group cannot be empty") - return false - } - return true -} diff --git a/inbound/server_test.go b/inbound/server_test.go deleted file mode 100644 index 3f07673..0000000 --- a/inbound/server_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "net" - "sync" - "testing" - - log "github.com/Sirupsen/logrus" - "github.com/agiledragon/gomonkey" - "github.com/janeczku/go-ipset/ipset" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils/mock" - "github.com/wolf-joe/ts-dns/hosts" - "github.com/wolf-joe/ts-dns/matcher" - "github.com/wolf-joe/ts-dns/outbound" -) - -type MockRespWriter struct { - dns.ResponseWriter - r *dns.Msg -} - -func (r *MockRespWriter) WriteMsg(resp *dns.Msg) error { - r.r = resp - return nil -} - -func (r *MockRespWriter) Close() error { - return nil -} - -func (r *MockRespWriter) RemoteAddr() net.Addr { - return &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 11111} -} - -func TestHandler_Resolve(t *testing.T) { - // 初始化Handler - handler := &Handler{Mux: new(sync.RWMutex), Cache: cache.NewDNSCache(0, 0, 0), - GFWMatcher: matcher.NewABPByText(""), CNIP: cache.NewRamSetByText(""), - HostsReaders: []hosts.Reader{hosts.NewReaderByText("1.1.1.1 dns1")}, - } - // 初始化Caller - ctx := context.Background() - caller1, err := outbound.NewDoHCallerV2(ctx, "https://dns1/", nil) - assert.Nil(t, err) - assert.NotNil(t, caller1) - caller2, err := outbound.NewDoHCallerV2(ctx, "https://dns2/", nil) - assert.Nil(t, err) - assert.NotNil(t, caller2) - callers := []outbound.Caller{caller1, caller2, &outbound.DNSCaller{}} - handler.Groups = map[string]*Group{"clean": {Callers: callers}} -} - -func TestHandler(t *testing.T) { - // 初始化handler - handler := &Handler{Mux: new(sync.RWMutex), Cache: cache.NewDNSCache(0, 0, 0), - GFWMatcher: matcher.NewABPByText(""), CNIP: cache.NewRamSetByText(""), - HostsReaders: []hosts.Reader{hosts.NewReaderByText("")}, - } - handler.QLogger = NewQueryLogger(log.New(), []string{"AAAA"}, false, false) - callers := []outbound.Caller{&outbound.DNSCaller{}} - group := &Group{Callers: callers, Matcher: matcher.NewABPByText(""), IPSet: &ipset.IPSet{}} - handler.Groups = map[string]*Group{"clean": group, "dirty": group} - // 初始化所需参数和返回值 - resp := &dns.Msg{Answer: []dns.RR{&dns.A{A: net.ParseIP("1.1.1.1")}}} - writer, req := &MockRespWriter{}, &dns.Msg{} - req.SetQuestion("ip.cn.", dns.TypeAAAA) - - // 测试DisableIPv6 - handler.DisableIPv6 = true - handler.ServeDNS(writer, req) - assert.NotNil(t, writer.r) - assert.Equal(t, len(writer.r.Answer), 0) - - // 测试DisableIPv6 - handler.DisableIPv6 = false - handler.DisableQTypes = map[string]bool{"AAAA": true} - handler.ServeDNS(writer, req) - assert.NotNil(t, writer.r) - assert.Equal(t, len(writer.r.Answer), 0) - - req.SetQuestion("ip.cn.", dns.TypeA) - - mocker := mock.Mocker{} - defer mocker.Reset() - - // 测试HitHosts - mocker.MethodSeq(handler.HostsReaders[0], "Record", []gomonkey.Params{ - {""}, {""}, {"ip.cn 0 IN A ???"}, {"ip.cn 0 IN A 1.1.1.1"}, - }) - ctx := context.Background() - assert.Nil(t, handler.HitHosts(ctx, req)) // Record返回空串(需要两个返回值) - assert.Nil(t, handler.HitHosts(ctx, req)) // Record返回值格式不正确 - assert.NotNil(t, handler.HitHosts(ctx, req)) // Record返回值正常 - - // 测试ServeDNS前半部分 - // mocker HitHosts - mocker.MethodSeq(handler, "HitHosts", []gomonkey.Params{ - {resp}, {nil}, {nil}, {nil}, // 前半部分用 - {nil}, {nil}, {nil}, - }) - handler.ServeDNS(writer, req) // 命中hosts - assert.Equal(t, writer.r, resp) - // mock缓存 - mocker.MethodSeq(handler.Cache, "Get", []gomonkey.Params{ - {resp}, {nil}, {nil}, // 前半部分用 - {nil}, {nil}, {nil}, - }) - handler.ServeDNS(writer, req) // 命中缓存 - assert.Equal(t, writer.r, resp) - // mocker 规则匹配结果 - mocker.MethodSeq(group.Matcher, "Match", []gomonkey.Params{ - {true, true}, {true, true}, // 前半部分用,每次只需只包含一次匹配 - // 后半部分需要两个不匹配跳过规则(可能要再加上GFWList的匹配/不匹配) - {false, false}, {false, false}, - {false, false}, {false, false}, {false, false}, - {false, false}, {false, false}, {true, true}, - }) - // 规则匹配后mock CallDNS - mocker.MethodSeq(group, "CallDNS", []gomonkey.Params{ - {resp}, {nil}, // 前半部分用 - {resp}, {resp}, {resp}, {resp}, - }) - handler.ServeDNS(writer, req) // 命中规则 - assert.Equal(t, writer.r, resp) - handler.ServeDNS(writer, req) // 命中规则 - assert.NotNil(t, writer.r) // CallDNS返回空后直接返回&dns.Msg{} - - // 测试ServeDNS后半部分:CN IP+GFWList - // mocker allInRange - mocker.FuncSeq(allInRange, []gomonkey.Params{ - {true}, {false}, {false}, - }) - handler.ServeDNS(writer, req) // 未出现非cn ip,直接返回 - assert.Equal(t, writer.r, resp) - handler.ServeDNS(writer, req) // 出现非cn ip但不匹配GFWList,直接返回 - assert.Equal(t, writer.r, resp) - handler.ServeDNS(writer, req) // 出现非cn ip且匹配GFWList,调dirty组CallDNS并返回 - assert.Equal(t, writer.r, resp) - - // 测试Refresh - handler.Refresh(handler) - // 测试IsValid - assert.True(t, handler.IsValid()) - handler = &Handler{} - assert.False(t, handler.IsValid()) - handler.Groups = map[string]*Group{} - assert.False(t, handler.IsValid()) -} - -func TestGroup(t *testing.T) { - callers := []outbound.Caller{&outbound.DNSCaller{}} - group := &Group{Callers: callers, Matcher: matcher.NewABPByText(""), - IPSet: &ipset.IPSet{}, NoCookie: true} - - mocker := mock.Mocker{} - defer mocker.Reset() - - ipv4 := net.IPv4(1, 1, 1, 1) - resp := &dns.Msg{Answer: []dns.RR{&dns.A{A: ipv4}}} - // 测试CallDNS - assert.Nil(t, group.CallDNS(nil, nil)) - mocker.MethodSeq(callers[0], "Call", []gomonkey.Params{ - {nil, fmt.Errorf("err")}, {resp, nil}, - {nil, fmt.Errorf("err")}, {resp, nil}, - {nil, fmt.Errorf("err")}, {resp, nil}, - }) - ctx := context.Background() - assert.Nil(t, group.CallDNS(ctx, &dns.Msg{})) // Call返回error - assert.NotNil(t, group.CallDNS(ctx, &dns.Msg{})) // Call正常返回 - // 测试并发CallDNS。两个Caller的并发在单测(-race)时会和mock冲突,这里就不测了 - //group.Callers = append(group.Callers, &outbound.DNSCaller{}) - group.Concurrent = true - assert.Nil(t, group.CallDNS(ctx, &dns.Msg{})) - assert.NotNil(t, group.CallDNS(ctx, &dns.Msg{})) - group.FastestV4 = true - assert.Nil(t, group.CallDNS(ctx, &dns.Msg{})) - assert.NotNil(t, group.CallDNS(ctx, &dns.Msg{})) - // 测试AddIPSet - group.AddIPSet(ctx, nil) - mocker.MethodSeq(group.IPSet, "Add", []gomonkey.Params{ - {fmt.Errorf("err")}, {nil}, - }) - group.AddIPSet(ctx, resp) // Add返回error - group.AddIPSet(ctx, resp) // Add正常返回 -} - -func TestQueryLogger(t *testing.T) { - log.SetLevel(log.DebugLevel) - logger := NewQueryLogger(log.StandardLogger(), []string{"A"}, false, false) - assert.False(t, logger.ShouldIgnore(nil, false, false)) - - log.SetLevel(log.InfoLevel) - logger = NewQueryLogger(log.StandardLogger(), []string{"A"}, true, false) - assert.True(t, logger.ShouldIgnore(nil, true, false)) - - logger = NewQueryLogger(log.StandardLogger(), []string{"A"}, false, true) - assert.True(t, logger.ShouldIgnore(nil, false, true)) - req := &dns.Msg{Question: []dns.Question{{Qtype: dns.TypeA}}} - assert.True(t, logger.ShouldIgnore(req, false, false)) -} diff --git a/inbound/tools.go b/inbound/tools.go deleted file mode 100644 index e9c230d..0000000 --- a/inbound/tools.go +++ /dev/null @@ -1,90 +0,0 @@ -package inbound - -import ( - "context" - "net" - "time" - - "github.com/miekg/dns" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/common" - "github.com/wolf-joe/ts-dns/core/utils" -) - -const ( - pingTimeout = 500 * time.Millisecond -) - -// 如dns响应中所有ipv4地址都在目标范围内(或没有ipv4地址)返回true,否则返回False -func allInRange(r *dns.Msg, ipRange *cache.RamSet) bool { - for _, a := range common.ExtractA(r) { - if ipv4 := net.ParseIP(a.A.String()).To4(); ipv4 != nil && !ipRange.Contain(ipv4) { - return false - } - } - return true -} - -func fastestA(ctx context.Context, ch <-chan *dns.Msg, chLen int, tcpPort int) *dns.Msg { - if chLen == 0 { - return nil - } - const maxGoNum = 15 // 最大并发数量 - // 从msg ch中提取所有IPv4地址,并建立IPv4地址到msg的映射 - allIP := make([]string, 0, maxGoNum) - msgMap := make(map[string]*dns.Msg, maxGoNum) - var fastestMsg *dns.Msg // 最早抵达的msg,当测速失败时使用该响应返回 - for i := 0; i < chLen; i++ { - msg := <-ch - if fastestMsg == nil { - fastestMsg = msg - } - for _, a := range common.ExtractA(msg) { - ipV4 := a.A.String() - if _, exists := msgMap[ipV4]; !exists { - allIP = append(allIP, ipV4) - msgMap[ipV4] = msg - if len(msgMap) >= maxGoNum { - goto doPing - } - } - } - } -doPing: - switch len(msgMap) { - case 0: // 没有任何IPv4地址 - return fastestMsg - case 1: // 只有一个IPv4地址 - for _, msg := range msgMap { - return msg - } - } - // 并发测速 - begin := time.Now() - pingDone := make(chan string, len(msgMap)) - for ipV4 := range msgMap { - go func(addr string) { - if err := utils.PingIP(addr, tcpPort, pingTimeout); err == nil { - pingDone <- addr - } - }(ipV4) - } - var fastestIP string // 第一个从resCh返回的地址就是ping值最低的地址 - select { - case fastestIP = <-pingDone: - case <-time.After(pingTimeout): - } - cost := time.Now().Sub(begin).Milliseconds() - utils.CtxDebug(ctx, "fastest ip of %s: %s(%dms)", allIP, fastestIP, cost) - if msg, exists := msgMap[fastestIP]; exists && fastestIP != "" { - // 删除msg内除fastestIP之外的其它A记录 - for i := 0; i < len(msg.Answer); i++ { - if a, ok := msg.Answer[i].(*dns.A); ok && a.A.String() != fastestIP { - msg.Answer = append(msg.Answer[:i], msg.Answer[i+1:]...) - i-- - } - } - return msg - } - return fastestMsg -} diff --git a/inbound/tools_test.go b/inbound/tools_test.go deleted file mode 100644 index dabbf12..0000000 --- a/inbound/tools_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package inbound - -import ( - "errors" - "time" - - "github.com/Sirupsen/logrus" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/cache" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/core/utils/mock" - - "net" - "testing" -) - -func TestAllInRange(t *testing.T) { - resp := &dns.Msg{Answer: []dns.RR{&dns.A{A: net.IPv4(1, 1, 1, 1)}}} - assert.False(t, allInRange(resp, cache.NewRamSetByText(""))) - assert.True(t, allInRange(resp, cache.NewRamSetByText("1.1.1.1"))) -} - -func TestFastestA(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) - tcpPort := -1 - ch := make(chan *dns.Msg, 3) - emptyMsg := &dns.Msg{} - - mocker := mock.Mocker{} - defer mocker.Reset() - mocker.Func(utils.PingIP, func(string, int, time.Duration) error { - return errors.New("cannot ping now") - }) - - ch <- emptyMsg - assert.Nil(t, fastestA(ctx, ch, 0, tcpPort)) - assert.Equal(t, emptyMsg, fastestA(ctx, ch, 1, tcpPort)) - - ch <- &dns.Msg{Answer: []dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}}} - ch <- &dns.Msg{Answer: []dns.RR{&dns.A{A: []byte{1, 1, 1, 1}}}} - assert.NotNil(t, fastestA(ctx, ch, 2, tcpPort)) - - makeMsg := func() *dns.Msg { - msg := &dns.Msg{} - for i := byte(1); i < 255; i++ { - msg.Answer = append(msg.Answer, &dns.A{A: []byte{1, 1, 1, i}}) - } - return msg - } - msg := makeMsg() - ch <- nil - ch <- msg - assert.Equal(t, msg, fastestA(ctx, ch, 2, tcpPort)) - - mocker.Func(utils.PingIP, func(addr string, _ int, _ time.Duration) error { - switch addr { - case "1.1.1.10": - return nil - case "1.1.1.1", "1.1.1.2", "1.1.1.3": - time.Sleep(50 * time.Millisecond) - return nil - default: - return errors.New("timeout") - } - }) - ch <- nil - ch <- makeMsg() - ch <- nil - msg = fastestA(ctx, ch, 3, tcpPort) - assert.NotNil(t, msg) - assert.Equal(t, 1, len(msg.Answer)) - assert.Equal(t, "1.1.1.10", msg.Answer[0].(*dns.A).A.String()) - - time.Sleep(100 * time.Millisecond) -} diff --git a/matcher/adblock.go b/matcher/adblock.go index 6ebb6da..a046a9d 100644 --- a/matcher/adblock.go +++ b/matcher/adblock.go @@ -2,6 +2,7 @@ package matcher import ( "encoding/base64" + "fmt" "io/ioutil" "regexp" "strings" @@ -9,7 +10,6 @@ import ( // ABPlus 基于部分AdBlock Plus规则的域名匹配器 type ABPlus struct { - DomainMatcher isBlocked map[string]bool blockedRegs []*regexp.Regexp unblockedRegs []*regexp.Regexp @@ -98,7 +98,7 @@ func NewABPByText(text string) (matcher *ABPlus) { domain := extractDomain(line) // 提取规则中的域名 // 判断域名中是否有通配符 - if strings.Index(domain, "*") != -1 { + if strings.Contains(domain, "*") { // 通配符表达式转正则表达式 regStr := strings.Replace(domain, ".", "\\.", -1) regStr = strings.Replace(regStr, "*", ".*", -1) @@ -129,22 +129,19 @@ func NewABPByText(text string) (matcher *ABPlus) { } // NewABPByFile 从文件内容读取AdBlock Plus规则 -func NewABPByFile(filename string, b64decode bool) (checker *ABPlus, err error) { - if filename == "" { - return NewABPByText(""), nil +func NewABPByFile(filename string, b64decode bool) (*ABPlus, error) { + raw, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("read file %q failed: %w", filename, err) } - var raw []byte - var text string - if raw, err = ioutil.ReadFile(filename); err == nil { - text = string(raw) - if b64decode { - if raw, err = base64.StdEncoding.DecodeString(text); err == nil { - text = string(raw) - } + if b64decode { + dst := make([]byte, len(raw)) + var n int + n, err = base64.StdEncoding.Decode(dst, raw) + if err != nil { + return nil, fmt.Errorf("decode base64 failed: %w", err) } + raw = dst[:n] } - if err != nil { - return nil, err - } - return NewABPByText(text), nil + return NewABPByText(string(raw)), nil } diff --git a/matcher/adblock_test.go b/matcher/adblock_test.go index 9b2c3e8..1ed6c46 100644 --- a/matcher/adblock_test.go +++ b/matcher/adblock_test.go @@ -22,24 +22,20 @@ unknown func TestNewChecker(t *testing.T) { filename := "go_test_adblock.txt" - // 空文件名 - matcher, err := NewABPByFile("", false) - assert.NotNil(t, matcher) - assert.Nil(t, err) // 文件不存在 - matcher, err = NewABPByFile(filename, false) + _, err := NewABPByFile(filename, false) assert.NotEqual(t, err, nil) // 写入不正确内容 content := base64.StdEncoding.EncodeToString([]byte(text)) + "???" _ = ioutil.WriteFile(filename, []byte(content), 0644) // 读取失败 - matcher, err = NewABPByFile(filename, true) + _, err = NewABPByFile(filename, true) assert.NotEqual(t, err, nil) // 写入正确内容 content = base64.StdEncoding.EncodeToString([]byte(text)) _ = ioutil.WriteFile(filename, []byte(content), 0644) // 读取成功 - matcher, err = NewABPByFile(filename, true) + matcher, err := NewABPByFile(filename, true) assert.NotEqual(t, matcher, nil) assert.Equal(t, err, nil) // 移除生成的文件 @@ -83,3 +79,11 @@ func TestABPlus_Extend(t *testing.T) { assert.True(t, matched) assert.True(t, ok) } + +func TestNewABPByFile(t *testing.T) { + matcher, err := NewABPByFile("testdata/gfwlist.txt", true) + assert.Nil(t, err) + matched, ok := matcher.Match("google.com") + assert.True(t, ok) + assert.True(t, matched) +} diff --git a/matcher/matcher.go b/matcher/matcher.go deleted file mode 100644 index e796a34..0000000 --- a/matcher/matcher.go +++ /dev/null @@ -1,6 +0,0 @@ -package matcher - -// DomainMatcher 域名匹配器基类 -type DomainMatcher interface { - Match(domain string) (match bool, ok bool) -} diff --git a/matcher/testdata/gfwlist.txt b/matcher/testdata/gfwlist.txt new file mode 100644 index 0000000..a465576 --- /dev/null +++ b/matcher/testdata/gfwlist.txt @@ -0,0 +1,3068 @@ +W0F1dG9Qcm94eSAwLjIuOV0KISBDaGVja3N1bTogenV5TzZWR015UWZEQSt3dHBP +NXR6dwohIEV4cGlyZXM6IDZoCiEgVGl0bGU6IEdGV0xpc3Q0TEwKISBHRldMaXN0 +IHdpdGggRVZFUllUSElORyBpbmNsdWRlZAohIExhc3QgTW9kaWZpZWQ6IE1vbiwg +MTQgTm92IDIwMjIgMDA6MjA6NDQgLTA1MDAKIQohIEhvbWVQYWdlOiBodHRwczov +L2dpdGh1Yi5jb20vZ2Z3bGlzdC9nZndsaXN0CiEgTGljZW5zZTogaHR0cHM6Ly93 +d3cuZ251Lm9yZy9saWNlbnNlcy9vbGQtbGljZW5zZXMvbGdwbC0yLjEudHh0CiEK +ISBHRldMaXN0IGlzIHVubGlrZWx5IHRvIGZ1bGx5IGNvbXByaXNlIHRoZSByZWFs +CiEgcnVsZXMgYmVpbmcgZGVwbG95ZWQgaW5zaWRlIEdGVyBzeXN0ZW0uIFdlIHRy +eQohIG91ciBiZXN0IHRvIGtlZXAgdGhlIGxpc3QgdXAgdG8gZGF0ZS4gUGxlYXNl +CiEgY29udGFjdCB1cyByZWdhcmRpbmcgVVJMIHN1Ym1pc3Npb24gLyByZW1vdmFs +LAohIG9yIHN1Z2dlc3Rpb24gLyBlbmhhbmNlbWVudCBhdCBpc3N1ZSB0cmFja2Vy +OgohIGh0dHBzOi8vZ2l0aHViLmNvbS9nZndsaXN0L2dmd2xpc3QvaXNzdWVzLy4K +CiEtLS0tLS0tLS00MDMvNDUxLzUwMy81MjAgJiBVUkwgUmVkaXJlY3RzLS0tLS0t +LS0tCiEtLWVoZW50YWkKfGh0dHA6Ly84NS4xNy43My4zMS8KIS0tfHxhZG9yYW1h +LmNvbQp8fGFmcmVlY2F0di5jb20KfHxhZ25lc2IuZnIKfHxha2liYS13ZWIuY29t +Cnx8YWx0cmVjLmNvbQp8fGFuZ2VsYS1tZXJrZWwuZGUKfHxhbmdvbGEub3JnCnx8 +YXBhcnRtZW50cmF0aW5ncy5jb20KfHxhcGFydG1lbnRzLmNvbQp8fGFyZW5hLnRh +aXBlaQp8fGFzaWFuc3Bpc3MuY29tCnx8YXNzaW1wLm9yZwp8fGF0aGVuYWVpem91 +LmNvbQp8fGF6dWJ1LnR2Cnx8YmFua21vYmlsZXZpYmUuY29tCnx8YmFub3J0ZS5j +b20KfHxiZWVnLmNvbQp8fGdsb2JhbC5iaW5nLmNvbQp8fGJvb2t0b3BpYS5jb20u +YXUKfHxib3lzbWFzdGVyLmNvbQp8fGJ5bmV0LmNvLmlsCnx8YnlydXQub3JnCnx8 +Y2FyZmF4LmNvbQouY2FzaW5vYmVsbGluaS5jb20KfHxjYXNpbm9iZWxsaW5pLmNv +bQp8fGNlbnRhdXJvLmNvbS5icgp8fGNob2JpdC5jYwp8fGNsZWFyc3VyYW5jZS5j +b20KfHxpbWFnZXMuY29taWNvLnR3Cnx8c3RhdGljLmNvbWljby50dwp8fGNvdW50 +ZXIuc29jaWFsCnx8Y29zdGNvLmNvbQp8fGNyb3NzZmlyZS5jby5rcgp8fGNydW5j +aHlyb2xsLmNvbQp8fGQycGFzcy5jb20KfHxkYXJwYS5taWwKfHxkYXdhbmdpZGMu +Y29tCnx8ZGVlemVyLmNvbQp8fGRlc2lwcm8uZGUKfHxkaW5nY2hpbi5jb20udHcK +fHxkaXNjb3JkLmNvbQp8fGRpc2NvcmQuZ2cKfHxkaXNjb3JkYXBwLmNvbQp8fGRp +c2NvcmRhcHAubmV0Cnx8ZGlzaC5jb20KfGh0dHA6Ly9pbWcuZGxzaXRlLmpwLwp8 +fGRtNTMwLm5ldApzaGFyZS5kbWh5Lm9yZwp8fGRtaHkub3JnCnx8ZG1tLmNvLmpw +CnxodHRwOi8vd3d3LmRtbS5jb20vbmV0Z2FtZQp8fGRudm9kLnR2Cnx8ZHVib3gu +Y29tCnx8ZHZkcGFjLmNvbQp8fGVlc3RpLmVlCnx8ZXN1cmFuY2UuY29tCi5leHBl +a3QuY29tCnx8ZXhwZWt0LmNvbQouZXh0bWF0cml4LmNvbQp8fGV4dG1hdHJpeC5j +b20KfHxmYWtrdS5uZXQKfHxmYXN0cGljLnJ1Cnx8ZmlsZXNvci5jb20KfHxmaW5h +bmNldHdpdHRlci5jb20KfHxmbGlwYm9hcmQuY29tCnx8ZmxpdHRvLmNvbQp8fGZu +YWMuYmUKfHxmbmFjLmNvbQp8fGZ1bmt5aW1nLmNvbQp8fGZ4bmV0d29ya3MuY29t +Cnx8Zy1hcmVhLm9yZwp8fGdldHR5aW1hZ2VzLmNvbQp8fGdldHVwbG9hZGVyLmNv +bQp8fGdoaWRyYS1zcmUub3JnCiEtLXxodHRwczovL2dpdGh1Yi5jb20vcHJvZ3Jh +bXRoaW5rL3poYW8KfGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9w +cm9ncmFtdGhpbmsvemhhbwp8fGdsYXNzOC5ldQp8fGdseXBlLmNvbQp8fGdvMTQx +LmNvbQp8fGd1by5tZWRpYQp8fGhhdXRlbG9vay5jb20KfHxoYXV0ZWxvb2tjZG4u +Y29tCnx8d2Vnby5oZXJlLmNvbQp8fGdhbWVyLWNkcy5jZG4uaGluZXQubmV0Cnx8 +Z2FtZXIyLWNkcy5jZG4uaGluZXQubmV0Cnx8aG1vZWdpcmwuY29tCnx8aG12ZGln +aXRhbC5jYQp8fGhtdmRpZ2l0YWwuY29tCnx8aG9tZWRlcG90LmNvbQp8fGhvb3Zl +cnMuY29tCnx8aHVsdS5jb20KfHxodWx1aW0uY29tCnxodHRwOi8vc2VjdXJlLmh1 +c3RsZXIuY29tCnxodHRwOi8vaHVzdGxlcmNhc2guY29tCnxodHRwOi8vd3d3Lmh1 +c3RsZXJjYXNoLmNvbQp8fGh5YnJpZC1hbmFseXNpcy5jb20KfHxjZG4qLmktc2Nt +cC5jb20KfHxpbGJlLmNvbQp8fGlsb3ZlbG9uZ3RvZXMuY29tCnxodHRwOi8vaW1n +bWVnYS5jb20vKi5naWYuaHRtbAp8aHR0cDovL2ltZ21lZ2EuY29tLyouanBnLmh0 +bWwKfGh0dHA6Ly9pbWdtZWdhLmNvbS8qLmpwZWcuaHRtbAp8aHR0cDovL2ltZ21l +Z2EuY29tLyoucG5nLmh0bWwKfHxpbWxpdmUuY29tCnx8dHcuaXFpeWkuY29tCnx8 +amF2aHViLm5ldAp8fGphdmh1Z2UuY29tCi5qYXZsaWJyYXJ5LmNvbQp8fGphdmxp +YnJhcnkuY29tCnx8amNwZW5uZXkuY29tCnx8amltcy5uZXQKfHx0di5qdGJjLmpv +aW5zLmNvbQp8fGp1a3Vqby1jbHViLmNvbQp8fGp1bGllcG9zdC5jb20KfHxrYXdh +aWlrYXdhaWkuanAKfHxrZW5kYXRpcmUuY29tCnx8a2hhdHJpbWF6YS5vcmcKfHxr +a2JveC5jb20KfHxsZWlzdXJlcHJvLmNvbQp8fGxpZmVtaWxlcy5jb20KfHxsb25n +dG9lcy5jb20KfHxsb3ZldHZzaG93LmNvbQp8aHR0cDovL3d3dy5tLXNwb3J0LmNv +LnVrCnx8bWFjZ2FtZXN0b3JlLmNvbQp8fG1hZG9ubmEtYXYuY29tCnx8bWFuZGlh +bnQuY29tCnx8bWFuZ2Fmb3guY29tCnx8bWFuZ2Fmb3gubWUKfHxtYW50YS5jb20K +fHxtYXRvbWUtcGx1cy5jb20KfHxtYXRvbWUtcGx1cy5uZXQKfHxtYXR0d2lsY294 +Lm5ldAp8fG1ldGFydGh1bnRlci5jb20KfHxtZnhtZWRpYS5jb20KfHxtb2ppbS5j +b20KfHxrYi5tb25pdG9yd2FyZS5jb20KfHxtb25zdGVyLmNvbQp8fG1vb2R5ei5j +b20KfHxtb29uYmluZ28uY29tCnx8bW9zLnJ1Cnx8bXNoYS5nb3YKfHxtdXp1LnR2 +Cnx8bXZnLmpwCi5teWJldC5jb20KfHxteWJldC5jb20KfHxteXBpa3Bhay5jb20K +fHxuYXRpb253aWRlLmNvbQp8aHR0cDovL3d3dy5uYmMuY29tL2xpdmUKfHxuZW8t +bWlyYWNsZS5jb20KfHxuZXRmbGl4LmNvbQp8fG5ldGZsaXgubmV0Cnx8bmZseGlt +Zy5jb20KfHxuZmx4aW1nLm5ldAp8fG5mbHhleHQuY29tCnx8bmZseHNvLm5ldAp8 +fG5mbHh2aWRlby5uZXQKfHxuaWMuZ292CnxodHRwOi8vbW8ubmlnaHRsaWZlMTQx +LmNvbQp8fHB1cnBvc2UubmlrZS5jb20KfHxub3hpbmZsdWVuY2VyLmNvbQpAQHx8 +Y24ubm94aW5mbHVlbmNlci5jb20KfHxub3Jkc3Ryb20uY29tCnx8bm9yZHN0cm9t +aW1hZ2UuY29tCnx8bm9yZHN0cm9tcmFjay5jb20KfHxub3R0aW5naGFtcG9zdC5j +b20KfHxucHNib29zdC5jb20KfHxudGR0di5jegp8fHMxLm51ZGV6ei5jb20KfHxu +dXNhdHJpcC5jb20KfHxudXV2ZW0uY29tCnx8b21uaTcuanAKfHxvbmFwcC5jb20K +IS0tV2UgYXJlIGNvbmZ1c2VkIGFzIHdlbGwKfHxvbnRyYWMuY29tCkBAfGh0dHA6 +Ly9ibG9nLm9udHJhYy5jb20KfHxwYW5kb3JhLmNvbQoucGFuZG9yYS50dgp8fHBh +cmthbnNreS5jb20KfHxwaG1zb2NpZXR5Lm9yZwp8aHR0cDovLyoucGltZy50dy8K +fHxwb2RjYXN0LmNvCnx8cHVyZTE4LmNvbQp8fHB5dG9yY2gub3JnCnx8cXEuY28u +emEKfHxyMTguY29tCnxodHRwOi8vcmFkaWtvLmpwCnx8cmFtY2l0eS5jb20uYXUK +fHxyYXRleW91cm11c2ljLmNvbQp8fHJkLmNvbQp8fHJkaW8uY29tCnxodHRwczov +L3Jpc2V1cC5uZXQKfHxzYWRpc3RpYy12LmNvbQp8fGlzYy5zYW5zLmVkdQp8aHR0 +cDovL2Nkbiouc2VhcmNoLnh4eC8KfHxzaGlrc2hhLmNvbQp8fHNsYWNrZXIuY29t +Cnx8c20tbWlyYWNsZS5jb20KfHxzb2Z0bm9sb2d5LmJpegp8fHNveWxlbnRuZXdz +Lm9yZwp8fHNwb3RpZnkuY29tCnx8c3ByZWFkc2hpcnQuZXMKfHxzcHJpbmdib2Fy +ZHBsYXRmb3JtLmNvbQp8fHNwcml0ZS5vcmcKQEB8aHR0cDovL3N0b3JlLnNwcml0 +ZS5vcmcKfHxzdXBlcm9rYXlhbWEuY29tCnx8c3VwZXJwYWdlcy5jb20KfHxzd2Fn +YnVja3MuY29tCnx8c3dpdGNoMS5qcAp8fHRhcGFud2FwLmNvbQp8fGdzcC50YXJn +ZXQuY29tCnx8bG9naW4udGFyZ2V0LmNvbQohLS1AQHx8aW50bC50YXJnZXQuY29t +Cnx8cmNhbS50YXJnZXQuY29tCnx8dGVjaG5ld3MudHcKfHx0ZXJhYm94LmNvbQp8 +fHRoaW5rZ2Vlay5jb20KfHx0aGVib2R5c2hvcC11c2EuY29tCnx8dGlrdG9rLmNv +bQp8fHRtYS5jby5qcAp8fHRyYWNmb25lLmNvbQp8fHRyeWhlYXJ0LmpwCnx8dHVy +bnRhYmxlLmZtCnx8dHdlcmtpbmdidXR0LmNvbQp8fHVsb3AubmV0Cnx8dXVrYW5z +aHUuY29tCnx8dmVnYXNyZWQuY29tCnx8dmV2by5jb20KfHx2aXAtZW50ZXJwcmlz +ZS5jb20KfGh0dHA6Ly92aXUudHYvY2gvCnxodHRwOi8vdml1LnR2L2VuY29yZS8K +fHx2bXBzb2Z0LmNvbQp8aHR0cDovL2Vjc20udnMuY29tLwp8fHdhbnotZmFjdG9y +eS5jb20KfHxzc2wud2VicGFjay5kZQp8fHdoZXJldG93YXRjaC5jb20KfHx3aW5n +YW1lc3RvcmUuY29tCnx8d2l6Y3JhZnRzLm5ldAp8fHdvd2hlYWQuY29tCnx8dm9k +Lnd3ZS5jb20KfHx4ZmluaXR5LmNvbQp8fHlvdXdpbi5jb20KfHx5dG4uY28ua3IK +fHx6YXR0b28uY29tCnx8emltLnZuCnx8em96b3Rvd24uY29tCgohIyMjIyMjIyMj +IyMjIyNHZW5lcmFsIExpc3QgU3RhcnQjIyMjIyMjIyMjIyMjIyMKIS0tLS0tLS0t +LS0tLS0tLS0tLS1QdXJlIElQLS0tLS0tLS0tLS0tLS0tLS0tLS0tCjE0LjEwMi4y +NTAuMTgKMTQuMTAyLjI1MC4xOQo1MC43LjMxLjIzMDo4ODk4CjE3NC4xNDIuMTA1 +LjE1Mwo2OS42NS4xOS4xNjAKCiEtLS0tLS0tLS0tLS0tLS0tLS0tLS0tSUROLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLQp8fHhuLS00Z3ExNzFwLmNvbQp8fHhuLS1jenE3 +NXB2djFhajVjLm9yZwp8fHhuLS1pMnJ1OHEycWcuY29tCnx8eG4tLW9pcS5jYwp8 +fHhuLS1wOGo5YTBkOWM5YS54bi0tcTlqeWI0Ywp8fHhuLS05cHI2MnIyNGEuY29t +CgohLS0tLS0tLS0tLS0tLS0tLS1ETlMgUG9pc29uaW5nLS0tLS0tLS0tLS0tLS0t +LS0KIS0tLUFtYXpvbi0tLQohLXx8Y2RuLWltYWdlcy5tYWlsY2hpbXAuY29tCnx8 +YWJlYm9va3MuY29tCnxodHRwczovLyouczMuYW1hem9uYXdzLmNvbQp8fHMzLWFw +LXNvdXRoZWFzdC0yLmFtYXpvbmF3cy5jb20KCnx8NDMxMTAuY2YKfHw5Y2FjaGUu +Y29tCnx8OWdhZy5jb20KfHxhZ3JvLmhrCnx8c2hhcmUuYW1lcmljYS5nb3YKfHxh +cGttaXJyb3IuY29tCnx8YXJ0ZS50dgp8fGFydHN0YXRpb24uY29tCnx8YmFuZ2Ry +ZWFtLnNwYWNlCnx8YmVoYW5jZS5uZXQKfHxiaXJkLnNvCnx8Yml0dGVyd2ludGVy +Lm9yZwp8fGJubi5jbwp8fGJ1c2luZXNzaW5zaWRlci5jb20KfHxib29tc3NyLmNv +bQp8fGJ3Z3lody5jb20KfHxjYXN0Ym94LmZtCnx8Y2hpbmF0aW1lcy5jb20KfHxj +bHlwLml0Cnx8Y21jbi5vcmcKfHxjbXguaW0KfHxkYWlseXZpZXcudHcKfHxkYXVt +Lm5ldAp8fGRlcG9zaXRwaG90b3MuY29tCnx8ZGlzY29ubmVjdC5tZQp8fGRvY3Vt +ZW50aW5ncmVhbGl0eS5jb20KfHxkb3ViaWJhY2t1cC5jb20KfHxkb3VibWlycm9y +LmNmCnx8ZW5jeWNsb3BlZGlhLmNvbQp8fGZhbmdlcWlhbmcuY29tCnx8ZmFucWlh +bmdkYW5nLmNvbQp8fGZlZWRseS5jb20KfHxmZWVkeC5uZXQKfHxmbHl6eTIwMDUu +Y29tCnx8Zm9yZWlnbnBvbGljeS5jb20KfHxmcmVlLXNzLnNpdGUKfHxmcmVlaG9u +Z2tvbmcub3JnCnx8YmxvZy5mdWNrZ2Z3MjMzLm9yZwp8fGcwdi5zb2NpYWwKfHxn +bG9iYWx2b2ljZXMub3JnCnx8Z2xvcnlzdGFyLm1lCnx8Z29yZWdyaXNoLmNvbQp8 +fGd1YW5nbmlhbnZwbi5jb20KfHxoYW5pbWUudHYKfHxoYm8uY29tCnx8c3BhY2Vz +LmhpZ2h0YWlsLmNvbQp8fGhrZ2FsZGVuLmNvbQp8fGhrZ29sZGVuLmNvbQp8fGh1 +ZHNvbi5vcmcKfHxpcGZzLmlvCnx8amFwYW50aW1lcy5jby5qcAp8fGppamkuY29t +Cnx8amludGlhbi5uZXQKfHxqaW54LmNvbQp8fGpvaW5tYXN0b2Rvbi5vcmcKfHxs +aWFuZ3poaWNodWFubWVpLmNvbQp8fGxpZ2h0aS5tZQp8fGxpZ2h0eWVhcnZwbi5j +b20KfHxsaWhrZy5jb20KfHxsaW5lLXNjZG4ubmV0Cnx8aS5saXRoaXVtLmNvbQp8 +fGNsb3VkLm1haWwucnUKfHxjZG4taW1hZ2VzLm1haWxjaGltcC5jb20KfHxtYXN0 +b2Rvbi5jbG91ZAp8fG1hc3RvZG9uLmhvc3QKfHxtYXN0b2Rvbi5zb2NpYWwKfHxt +YXN0b2Rvbi54eXoKfHxtYXR0ZXJzLm5ld3MKfHxtZS5tZQp8fG1ldGFydC5jb20K +fHxtb2h1LmNsdWIKfHxtb2h1Lm1sCnx8bW90aXl1bi5jb20KfHxtc2EtaXQub3Jn +Cnx8Z29vLm5lLmpwCnx8Z28ubmVzbm9kZS5jb20KfHxpbnRlcm5hdGlvbmFsLW5l +d3MubmV3c21hZ2F6aW5lLmFzaWEKfHxuaWtrZWkuY29tCnx8bml0dGVyLmNjCnx8 +bml0dGVyLm5ldAp8fG5pdS5tb2UKfHxub2ZpbGUuaW8KfHxub3cuY29tCnx8b3Bl +bnZwbi5vcmcKfHxvbmVqYXYuY29tCnx8cGFzdGUuZWUKfHxteS5wY2xvdWQuY29t +Cnx8cGljYWNvbWljLmNvbQp8fHBpbmNvbmcucm9ja3MKfHxwaXhpdi5uZXQKfHxw +b3RhdG8uaW0KfHxwcmVtcHJveHkuY29tCnx8cHJpc20tYnJlYWsub3JnCnx8cHJv +dG9udnBuLmNvbQp8fGFwaS5wdXJlYXBrLmNvbQp8fHF1b3JhLmNvbQp8fHF1b3Jh +Y2RuLm5ldAp8fHF6LmNvbQp8fGNkbi5zZWF0Z3VydS5jb20KfHxzZWN1cmUucmF4 +Y2RuLmNvbQp8fHJlZGQuaXQKfHxyZWRkaXQuY29tCi5yZWRkaXRsaXN0LmNvbQp8 +aHR0cDovL3JlZGRpdGxpc3QuY29tCnx8cmVkZGl0bWVkaWEuY29tCnx8cmVkZGl0 +c3RhdGljLmNvbQohLS1kZWZ1bmN0Cnx8cml4Y2xvdWQuY29tCnx8cml4Y2xvdWQu +dXMKfHxyc2RsbW9uaXRvci5jb20KfHxzaGFkb3dzb2Nrcy5iZQp8fHNoYWRvd3Nv +Y2tzOS5jb20KfHx0bjEuc2hlbWFsZXouY29tCnx8dG4yLnNoZW1hbGV6LmNvbQp8 +fHRuMy5zaGVtYWxlei5jb20KfHxzdGF0aWMuc2hlbWFsZXouY29tCnx8c2l4LWRl +Z3JlZXMuaW8KfHxzb2Z0ZmFtb3VzLmNvbQp8fHNvZnRzbWlycm9yLmNmCnx8c29z +cmVhZGVyLmNvbQp8fHNzcGFuZWwubmV0Cnx8c3VsaWFuLm1lCnx8c3VwY2hpbmEu +Y29tCnx8dGVkZHlzdW4uY29tCnx8dGV4dG5vdy5tZQp8fHRpbmV5ZS5jb20KfHx0 +b3AxMHZwbi5jb20KfHx0dWJlcG9ybmNsYXNzaWMuY29tCnx8dWt1LmltCnx8dW5z +ZWVuLmlzCnx8Y24udXB0b2Rvd24uY29tCnx8dXJhYmFuLm1lCnx8dnJzbWFzaC5j +b20KfHx2dWx0cnlody5jb20KfHxzY2FjaGUudnp3LmNvbQp8fHNjYWNoZTEudnp3 +LmNvbQp8fHNjYWNoZTIudnp3LmNvbQp8fHNzNy52encuY29tCnx8c3NyLnRvb2xz +Cnx8c3RlZW1pdC5jb20KfHx0YWl3YW5qdXN0aWNlLm5ldAp8fHRpbmMtdnBuLm9y +Zwp8fHUxNS5pbmZvCnx8d2FzaGluZ3RvbnBvc3QuY29tCnx8d2Vuemhhby5jYQp8 +fHdoYXRzb253ZWliby5jb20KfHx3aXJlLmNvbQp8fGJsb2cud29ya2Zsb3cuaXMK +fHx4bS5jb20KfHx4dWVodWEudXMKfHx5ZXMtbmV3cy5jb20KfHx5aWdlbmkuY29t +Cnx8eW91LWdldC5vcmcKfHx6emNsb3VkLm1lCgohLS0tRGlnaXRhbCBDdXJyZW5j +eSBFeGNoYW5nZShDUllQVE8pLS0tCnx8YWV4LmNvbQp8fGFsbGNvaW4uY29tCnx8 +YWRjZXguY29tCnx8YmNleC5jYQp8fGJpYm94LmNvbQp8fGJpZy5vbmUKfHxiaWdv +bmUuY29tCnx8YmluYW5jZS5jb20KfHxiaXQtei5jb20KfHxiaXR6LmFpCnx8Yml0 +YmF5Lm5ldAp8fGJpdGNvaW53b3JsZC5jb20KfHxiaXRmaW5leC5jb20KfHxiaXRo +dW1iLmNvbQp8fGJpdGlua2EuY29tLmFyCnx8Yml0bWV4LmNvbQp8fGJ0Yzk4LmNv +bQp8fGJ0Y2JhbmsuYmFuawp8fGJ0Y3RyYWRlLmltCnx8YnliaXQuY29tCnx8YzJj +eC5jb20KfHxjaGFvZXguY29tCnx8Y29iaW5ob29kLmNvbQp8fGNvaW4yY28uaW4K +fHxjb2luYmVuZS5jb20KLmNvaW5lZ2cuY29tCnx8Y29pbmVnZy5jb20KfHxjb2lu +ZXguY29tCiEtLXxodHRwczovL3d3dy5jb2luZXhjaGFuZ2UuaW8vCnx8Y29pbmdl +Y2tvLmNvbQp8fGNvaW5naS5jb20KfHxjb2lubWFya2V0Y2FwLmNvbQp8fGNvaW5y +YWlsLmNvLmtyCnx8Y29pbnRpZ2VyLmNvbQp8fGNvaW50b2JlLmNvbQp8fGNvaW51 +dC5jb20KfHxkaXNjb2lucy5jb20KfHxkcmFnb25leC5pbwp8fGVidGNiYW5rLmNv +bQp8fGV0aGVyZGVsdGEuY29tCnx8ZXRoZXJzY2FuLmlvCnx8ZXhtby5jb20KfHxl +eHJhdGVzLm1lCnx8ZXh4LmNvbQp8fGZhdGJ0Yy5jb20KfHxmdHguY29tCnx8Z2F0 +ZS5pbwp8fGdhdGVjb2luLmNvbQp8fGhiZy5jb20KfHxoaXRidGMuY29tCnx8aHVv +YmkuY28KfHxodW9iaS5jb20KfHxodW9iaS5tZQohLS18fGh1b2JpLmxpCnx8aHVv +YmkucHJvCnx8aHVvYmkuc2MKfHxodW9iaXByby5jb20KfHxieC5pbi50aAp8fGpl +eC5jb20KfHxrZXguY29tCnx8a3NwY29pbi5jb20KfHxrdWNvaW4uY29tCnx8bGJh +bmsuaW5mbwp8fGxpcXVpZGl0eXRwLmNvbQp8fGxpdmVjb2luLm5ldAp8fGxvY2Fs +Yml0Y29pbnMuY29tCnx8bWVyY2F0b3guY29tCnx8b2FuZGEuY29tCnx8b2V4LmNv +bQp8fG9rZXguY29tCnx8b2t4LmNvbQp8fG9wZW5zZWEuaW8KfHxvdGNidGMuY29t +Cnx8cGF4ZnVsLmNvbQp8fHBvb2xpbi5jb20KfHxyaWdodGJ0Yy5jb20KfHxzb2x2 +LmZpbmFuY2UKfHx0b3BidGMuY29tCnx8dHJvbnNjYW4ub3JnCnx8eGJ0Y2UuY29t +Cnx8eW9iaXQubmV0Cnx8emIuY29tCgohLS0tLS0tLS0tLS0tLS0tLUZyYXVkcyAm +IFNjYW1zLS0tLS0tLS0tLS0tLS0tLS0KISEtLS1Db250ZW50IEZhcm0oZmFrZSA1 +MDAgZXJyb3IpLS0tCnx8cmVhZDAxLmNvbQp8fGtrbmV3cy5jYwoKY2hpbmEtbW1t +LmpwLm5ldAoubHN4c3p6Zy5jb20KLmNoaW5hLW1tbS5uZXQKfHxjaGluYS1tbW0u +bmV0CmNoaW5hLW1tbS5zYS5jb20KCiEtLS0tLS0tLS0tLS0tLS0tLS0tLS1Hcm91 +cHMtLS0tLS0tLS0tLS0tLS0tLS0tLQohIS0tLUFmcmFpZCBGcmVlRE5TLS0tCi5h +bGxvd2VkLm9yZwoubm93LmltCgohIS0tLUFtYXpvbi0tLQp8fGFtYXpvbi5jby5q +cAouYW1hem9uLmNvbS9EYWxhaS1MYW1hCmFtYXpvbi5jb20vUHJpc29uZXItU3Rh +dGUtU2VjcmV0LUpvdXJuYWwtUHJlbWllcgpzMy1hcC1ub3J0aGVhc3QtMS5hbWF6 +b25hd3MuY29tCgohIS0tLUFPTC0tLQp8fGFvbGNoYW5uZWxzLmFvbC5jb20Kdmlk +ZW8uYW9sLmNhL3ZpZGVvLWRldGFpbAp2aWRlby5hb2wuY28udWsvdmlkZW8tZGV0 +YWlsCnZpZGVvLmFvbC5jb20KfHx2aWRlby5hb2wuY29tCnx8c2VhcmNoLmFvbC5j +b20Kd3d3LmFvbG5ld3MuY29tCgohIS0tLUF2TW9vLS0tCi5hdm1vLnB3CiEtLXxo +dHRwOi8vYXZtby5wdwouYXZtb28uY29tCnxodHRwOi8vYXZtb28uY29tCi5hdm1v +by5uZXQKfGh0dHA6Ly9hdm1vby5uZXQKfHxhdm1vby5wdwouamF2bW9vLnh5egp8 +aHR0cDovL2phdm1vby54eXoKLmphdnRhZy5jb20KfGh0dHA6Ly9qYXZ0YWcuY29t +Ci5qYXZ6b28uY29tCnxodHRwOi8vamF2em9vLmNvbQoudGVsbG1lLnB3CgohIS0t +LUJCQy0tLQohLS0uYmJjLmNvLnVrL2Jsb2dzCiEtLS5iYmMuY28udWsvY2hpbmVz +ZQohLS0uYmJjLmNvLnVrL25ld3Mvd29ybGQtYXNpYS1jaGluYQohLS0uYmJjLmNv +LnVrL3R2CiEtLS5iYmMuY28udWsvemhvbmd3ZW4KIS0tLmJiYy5jb20vdWtjaGlu +YQohLS0uYmJjLmNvbS96aG9uZ3dlbgohLS0uYmJjLmNvbSUyRnpob25nd2VuCiEt +LW5ld3MuYmJjLmNvLnVrL29udGhpc2RheSpuZXdzaWRfMjQ5NjAwMC8yNDk2Mjc3 +CiEtLW5ld3Nmb3J1bXMuYmJjLmNvLnVrCi5iYmMuY29tCnx8YmJjLmNvbQouYmJj +LmNvLnVrCnx8YmJjLmNvLnVrCnx8YmJjaS5jby51awouYmJjY2hpbmVzZS5jb20K +fHxiYmNjaGluZXNlLmNvbQp8aHR0cDovL2JiYy5pbgoKISEtLS1CbG9vbWJlcmct +LS0KLmJsb29tYmVyZy5jbgp8fGJsb29tYmVyZy5jbgouYmxvb21iZXJnLmNvbQp8 +fGJsb29tYmVyZy5jb20KYmxvb21iZXJnLmRlCnx8Ymxvb21iZXJnLmRlCnx8Ymxv +b21iZXJndmlldy5jb20KLmJ1c2luZXNzd2Vlay5jb20KCiEhLS0tQ2hhbmdlSVAt +LS0KLjFkdW1iLmNvbQouMjV1LmNvbQouMndha3kuY29tCi4zLWEubmV0Ci40ZHEu +Y29tCi40bXlkb21haW4uY29tCi40cHUuY29tCi5hY21ldG95LmNvbQouYWxtb3N0 +bXkuY29tCi5hbWVyaWNhbnVuZmluaXNoZWQuY29tCi5hdXRob3JpemVkZG5zLm5l +dAouYXV0aG9yaXplZGRucy5vcmcKLmF1dGhvcml6ZWRkbnMudXMKLmJpZ21vbmV5 +LmJpegouY2hhbmdlaXAubmFtZQouY2hhbmdlaXAubmV0Ci5jaGFuZ2VpcC5vcmcK +LmNsZWFuc2l0ZS5iaXoKLmNsZWFuc2l0ZS5pbmZvCi5jbGVhbnNpdGUudXMKLmNv +bXByZXNzLnRvCi5kZG5zLmluZm8KLmRkbnMubWUudWsKLmRkbnMubW9iaQouZGRu +cy5tcwouZGRucy5uYW1lCi5kZG5zLnVzCi5kaGNwLmJpegouZG5zLWRucy5jb20K +LmRucy1zdHVmZi5jb20KLmRuczA0LmNvbQouZG5zMDUuY29tCi5kbnMxLnVzCi5k +bnMyLnVzCi5kbnNldC5jb20KLmRuc3JkLmNvbQouZHNtdHAuY29tCi5kdW1iMS5j +b20KLmR5bmFtaWMtZG5zLm5ldAouZHluYW1pY2Rucy5iaXoKLmR5bmFtaWNkbnMu +Y28udWsKLmR5bmFtaWNkbnMubWUudWsKLmR5bmFtaWNkbnMub3JnLnVrCi5keW5k +bnMucHJvCi5keW5zc2wuY29tCi5lZG5zLmJpegouZXBhYy50bwouZXNtdHAuYml6 +Ci5lenVhLmNvbQouZmFxc2Vydi5jb20KLmZhcnRpdC5jb20KLmZyZWVkZG5zLmNv +bQouZnJlZXRjcC5jb20KLmZyZWV3d3cuYml6Ci5mcmVld3d3LmluZm8KLmZ0cDEu +Yml6Ci5mdHBzZXJ2ZXIuYml6Ci5nZXR0cmlhbHMuY29tCi5nb3QtZ2FtZS5vcmcK +LmdyOGRvbWFpbi5iaXoKLmdyOG5hbWUuYml6Ci5odHRwczQ0My5uZXQKLmh0dHBz +NDQzLm9yZwouaWt3Yi5jb20KLmluc3RhbnRocS5jb20KLmlvd255b3VyLmJpegou +aW93bnlvdXIub3JnCi5pc2FzZWNyZXQuY29tCi5pdGVtZGIuY29tCi5pdHNhb2wu +Y29tCi5qZXRvcy5jb20KLmprdWIuY29tCi5qdW5nbGVoZWFydC5jb20KLmp1c3Rk +aWVkLmNvbQoubGZsaW5rLmNvbQoubGZsaW5rdXAuY29tCi5sZmxpbmt1cC5uZXQK +LmxmbGlua3VwLm9yZwoubG9uZ211c2ljLmNvbQoubWVmb3VuZC5jb20KLm1vbmV5 +aG9tZS5iaXoKLm1yYmFzaWMuY29tCi5tcmJvbnVzLmNvbQoubXJmYWNlLmNvbQou +bXJzbG92ZS5jb20KLm15MDMuY29tCi5teWRhZC5pbmZvCi5teWRkbnMuY29tCi5t +eWZ0cC5pbmZvCi5teWZ0cC5uYW1lCi5teWxmdHYuY29tCi5teW1vbS5pbmZvCi5t +eW5ldGF2Lm5ldAoubXluZXRhdi5vcmcKLm15bnVtYmVyLm9yZwoubXlwaWN0dXJl +LmluZm8KLm15cG9wMy5uZXQKLm15cG9wMy5vcmcKLm15c2Vjb25kYXJ5ZG5zLmNv +bQoubXl3d3cuYml6Ci5teXouaW5mbwoubmludGguYml6Ci5uczAxLmJpegoubnMw +MS5pbmZvCi5uczAxLnVzCi5uczAyLmJpegoubnMwMi5pbmZvCi5uczAyLnVzCi5u +czEubmFtZQoubnMyLm5hbWUKLm5zMy5uYW1lCi5vY3J5LmNvbQoub25lZHVtYi5j +b20KLm9ubXlwYy5iaXoKLm9ubXlwYy5pbmZvCi5vbm15cGMubmV0Ci5vbm15cGMu +b3JnCi5vbm15cGMudXMKLm9yZ2FuaWNjcmFwLmNvbQoub3R6by5jb20KLm91cmhv +YmJ5LmNvbQoucGNhbnl3aGVyZS5uZXQKLnBvcnQyNS5iaXoKLnByb3h5ZG5zLmNv +bQoucWhpZ2guY29tCi5xcG9lLmNvbQoucmViYXRlc3J1bGUubmV0Ci5zZWxsY2xh +c3NpY3MuY29tCi5zZW5kc210cC5jb20KLnNlcnZldXNlci5jb20KLnNlcnZldXNl +cnMuY29tCi5zZXhpZHVkZS5jb20KLnNleHh4eS5iaXoKLnNpeHRoLmJpegouc3F1 +aXJseS5pbmZvCi5zc2w0NDMub3JnCi50b2guaW5mbwoudG95dGhpZXZlcy5jb20K +LnRyaWNraXAubmV0Ci50cmlja2lwLm9yZwoudml6dmF6LmNvbQoud2hhLmxhCi53 +aWthYmEuY29tCi53d3cxLmJpegoud3d3aG9zdC5iaXoKQEB8aHR0cDovL3h4Lnd3 +d2hvc3QuYml6Ci54MjRoci5jb20KLnh4dXouY29tCi54eHh5LmJpegoueHh4eS5p +bmZvCi55Z3RvLmNvbQoueW91ZG9udGNhcmUuY29tCi55b3VydHJhcC5jb20KLnp5 +bnMuY29tCi56enV4LmNvbQoKISEtLUNsb3VkZmxhcmUtLQp8fHBhZ2VzLmRldgoK +ISEtLS1DbG91ZEZyb250LS0tCmQxYjE4M3NnMG52bnVoLmNsb3VkZnJvbnQubmV0 +CnxodHRwczovL2QxYjE4M3NnMG52bnVoLmNsb3VkZnJvbnQubmV0CmQxYzM3Z2p3 +YTI2dGFhLmNsb3VkZnJvbnQubmV0CnxodHRwczovL2QxYzM3Z2p3YTI2dGFhLmNs +b3VkZnJvbnQubmV0CmQzYzMzaGNnaXdldjMuY2xvdWRmcm9udC5uZXQKfGh0dHBz +Oi8vZDNjMzNoY2dpd2V2My5jbG91ZGZyb250Lm5ldAp8fGQzcmhyN2tnbXRycTF2 +LmNsb3VkZnJvbnQubmV0CgohIS0tLUR0RE5TLS0tCiEjIyNodHRwczovL3d3dy5k +dGRucy5jb20vZHRzaXRlL2ZhcQouM2QtZ2FtZS5jb20KLjRpcmMuY29tCi5iMG5l +LmNvbQouY2hhdG5vb2suY29tCi5kYXJrdGVjaC5vcmcKLmRlYWZ0b25lLmNvbQou +ZHRkbnMubmV0Ci5lZmZlcnMuY29tCi5ldG93bnMubmV0Ci5ldG93bnMub3JnCi5m +bG5ldC5vcmcKLmdvdGdlZWtzLmNvbQouc2NpZXJvbi5jb20KLnNseWlwLmNvbQou +c2x5aXAubmV0Ci5zdXJvb3QuY29tCgohIS0tLUR5bkROUy0tLQohIyMjaHR0cHM6 +Ly9oZWxwLmR5bi5jb20vbGlzdC1vZi1keW4tZG5zLXByby1yZW1vdGUtYWNjZXNz +LWRvbWFpbi1uYW1lcy8KLmJsb2dkbnMub3JnCi5keW5kbnMub3JnCi5keW5kbnMt +aXAuY29tCi5keW5kbnMtcGljcy5jb20KLmZyb20tc2QuY29tCi5mcm9tLXByLmNv +bQouaXMtYS1odW50ZXIuY29tCgohIS0tLUR5bnUtLS0KLmR5bnUuY29tCnx8ZHlu +dS5jb20KLmR5bnUubmV0Ci5mcmVlZGRucy5vcmcKCiEhLS0tRmFjZWJvb2stLS0K +fHxhY2NvdW50a2l0LmNvbQpjZG5pbnN0YWdyYW0uY29tCnx8Y2RuaW5zdGFncmFt +LmNvbQp8fGY4LmNvbQp8fGZhY2Vib29rLmJyCi5mYWNlYm9vay5jb20KfHxmYWNl +Ym9vay5jb20KIS0tL15odHRwcz86XC9cL1teXC9dK2ZhY2Vib29rXC5jb20vCkBA +fHx2Ni5mYWNlYm9vay5jb20KfHxmYWNlYm9vay5kZXNpZ24KfHxjb25uZWN0LmZh +Y2Vib29rLm5ldAp8fGZhY2Vib29rLmh1Cnx8ZmFjZWJvb2suaW4KfHxmYWNlYm9v +ay5ubAp8fGZhY2Vib29rLnNlCnx8ZmFjZWJvb2ttYWlsLmNvbQp8fGZiLmNvbQp8 +fGZiLm1lCnx8ZmIud2F0Y2gKfHxmYmNkbi5uZXQKfHxmYnNieC5jb20KfHxmYmFk +ZGlucy5jb20KfHxmYndvcmttYWlsLmNvbQouaW5zdGFncmFtLmNvbQp8fGluc3Rh +Z3JhbS5jb20KfHxtLm1lCnx8bWVzc2VuZ2VyLmNvbQp8fG9jdWx1cy5jb20KfHxv +Y3VsdXNjZG4uY29tCnx8cm9ja3NkYi5vcmcKQEB8fGlwNi5zdGF0aWMuc2wtcmV2 +ZXJzZS5jb20KfHxwYXJzZS5jb20KfHx0aGVmYWNlYm9vay5jb20KfHx3aGF0c2Fw +cC5jb20KfHx3aGF0c2FwcC5uZXQKCiEhLS0tRmFuZG9tLS0tCnx8YXVudG9sb2d5 +LmZhbmRvbS5jb20KfHxob25na29uZy5mYW5kb20uY29tCgohIS0tLUZUQ2hpbmVz +ZS0tLQouZnRjaGluZXNlLmNvbQp8fGZ0Y2hpbmVzZS5jb20KIS0tLmZ0Y2hpbmVz +ZS5jb20vY2hhbm5lbC92aWRlbwohLS0uZnRjaGluZXNlLmNvbS9wcmVtaXVtLzAw +MTA4MTA2NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMjc1MwohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwMjY2MTYKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDI2NzQ5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAyNjgwNwoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMjY4MDgKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDI2ODM0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAy +Njg4MAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMjc0MjkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDMwMzQxCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTAzMDUwMgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzA4MDMKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDMxMzE3CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTAzMjYxNwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzI2 +MzYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDMyNjkyCiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTAzMjc2MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwMzMxMzgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM0OTE3CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzNDkyNgohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwMzQ5MjcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM0OTI4 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzNDk1MgohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwMzU4OTAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDM1OTcyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzNTk5MwohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwMzY0MTcKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDM3MDkwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzNzA5MQoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzgxNzgKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDM4MTk5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAz +ODIyMAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzg4MTkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDM4ODYyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTAzOTA2NwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzkxNzgKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM5MjExCiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTAzOTI3MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwMzky +OTUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM5MzY5CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTAzOTQ4MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwMzk1MzQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM5NTU1CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzOTU3NgohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwMzk3MTIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDM5Nzc5 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTAzOTgwOQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNDAxMzQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDQwODM1CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0MDg5MAohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNDA5MTgKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDQwOTkyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0MTIwOQoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDIxMDAKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDQyMjUyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0 +MjI3MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDIyODAKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDQzMDI5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA0MzA2NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDMwOTYKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQzMTI0CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA0MzE1MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDMx +ODkKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQzNDI4CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA0MzQzOQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNDM1MzQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQzNjc1CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0MzY4MAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNDM3MDIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQzODQ5 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NDA5OQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNDQ3NzYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDQ0ODcxCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NDg5NwohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNDUxMTQKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDQ1MTM5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NTE4Ngoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDU3NTUKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDQ2MDg3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0 +NjEwNQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDYxMTgKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDQ2MTMyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA0NjUxNwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDY4MjIKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQ2ODY2CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA0Njk0MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDcx +ODAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQ3MjA2CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA0NzMwNAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNDczMTcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQ3MzQ1CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NzM1OAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNDczNzUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDQ3Mzgx +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NzQxMwohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNDc0NTYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDQ3NDkxCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NzU0NQohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNDc1NTgKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDQ3NTY4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0NzYyNwoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDgyOTMKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDQ4MzQzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA0 +ODcxMAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNDkyODkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDQ5MzYwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA0OTg5NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTAxNTIKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDUxMDI3CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1MTE2MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTEz +NzIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDUxNDc5CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA1MjEzOAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNTIxNjEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDUyNTI1CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1MjU0OQohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNTI3MDEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDUyOTY1 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1MzE0OQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNTMxNTAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDUzMjAwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1MzQyNQohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNTM0OTYKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDUzNTI2CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1MzU1Nwoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTM5MDYKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDU0MDQ5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1 +NDEwMwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTQxMDkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDU0MTE5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA1NDEyMwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTQxMzkKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU0MTY2CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1NDE2OAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTQx +OTAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU0NDM3CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA1NDUyNgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNTQ2MDcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU0NjQ0CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NDc4NgohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNTQ4NDMKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU0OTI1 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NDk0MAohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNTUwNTEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDU1MDYzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NTA2OQohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNTUxMzYKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDU1MTcwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NTIwMgoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTUyNDIKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDU1MjYzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1 +NTI3NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTUyOTkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDU1NDgwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA1NTU1MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTU1NTkKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU1NTY2CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1NTg0MAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTYw +OTkKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU2MTA4CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA1NjEzMQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNTYzNzUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU2NDkxCiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NjUyOQohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNTY1MzQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU2NTM4 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NjU0MQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNTY1NTQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDU2NTU3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NjU2MAohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNTY1NjcKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDU2NTc0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NjU4OAoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTY1OTQKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDU2NTk2CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1 +NjY4NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTY4MzIKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDU2ODMzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA1Njg1MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTY4NzQKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU2ODk2CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1NjkyNwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTcw +MTEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU3MDE4CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA1NzA0NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNTcxNjIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU3NTAwCiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NzUwNAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNTc1MDkKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU3NTE4 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NzUzMgohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNTc1MzMKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDU3NTU2CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NzU4MAohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNTc2MzgKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDU3NjQ0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1NzgxNwoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTc4NzUKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDU4MDA5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1 +ODA1NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTgyMjQKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDU4MjU3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA1ODI5NQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTgzMjgKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU4MzM5CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1ODM0NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTgz +NTIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU4NDEzCiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA1ODQyMQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNTg0NDAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU4NDU4CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1ODQ2OAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNTg1NjEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU4NTY2 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1ODU2NwohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNTg1ODUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDU4NjI4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1ODY1NgohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNTg2NjUKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDU4Njc4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1ODY5MQoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTg3MjEKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDU4NzI4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA1 +OTQ2NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTk0ODQKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDU5NTM3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA1OTUzOAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTk1NTEKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU5ODE4CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA1OTkxNAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNTk5 +MjAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDU5OTU3CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA2MDA4OAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNjAxNTYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYwMTU3CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MDE2MAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNjAxODEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYwMTg1 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MDQ5MwohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNjA0OTUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDYwNTkwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MDg0NgohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNjA4NDcKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDYwODc1CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MDkyMQoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjA5NDYKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDYxMTIwCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2 +MTQ3NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjE1MjQKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDYxNjQyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA2MjAxNwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjIwMjAKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYyMDI4CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA2MjA5MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjIw +OTYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYyMTQ3CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA2MjE3NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNjIxODgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYyMjU0CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MjM3NAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNjI0ODIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDYyNDk2 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MjUwMQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNjI1MDgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDYyNTE5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MjU1NAohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNjI3NDEKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDYyNzk0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2MzE2MAoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjMzNTkKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDYzNTEyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2 +MzY2OAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjM2OTIKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDYzNzYzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA2Mzc2NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjM4MjYKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY0MTI3CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA2NDMxMgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjQ3 +MDUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY0ODA3CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA2NTEyMAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNjUxNjgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY1MjQ5CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2NTI4NwohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNjUzMzUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY1MzM3 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2NTU0MQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNjU3MTUKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDY1NzM1CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2NTc1NgohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNjU4MDIKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDY2MTEyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2NjEzNgoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjYxNDAKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDY2NDY1CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2 +Njg4MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjY5NTAKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDY2OTU5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA2NzQzNQohLS13d3cuZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjc0NzkK +IS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY3NTI4CiEtLS5mdGNoaW5lc2Uu +Y29tL3N0b3J5LzAwMTA2NzU0NQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEw +Njc1NzIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY3NjQ4CiEtLS5mdGNo +aW5lc2UuY29tL3N0b3J5LzAwMTA2NzY1MAohLS0uZnRjaGluZXNlLmNvbS9zdG9y +eS8wMDEwNjc2ODAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY3NjkyCiEt +LS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2Nzg3MQohLS0uZnRjaGluZXNlLmNv +bS9zdG9yeS8wMDEwNjc5MjMKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY4 +MDYyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2ODI0OAohLS0uZnRjaGlu +ZXNlLmNvbS9zdG9yeS8wMDEwNjgyNzgKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rvcnkv +MDAxMDY4Mzc5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2ODQ4MwohLS0u +ZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjg1MDYKIS0tLmZ0Y2hpbmVzZS5jb20v +c3RvcnkvMDAxMDY4NTQ3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA2ODYx +NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjg2MjIKIS0tLmZ0Y2hpbmVz +ZS5jb20vc3RvcnkvMDAxMDY4NzA3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAw +MTA2OTE0NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjkzNzMKIS0tLmZ0 +Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY5NTE2CiEtLS5mdGNoaW5lc2UuY29tL3N0 +b3J5LzAwMTA2OTUxNwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNjk2ODcK +IS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDY5NzQxCiEtLS5mdGNoaW5lc2Uu +Y29tL3N0b3J5LzAwMTA2OTg2MQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEw +Njk5NTIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDcwMDUzCiEtLS5mdGNo +aW5lc2UuY29tL3N0b3J5LzAwMTA3MDE3NwohLS0uZnRjaGluZXNlLmNvbS9zdG9y +eS8wMDEwNzAzMDcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDcwODA5CiEt +LS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3MDk5MAohLS0uZnRjaGluZXNlLmNv +bS9zdG9yeS8wMDEwNzEwNDIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDcx +MDQ0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3MTEwNgohLS0uZnRjaGlu +ZXNlLmNvbS9zdG9yeS8wMDEwNzExNjYKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rvcnkv +MDAxMDcxMTgxCiEtLWZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDcxMjAwCiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3MTIwOAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNzEyMzgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDcxNjgz +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3MjI3MQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNzIzNDgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDcyNjc3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3MjcyNgohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNzI3OTQKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDcyODUzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3Mjg5NQoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzI5OTMKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDczMDQzCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3 +MzEwMwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzMxNTcKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDczMjE2CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA3MzI0NgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzMzMDUKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDczMzA3CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA3MzQwOAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzM1 +MzcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDczNjcyCiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA3Mzg0OQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNzM5MDYKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc0MDg5CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NDExMAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNzQxMjgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc0MTU3 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NDI0NgohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNzQzMDcKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDc0MzQ3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NDQyMwohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNzQ0NTQKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDc0NDY3CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NDQ5Mwoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzQ1NTAKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDc0NTYyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3 +NDY1MwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzQ2OTMKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDc0Njk5CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA3NDcxMgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzQ3MTMKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc0NzY4CiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA3NDc4MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzQ3 +OTQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc0ODIyCiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA3NDg3NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNzQ4OTEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc0OTE4CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NTA4MQohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNzUxMzQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc1MTQy +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NTIxNgohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNzUyMzAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDc1MjM4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NTI2MgohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNzUyNjkKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDc1NDkxCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NTUwMAoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzU2NTAKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDc1Njc4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3 +NTcwMwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzU3MzkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDc2MDY2CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA3NjE0MgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzY0NTkKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc2NDcwCiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA3NjUzOAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzY1 +NzMKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc2OTAxCiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA3NzA2NwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwNzcwODQKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc3MjM1CiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NzM0NAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwNzczOTAKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc3Mzky +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3NzQ2NQohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwNzc0NjgKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDc3NDkyCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3Nzc0NQohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwNzc3NjgKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDc3ODA0CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3Nzg1Mgoh +LS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzg2NDYKIS0tLmZ0Y2hpbmVzZS5j +b20vc3RvcnkvMDAxMDc4OTI4CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA3 +ODk2NwohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzk1NTkKIS0tLmZ0Y2hp +bmVzZS5jb20vc3RvcnkvMDAxMDc5NjQxCiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5 +LzAwMTA3OTkwOQohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwNzk5MzQKIS0t +LmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDc5OTkyCiEtLS5mdGNoaW5lc2UuY29t +L3N0b3J5LzAwMTA4MDA1NAohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8wMDEwODAx +MDkKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDgwMTY5CiEtLS5mdGNoaW5l +c2UuY29tL3N0b3J5LzAwMTA4MDIyNgohLS0uZnRjaGluZXNlLmNvbS9zdG9yeS8w +MDEwODA0MjkKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDgwNDcxCiEtLS5m +dGNoaW5lc2UuY29tL3N0b3J5LzAwMTA4MDU1MAohLS0uZnRjaGluZXNlLmNvbS9z +dG9yeS8wMDEwODA1ODEKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAxMDgwNjQ3 +CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA4MDc3OAohLS0uZnRjaGluZXNl +LmNvbS9zdG9yeS8wMDEwODA4OTIKIS0tLmZ0Y2hpbmVzZS5jb20vc3RvcnkvMDAx +MDgwOTE1CiEtLS5mdGNoaW5lc2UuY29tL3N0b3J5LzAwMTA4MDkzNQohLS0uZnRj +aGluZXNlLmNvbS9zdG9yeS8wMDEwODEwNTkKIS0tLmZ0Y2hpbmVzZS5jb20vc3Rv +cnkvMDAxMDgxMTI3CiEtLS5mdGNoaW5lc2UuY29tL3RhZy8lRTUlOEQlODElRTUl +ODUlQUIlRTUlQjElOEElRTQlQjglODklRTQlQjglQUQlRTUlODUlQTglRTQlQkMl +OUEKIS0tLmZ0Y2hpbmVzZS5jb20vdGFnLyVFNiVCOCVBOSVFNSVBRSVCNiVFNSVB +RSU5RAohLS0uZnRjaGluZXNlLmNvbS90YWcvJUU4JTk2JTg0JUU3JTg2JTk5JUU2 +JTlEJUE1CiEtLS5mdGNoaW5lc2UuY29tL3ZpZGVvLzE0MzcKIS0tLmZ0Y2hpbmVz +ZS5jb20vdmlkZW8vMTg4MgohLS0uZnRjaGluZXNlLmNvbS92aWRlby8yNDQ2CiEt +LS5mdGNoaW5lc2UuY29tL3ZpZGVvLzI2MDEKIS0tLmZ0Y2hpbmVzZS5jb20vY29t +bWVudHMKCiEhLS0tR29vZ2xlLS0tCiEjIyNodHRwczovL3d3dy5nb29nbGUuY29t +L3N1cHBvcnRlZF9kb21haW5zIyMjCiEuLi5HRldMaXN0IGRvZXNuJ3QgaW50ZW5k +IHRvIHN1cHBvcnQgdHlwb3NxdWF0dGluZy4uLgp8fDFlMTAwLm5ldAp8fDQ2NjQ1 +My5jb20KfHxhYmMueHl6Cnx8YWJvdXQuZ29vZ2xlCnx8YWRtb2IuY29tCnx8YWRz +ZW5zZS5jb20KfHxhZHZlcnRpc2VyY29tbXVuaXR5LmNvbQp8fGFnb29nbGVhZGF5 +LmNvbQp8fGFpLmdvb2dsZQp8fGFtcHByb2plY3Qub3JnCkBAfGh0dHBzOi8vd3d3 +LmFtcHByb2plY3Qub3JnCkBAfGh0dHBzOi8vY2RuLmFtcHByb2plY3Qub3JnCnx8 +YW5kcm9pZC5jb20KfHxhbmRyb2lkaWZ5LmNvbQp8fGFuZHJvaWR0di5jb20KfHxh +cGkuYWkKLmFwcHNwb3QuY29tCnx8YXBwc3BvdC5jb20KfHxhdXRvZHJhdy5jb20K +fHxibG9nLmdvb2dsZQp8fGJsb2dibG9nLmNvbQpibG9nc3BvdC5jb20KL15odHRw +cz86XC9cL1teXC9dK2Jsb2dzcG90XC4oLiopLwouYmxvZ3Nwb3QuaGsKLmJsb2dz +cG90LmpwCi5ibG9nc3BvdC50dwp8fGJ1c2luZXNzLnBhZ2UKIS0tfHxjYXBpdGFs +Zy5jb20KfHxjZXJ0aWZpY2F0ZS10cmFuc3BhcmVuY3kub3JnCnx8Y2hyb21lLmNv +bQp8fGNocm9tZWNhc3QuY29tCnx8Y2hyb21lZW50ZXJwcmlzZS5nb29nbGUKfHxj +aHJvbWVleHBlcmltZW50cy5jb20KfHxjaHJvbWVyY2lzZS5jb20KfHxjaHJvbWVz +dGF0dXMuY29tCnx8Y2hyb21pdW0ub3JnCnx8Y29tLmdvb2dsZQp8fGNyYnVnLmNv +bQp8fGNyZWF0aXZlbGFiNS5jb20KfHxjcmlzaXNyZXNwb25zZS5nb29nbGUKfHxj +cnJldi5jb20KfHxkYXRhLXZvY2FidWxhcnkub3JnCnx8ZGVidWcuY29tCnx8ZGVl +cG1pbmQuY29tCnx8ZGVqYS5jb20KfHxkZXNpZ24uZ29vZ2xlCnx8ZGlnaXNmZXJh +LmNvbQp8fGRucy5nb29nbGUKfHxkb21haW5zLmdvb2dsZQp8fGR1Y2suY29tCnx8 +ZW52aXJvbm1lbnQuZ29vZ2xlCnx8ZmVlZGJ1cm5lci5jb20KfHxmaXJlYmFzZWlv +LmNvbQp8fGcuY28KfHxnY3IuaW8KfHxnZXQuYXBwCnx8Z2V0LmRldgp8fGdldC5o +b3cKfHxnZXQucGFnZQp8fGdldG1kbC5pbwp8fGdldG91dGxpbmUub3JnCnx8Z2dw +aHQuY29tCnx8Z21haWwuY29tCnx8Z21vZHVsZXMuY29tCnx8Z29kb2Mub3JnCnx8 +Z29sYW5nLm9yZwp8fGdvby5nbAp8fGdvby5nbGUKLmdvb2dsZS5hZQouZ29vZ2xl +LmFzCi5nb29nbGUuYW0KLmdvb2dsZS5hdAouZ29vZ2xlLmF6Ci5nb29nbGUuYmEK +Lmdvb2dsZS5iZQouZ29vZ2xlLmJnCi5nb29nbGUuY2EKLmdvb2dsZS5jZAouZ29v +Z2xlLmNpCi5nb29nbGUuY28uaWQKLmdvb2dsZS5jby5qcAouZ29vZ2xlLmNvLmty +Ci5nb29nbGUuY28ubWEKLmdvb2dsZS5jby51awouZ29vZ2xlLmNvbQouZ29vZ2xl +LmRlCnx8Z29vZ2xlLmRldgouZ29vZ2xlLmRqCi5nb29nbGUuZGsKLmdvb2dsZS5l +cwouZ29vZ2xlLmZpCi5nb29nbGUuZm0KLmdvb2dsZS5mcgouZ29vZ2xlLmdnCi5n +b29nbGUuZ2wKLmdvb2dsZS5ncgouZ29vZ2xlLmllCi5nb29nbGUuaXMKLmdvb2ds +ZS5pdAouZ29vZ2xlLmpvCi5nb29nbGUua3oKLmdvb2dsZS5sdgouZ29vZ2xlLm1u +Ci5nb29nbGUubXMKLmdvb2dsZS5ubAouZ29vZ2xlLm51Ci5nb29nbGUubm8KLmdv +b2dsZS5ybwouZ29vZ2xlLnJ1Ci5nb29nbGUucncKLmdvb2dsZS5zYwouZ29vZ2xl +LnNoCi5nb29nbGUuc2sKLmdvb2dsZS5zbQouZ29vZ2xlLnNuCi5nb29nbGUudGsK +Lmdvb2dsZS50bQouZ29vZ2xlLnRvCi5nb29nbGUudHQKLmdvb2dsZS52dQouZ29v +Z2xlLndzCi9eaHR0cHM/OlwvXC8oW15cL10rXC4pKmdvb2dsZVwuKGFjfGFkfGFl +fGFmfGFpfGFsfGFtfGFzfGF0fGF6fGJhfGJlfGJmfGJnfGJpfGJqfGJzfGJ0fGJ5 +fGNhfGNhdHxjZHxjZnxjZ3xjaHxjaXxjbHxjbXxjby5hb3xjby5id3xjby5ja3xj +by5jcnxjby5pZHxjby5pbHxjby5pbnxjby5qcHxjby5rZXxjby5rcnxjby5sc3xj +by5tYXxjb218Y29tLmFmfGNvbS5hZ3xjb20uYWl8Y29tLmFyfGNvbS5hdXxjb20u +YmR8Y29tLmJofGNvbS5ibnxjb20uYm98Y29tLmJyfGNvbS5ienxjb20uY298Y29t +LmN1fGNvbS5jeXxjb20uZG98Y29tLmVjfGNvbS5lZ3xjb20uZXR8Y29tLmZqfGNv +bS5naHxjb20uZ2l8Y29tLmd0fGNvbS5oa3xjb20uam18Y29tLmtofGNvbS5rd3xj +b20ubGJ8Y29tLmx5fGNvbS5tbXxjb20ubXR8Y29tLm14fGNvbS5teXxjb20ubmF8 +Y29tLm5mfGNvbS5uZ3xjb20ubml8Y29tLm5wfGNvbS5vbXxjb20ucGF8Y29tLnBl +fGNvbS5wZ3xjb20ucGh8Y29tLnBrfGNvbS5wcnxjb20ucHl8Y29tLnFhfGNvbS5z +YXxjb20uc2J8Y29tLnNnfGNvbS5zbHxjb20uc3Z8Y29tLnRqfGNvbS50cnxjb20u +dHd8Y29tLnVhfGNvbS51eXxjb20udmN8Y29tLnZufGNvLm16fGNvLm56fGNvLnRo +fGNvLnR6fGNvLnVnfGNvLnVrfGNvLnV6fGNvLnZlfGNvLnZpfGNvLnphfGNvLnpt +fGNvLnp3fGN2fGN6fGRlfGRqfGRrfGRtfGR6fGVlfGVzfGV1fGZpfGZtfGZyfGdh +fGdlfGdnfGdsfGdtfGdwfGdyfGd5fGhrfGhufGhyfGh0fGh1fGllfGltfGlxfGlz +fGl0fGl0LmFvfGplfGpvfGtnfGtpfGt6fGxhfGxpfGxrfGx0fGx1fGx2fG1kfG1l +fG1nfG1rfG1sfG1ufG1zfG11fG12fG13fG14fG5lfG5sfG5vfG5yfG51fG9yZ3xw +bHxwbnxwc3xwdHxyb3xyc3xydXxyd3xzY3xzZXxzaHxzaXxza3xzbXxzbnxzb3xz +cnxzdHx0ZHx0Z3x0a3x0bHx0bXx0bnx0b3x0dHx1c3x2Z3x2bnx2dXx3cylcLy4q +LwohLS18fGdvb2dsZS1hbmFseXRpY3MuY29tCiEtLXx8Z29vZ2xlYWRzZXJ2aWNl +cy5jb20KfHxnb29nbGVhcGlzLmNuCnx8Z29vZ2xlYXBpcy5jb20KfHxnb29nbGVh +cHBzLmNvbQp8fGdvb2dsZWFydHByb2plY3QuY29tCnx8Z29vZ2xlYmxvZy5jb20K +fHxnb29nbGVib3QuY29tCiEtLXx8Z29vZ2xlY2FwaXRhbC5jb20KfHxnb29nbGVj +aGluYXdlYm1hc3Rlci5jb20KfHxnb29nbGVjb2RlLmNvbQp8fGdvb2dsZWNvbW1l +cmNlLmNvbQp8fGdvb2dsZWRvbWFpbnMuY29tCnx8Z29vZ2xlYXJ0aC5jb20KfHxn +b29nbGVlYXJ0aC5jb20KfHxnb29nbGVkcml2ZS5jb20KfHxnb29nbGVmaWJlci5u +ZXQKfHxnb29nbGVncm91cHMuY29tCnx8Z29vZ2xlaG9zdGVkLmNvbQp8fGdvb2ds +ZWlkZWFzLmNvbQp8fGdvb2dsZWluc2lkZXNlYXJjaC5jb20KfHxnb29nbGVsYWJz +LmNvbQp8fGdvb2dsZW1haWwuY29tCnx8Z29vZ2xlbWFzaHVwcy5jb20KfHxnb29n +bGVwYWdlY3JlYXRvci5jb20KfHxnb29nbGVwbGF5LmNvbQp8fGdvb2dsZXBsdXMu +Y29tCnx8Z29vZ2xlc2Nob2xhci5jb21VU0EKfHxnb29nbGVzb3VyY2UuY29tCiEt +LXx8Z29vZ2xlc3luZGljYXRpb24uY29tCiEtLXx8Z29vZ2xldGFnbWFuYWdlci5j +b20KIS0tfHxnb29nbGV0YWdzZXJ2aWNlcy5jb20KfHxnb29nbGV1c2VyY29udGVu +dC5jb20KLmdvb2dsZXZpZGVvLmNvbQp8fGdvb2dsZXZpZGVvLmNvbQp8fGdvb2ds +ZXdlYmxpZ2h0LmNvbQp8fGdvb2dsZXppcC5uZXQKfHxncm91cHMuZ29vZ2xlLmNu +Cnx8Z3Jvdy5nb29nbGUKfHxnc3RhdGljLmNvbQohLS18fGd2LmNvbQp8fGd2dDAu +Y29tCnx8Z3Z0MS5jb20KQEB8fHJlZGlyZWN0b3IuZ3Z0MS5jb20KfHxndnQzLmNv +bQp8fGd3dHByb2plY3Qub3JnCnx8aHRtbDVyb2Nrcy5jb20KfHxpYW0uc295Cnx8 +aWdvb2dsZS5jb20KfHxpdGFzb2Z0d2FyZS5jb20KfHxsZXJzLmdvb2dsZQp8fGxp +a2UuY29tCnx8bWFkZXdpdGhjb2RlLmNvbQp8fG1hdGVyaWFsLmlvCnx8bmljLmdv +b2dsZQp8fG9uMi5jb20KfHxvcGVuc291cmNlLmdvb2dsZQp8fHBhbm9yYW1pby5j +b20KfHxwaWNhc2F3ZWIuY29tCnx8cGtpLmdvb2cKfHxwbHVzLmNvZGVzCnx8cG9s +eW1lci1wcm9qZWN0Lm9yZwp8fHByaWRlLmdvb2dsZQp8fHF1ZXN0dmlzdWFsLmNv +bQp8fGFkbWluLnJlY2FwdGNoYS5uZXQKfHxhcGkucmVjYXB0Y2hhLm5ldAp8fGFw +aS1zZWN1cmUucmVjYXB0Y2hhLm5ldAp8fGFwaS12ZXJpZnkucmVjYXB0Y2hhLm5l +dAp8fHJlZGhvdGxhYnMuY29tCnx8cmVnaXN0cnkuZ29vZ2xlCnx8cmVzZWFyY2gu +Z29vZ2xlCnx8c2FmZXR5Lmdvb2dsZQp8fHNhdmV0aGVkYXRlLmZvbwp8fHNjaGVt +YS5vcmcKfHxzaGF0dGVyZWQuaW8KfGh0dHA6Ly9zaXBtbDUub3JnLwp8fHN0b3Jp +ZXMuZ29vZ2xlCnx8c3VzdGFpbmFiaWxpdHkuZ29vZ2xlCnx8c3luZXJneXNlLmNv +bQp8fHRlYWNocGFyZW50c3RlY2gub3JnCnx8dGVuc29yZmxvdy5vcmcKfHx0Zmh1 +Yi5kZXYKfHx0aGlua3dpdGhnb29nbGUuY29tCnx8dGlsdGJydXNoLmNvbQp8fHRy +YW5zbGF0ZS5nb29nCnx8dHJhbnNsYXRlLmdvb2dsZQp8fHR2Lmdvb2dsZQp8fHVy +Y2hpbi5jb20KIS0tfHx3d3cuZ29vZ2xlCnx8d2F2ZXByb3RvY29sLm9yZwp8fHdh +eW1vLmNvbQp8fHdlYi5kZXYKfHx3ZWJtcHJvamVjdC5vcmcKfHx3ZWJydGMub3Jn +Cnx8d2hhdGJyb3dzZXIub3JnCnx8d2lkZXZpbmUuY29tCnx8d2l0aGdvb2dsZS5j +b20KfHx3aXRoeW91dHViZS5jb20KfHx4LmNvbXBhbnkKfHx4bi0tbmdzdHItbHJh +OGouY29tCnx8eW91dHUuYmUKLnlvdXR1YmUuY29tCnx8eW91dHViZS5jb20KfHx5 +b3V0dWJlLW5vY29va2llLmNvbQp8fHlvdXR1YmVlZHVjYXRpb24uY29tCnx8eW91 +dHViZWdhbWluZy5jb20KfHx5b3V0dWJla2lkcy5jb20KfHx5dC5iZQp8fHl0aW1n +LmNvbQp8fHp5bmFtaWNzLmNvbQoKISEtLS1LaWNrQVNTLS0tCiEtLU9GRklDSUFM +IFVSTCBsaXN0IGF0OiBodHRwczovL2thc3RhdHVzLmNvbQoKISEtLS1OYXVnaHR5 +QW1lcmljYS0tLQp8fG5hdWdodHlhbWVyaWNhLmNvbQoKISEtLS1OWVRpbWVzLS0t +CiEtLXx8ZDFmMWVyeWlxeWpzMHIuY2xvdWRmcm9udC5uZXQKIS0tfHxkM2xhcjA5 +eGJ3bHNnZS5jbG91ZGZyb250Lm5ldAohLS18fGQzcTFxajlqenN1OG53LmNsb3Vk +ZnJvbnQubmV0CiEtLXx8ZGM4eGwwbmR6bjJjYi5jbG91ZGZyb250Lm5ldAohLS18 +fGExLm55dC5jb20KIS0tfHxpbnQubnl0LmNvbQohLS18fHMxLm55dC5jb20Kc3Rh +dGljMDEubnl0LmNvbQohLS18fHN0YXRpYzAxLm55dC5jb20KIS0tfHx0eXBlZmFj +ZS5ueXQuY29tCnx8bnl0LmNvbQpueXRjaGluYS5jb20Kbnl0Y24ubWUKfHxueXRj +bi5tZQp8fG55dGNvLmNvbQp8aHR0cDovL255dGkubXMvCi5ueXRpbWVzLmNvbQp8 +fG55dGltZXMuY29tCnx8bnl0aW1nLmNvbQp1c2VyYXBpLm55dGxvZy5jb20KY24u +bnl0c3R5bGUuY29tCnx8bnl0c3R5bGUuY29tCgohIS0tLVN0ZWFtLS0tCi5zdGVh +bWNvbW11bml0eS5jb20KfHxzdGVhbWNvbW11bml0eS5jb20KIS0tc3RlYW1jb21t +dW5pdHkuY29tL3Byb2ZpbGVzLzc2NTYxMTk4MDYyNzcxNjA5CiEtLXN0ZWFtY29t +bXVuaXR5LmNvbS9ncm91cHMvTGliZXRUaWJldAohLS1zdGVhbWNvbW11bml0eS5j +b20vZ3JvdXBzL3pob25nZ29uZwohLS1zdGVhbWNvbW11bml0eS5jb20vaWQvQ0pU +X0phY2t0b24KfHxzdG9yZS5zdGVhbXBvd2VyZWQuY29tCgohIS0tLVRlbGVncmFt +LS0tCiEhIS0tLURvbWFpbi0tLQp8fHQubWUKfHx1cGRhdGVzLnRkZXNrdG9wLmNv +bQp8fHRlbGVncmFtLmRvZwp8fHRlbGVncmFtLm1lCnx8dGVsZWdyYW0ub3JnCi50 +ZWxlZ3JhbWRvd25sb2FkLmNvbQp8fHRlbGVzY28ucGUKISEhLS0tSVAtLS0KCiEh +LS0tVHdpdGNoLS0tCnx8anR2bncubmV0Cnx8dHR2bncubmV0Cnx8dHdpdGNoLnR2 +Cnx8dHdpdGNoY2RuLm5ldAoKISEtLS1Ud2l0dGVyLS0tCnx8cGVyaXNjb3BlLnR2 +Ci5wc2NwLnR2Cnx8cHNjcC50dgoudC5jbwp8fHQuY28KLnR3ZWV0ZGVjay5jb20K +fHx0d2VldGRlY2suY29tCnx8dHdpbWcuY29tCi50d2l0cGljLmNvbQp8fHR3aXRw +aWMuY29tCi50d2l0dGVyLmNvbQp8fHR3aXR0ZXIuY29tCnx8dHdpdHRlci5qcAp8 +fHZpbmUuY28KCiEhLS0tVGFpd2FuLS0tCnx8Z292LnRhaXBlaQouZ292LnR3Cnxo +dHRwczovL2Fpc3MuYW53cy5nb3YudHcKfHxhcmNoaXZlcy5nb3YudHcKfHx0YWNj +LmN3Yi5nb3YudHcKfHxkYXRhLmdvdi50dwp8fGVwYS5nb3YudHcKfHxmYS5nb3Yu +dHcKfHxmZGEuZ292LnR3Cnx8aHBhLmdvdi50dwp8fGltbWlncmF0aW9uLmdvdi50 +dwp8fGl0YWl3YW4uZ292LnR3Cnx8bWppYi5nb3YudHcKfHxtb2VhaWMuZ292LnR3 +Cnx8bW9mYS5nb3YudHcKfHxtb2wuZ292LnR3Cnx8bXZkaXMuZ292LnR3Cnx8bmF0 +Lmdvdi50dwp8fG5oaS5nb3YudHcKfHxucGEuZ292LnR3Cnx8bnNjLmdvdi50dwp8 +fG50YmsuZ292LnR3Cnx8bnRibmEuZ292LnR3Cnx8bnRidC5nb3YudHcKfHxudHNu +YS5nb3YudHcKfHxwY2MuZ292LnR3Cnx8c3RhdC5nb3YudHcKfHx0YWlwZWkuZ292 +LnR3Cnx8dGFpd2Fuam9icy5nb3YudHcKfHx0aGIuZ292LnR3Cnx8dGlwby5nb3Yu +dHcKfHx3ZGEuZ292LnR3Cgp8fHRlY28taGsub3JnCnx8dGVjby1tby5vcmcKCkBA +fHxhZnR5Z2guZ292LnR3CkBAfHxhaWRlLmdvdi50dwpAQHx8dHBkZS5haWRlLmdv +di50dwpAQHx8YXJ0ZS5nb3YudHcKQEB8fGNodWt1YW5nLmdvdi50dwpAQHx8Y3di +Lmdvdi50dwpAQHx8Y3ljYWIuZ292LnR3CkBAfHxkYm5zYS5nb3YudHcKQEB8fGRm +Lmdvdi50dwpAQHx8ZWFzdGNvYXN0LW5zYS5nb3YudHcKQEB8fGVydi1uc2EuZ292 +LnR3CkBAfHxncmIuZ292LnR3CkBAfHxneXNkLm55Yy5nb3YudHcKQEB8fGhjaGNj +Lmdvdi50dwpAQHx8aHNpbmNodS1jYy5nb3YudHcKQEB8fGluZXIuZ292LnR3CkBA +fHxrbHNpby5nb3YudHcKQEB8fGttc2VoLmdvdi50dwpAQHx8bHVuZ3RhbmhyLmdv +di50dwpAQHx8bWFvbGluLW5zYS5nb3YudHcKQEB8fG1hdHN1LW5ld3MuZ292LnR3 +CkBAfHxtYXRzdS1uc2EuZ292LnR3CkBAfHxtYXRzdWNjLmdvdi50dwpAQHx8bW9l +Lmdvdi50dwpAQHx8bmFua2FuLmdvdi50dwpAQHx8bmNyZWUuZ292LnR3CkBAfHxu +ZWNvYXN0LW5zYS5nb3YudHcKQEB8fHNpcmF5YS1uc2EuZ292LnR3CkBAfHxjcm9t +b3RjLm5hdC5nb3YudHcKQEB8fHRheC5uYXQuZ292LnR3CkBAfHxuZWNvYXN0LW5z +YS5nb3YudHcKQEB8fG5lci5nb3YudHcKQEB8fG5tbWJhLmdvdi50dwpAQHx8bm1w +Lmdvdi50dwpAQHx8bm12dHRjLmdvdi50dwpAQHx8bm9ydGhndWFuLW5zYS5nb3Yu +dHcKQEB8fG5wbS5nb3YudHcKQEB8fG5zdG0uZ292LnR3CkBAfHxudGRtaC5nb3Yu +dHcKQEB8fG50bC5nb3YudHcKQEB8fG50c2VjLmdvdi50dwpAQHx8bnR1aC5nb3Yu +dHcKQEB8fG52cmkuZ292LnR3CkBAfHxwZW5naHUtbnNhLmdvdi50dwpAQHx8cG9z +dC5nb3YudHcKQEB8fHNpcmF5YS1uc2EuZ292LnR3CkBAfHxzdGR0aW1lLmdvdi50 +dwpAQHx8c3VubW9vbmxha2UuZ292LnR3CkBAfHx0YWl0dW5nLWhvdXNlLmdvdi50 +dwpAQHx8dGFveXVhbi5nb3YudHcKQEB8fHRwaGNjLmdvdi50dwpAQHx8dHJpbXQt +bnNhLmdvdi50dwpAQHx8dmdodHBlLmdvdi50dwpAQHx8dmdoa3MuZ292LnR3CkBA +fHx2Z2h0Yy5nb3YudHcKQEB8fHdhbmZhbmcuZ292LnR3CkBAfHx5YXRzZW4uZ292 +LnR3CkBAfHx5ZGEuZ292LnR3CgohLS1AQHx8NHBwcGMuZ292LnR3CiEtLUBAfHw5 +MjEuZ292LnR3CiEtLUBAfHxkbXRpcC5nb3YudHcKIS0tQEB8fGV0cmFpbmluZy5n +b3YudHcKIS0tQEB8fGdzbi1jZXJ0Lm5hdC5nb3YudHcKIS0tQEB8fG5pY2kubmF0 +Lmdvdi50dwohLS1AQHx8aGNjLmdvdi50dwohLS1AQHx8aGVuZ2NodWVuLmdvdi50 +dwohLS1AQHx8a2hjYy5nb3YudHcKIS0tQEB8fGtobXMuZ292LnR3CiEtLUBAfHxr +ay5nb3YudHcKIS0tQEB8fGtsY2NhYi5nb3YudHcKIS0tQEB8fGtscmEuZ292LnR3 +CiEtLUBAfHxubWguZ292LnR3CiEtLUBAfHxubXRsLmdvdi50dwohLS1AQHx8cGFi +cC5nb3YudHcKIS0tQEB8fHBldC5nb3YudHcKIS0tQEB8fHRjaGIuZ292LnR3CiEt +LUBAfHx0Y3NhYy5nb3YudHcKIS0tQEB8fHRuY3NlYy5nb3YudHcKfHxraW5tZW4u +b3JnLnR3CgohIS0tLVVTQS0tLQp8aHR0cDovL3d3dy5hbWVyaWNvcnBzLmdvdgp8 +fGpwbC5uYXNhLmdvdgp8fHBkcy5uYXNhLmdvdgp8fHNvbGFyc3lzdGVtLm5hc2Eu +Z292CmlpcGRpZ2l0YWwudXNlbWJhc3N5Lmdvdgp8fHVzZmsubWlsCnx8dXNtYy5t +aWwKfGh0dHA6Ly90YXJyLnVzcHRvLmdvdi8KfHx0c2RyLnVzcHRvLmdvdgoKISEt +LS1WMkVYLS0tCnx8djJleC5jb20KIS0tLnYyZXguY29tCiEtLUluY2x1ZGVkIGlu +IGFib3ZlIHJ1bGU6IGRucy52MmV4LmNvbQohLS1AQHxodHRwOi8vdjJleC5jb20K +IS0tQEB8aHR0cDovL2Nkbi52MmV4LmNvbQohLS1AQHxodHRwOi8vY24udjJleC5j +b20KIS0tQEB8aHR0cDovL2hrLnYyZXguY29tCiEtLUBAfGh0dHA6Ly9pLnYyZXgu +Y29tCiEtLUBAfGh0dHA6Ly9sYXgudjJleC5jb20KIS0tQEB8aHR0cDovL25ldWUu +djJleC5jb20KIS0tQEB8aHR0cDovL3BhZ2VzcGVlZC52MmV4LmNvbQohLS1AQHxo +dHRwOi8vc3RhdGljLnYyZXguY29tCiEtLUBAfGh0dHA6Ly93b3Jrc3BhY2UudjJl +eC5jb20KIS0tQEB8aHR0cDovL3d3dy52MmV4LmNvbQoKISEtLS1WT0EtLS0KY24u +dm9hLm1vYmkKdHcudm9hLm1vYmkKfHx2b2FjYW1ib2RpYS5jb20KLnZvYWNoaW5l +c2VibG9nLmNvbQp8fHZvYWNoaW5lc2VibG9nLmNvbQoudm9hY2FudG9uZXNlLmNv +bQp8fHZvYWNhbnRvbmVzZS5jb20Kdm9hY2hpbmVzZS5jb20KfHx2b2FjaGluZXNl +LmNvbQp2b2FnZC5jb20KfHx2b2FpbmRvbmVzaWEuY29tCi52b2FuZXdzLmNvbQp8 +fHZvYW5ld3MuY29tCnZvYXRpYmV0YW4uY29tCnx8dm9hdGliZXRhbi5jb20KLnZv +YXRpYmV0YW5lbmdsaXNoLmNvbQp8fHZvYXRpYmV0YW5lbmdsaXNoLmNvbQoKISEt +LS1XaWtpYS0tLQp8fHpoLmVjZG0ud2lraWEuY29tCnx8ZXZjaGsud2lraWEuY29t +CmZxLndpa2lhLmNvbQp6aC5wdHRwZWRpYS53aWtpYS5jb20vd2lraS8lRTclQkYl +OTIlRTUlOEMlODUlRTUlQUQlOTAlRTQlQjklOEIlRTQlQkElODIKY24udW5jeWNs +b3BlZGlhLndpa2lhLmNvbQp6aC51bmN5Y2xvcGVkaWEud2lraWEuY29tCgohLS0t +LS0tLS0tLS0tLVdpa2lwZWRpYSBSZWxhdGVkLS0tLS0tLS0tLS0tLQohIUVtZXJn +ZW5jeSBuZWVkIG9ubHkoSVAvUG9ydCBibG9jayB1c2FnZSkhIQohLS0tLS0tMC0t +LS0tLQohLS18fG1lZGlhd2lraS5vcmcKIS0tQEB8fG0ubWVkaWF3aWtpLm9yZwoh +LS0tLS0tMS0tLS0tLQohLS18fHdpa2lkYXRhLm9yZwohLS1AQHx8bS53aWtpZGF0 +YS5vcmcKIS0tLS0tLTItLS0tLS0KfHx3aWtpbWVkaWEub3JnCiEtLUBAfHxsaXN0 +cy53aWtpbWVkaWEub3JnCiEtLUBAfHxtLndpa2ltZWRpYS5vcmcKIS0tQEB8fHBo +YWJyaWNhdG9yLndpa2ltZWRpYS5vcmcKIS0tQEB8fHVwbG9hZC53aWtpbWVkaWEu +b3JnCiEtLUBAfHx3aWtpdGVjaC53aWtpbWVkaWEub3JnCiEtLS0tLS0zLS0tLS0t +CiEtLXx8d2lraWJvb2tzLm9yZwohLS1AQHx8bS53aWtpYm9va3Mub3JnCiEtLS0t +LS00LS0tLS0tCiEtLXx8d2lraXZlcnNpdHkub3JnCiEtLUBAfHxtLndpa2l2ZXJz +aXR5Lm9yZwohLS0tLS0tNS0tLS0tLQohLS18fHdpa2lzb3VyY2Uub3JnCiEtLUBA +fHxtLndpa2lzb3VyY2Uub3JnCnxodHRwOi8vemgud2lraXNvdXJjZS5vcmcKIS0t +LS0tLTYtLS0tLS0KfHx6aC53aWtpcXVvdGUub3JnCiEtLUBAfHxtLndpa2lxdW90 +ZS5vcmcKIS0tLS0tLTctLS0tLS0KIS0tfHx3aWtpbmV3cy5vcmcKIS0tQEB8fG0u +d2lraW5ld3Mub3JnCnx8emgud2lraW5ld3Mub3JnCiEtLS0tLS04LS0tLS0tCiEt +LXx8d2lraXZveWFnZS5vcmcKIS0tQEB8fG0ud2lraXZveWFnZS5vcmcKIS0tfGh0 +dHA6Ly96aC53aWtpdm95YWdlLm9yZwohLS0tLS0tOS0tLS0tLQohLS18fHdpa3Rp +b25hcnkub3JnCiEtLUBAfHxtLndpa3Rpb25hcnkub3JnCiEtLXxodHRwOi8vemgu +d2lrdGlvbmFyeS5vcmcKIS0tLS0tMTAtLS0tLS0KIS0tfHx3aWtpbWVkaWFmb3Vu +ZGF0aW9uLm9yZwohLS1AQHx8bS53aWtpbWVkaWFmb3VuZGF0aW9uLm9yZwohLS0t +LU1haW4tLS0tLQohIS0tfHxlbi53aWtpcGVkaWEub3JnCiEtLXx8d2lraXBlZGlh +Lm9yZwp8fGphLndpa2lwZWRpYS5vcmcKISEtLXpoLndpa2lwZWRpYS5vcmcKIS0t +fHx6aC53aWtpcGVkaWEub3JnCiEhLS18fHVnLm0ud2lraXBlZGlhLm9yZwohIS0t +emgubS53aWtpcGVkaWEub3JnCiEhLS18aHR0cHM6Ly96aC5tLndpa2lwZWRpYS5v +cmcKIS0tQEB8fG0ud2lraXBlZGlhLm9yZwohIS0tfGh0dHBzOi8vemgud2lraXBl +ZGlhLm9yZwohLS1PdGhlciBMYW5ndWFnZXMgb2YgV2lraXBlZGlhCiEhLS13dXUu +d2lraXBlZGlhLm9yZwohIS0tfGh0dHBzOi8vd3V1Lndpa2lwZWRpYS5vcmcKISEt +LXpoLXl1ZS53aWtpcGVkaWEub3JnCiEhLS18aHR0cHM6Ly96aC15dWUud2lraXBl +ZGlhLm9yZwohISEgU3RhcnRpbmcgd2l0aCAhISBhcmUgcHJldmlvdXMgcnVsZXMg +cmVwbGFjZWQgYnk6Cnx8d2lraXBlZGlhLm9yZwoKISEtLS1ZYWhvby0tLQp8fGRh +dGEuZmx1cnJ5LmNvbQp8fHBhZ2UuYmlkLnlhaG9vLmNvbQp8fHR3LmJpZC55YWhv +by5jb20KfHxhdWN0aW9ucy55YWhvby5jby5qcAp8fGJsb2dzLnlhaG9vLmNvLmpw +Cnx8c2VhcmNoLnlhaG9vLmNvLmpwCnx8YnV5LnlhaG9vLmNvbS50dwp8fGhrLnlh +aG9vLmNvbQp8fGhrLmtub3dsZWRnZS55YWhvby5jb20KfHx0dy5tb25leS55YWhv +by5jb20KfHxoay5teWJsb2cueWFob28uY29tCm5ld3MueWFob28uY29tL2NoaW5h +LWJsb2Nrcy1iYmMKfHxoay5uZXdzLnlhaG9vLmNvbQpoay5yZC55YWhvby5jb20K +aGsuc2VhcmNoLnlhaG9vLmNvbS9zZWFyY2gKaGsudmlkZW8ubmV3cy55YWhvby5j +b20vdmlkZW8KbWVtZS55YWhvby5jb20KIS0tdHcueWFob28uY29tCnR3LmFuc3dl +cnMueWFob28uY29tCnxodHRwczovL3R3LmFuc3dlcnMueWFob28uY29tCnx8dHcu +a25vd2xlZGdlLnlhaG9vLmNvbQp8fHR3Lm1hbGwueWFob28uY29tCnR3LnlhaG9v +LmNvbQp8fHR3Lm1vYmkueWFob28uY29tCnR3Lm15YmxvZy55YWhvby5jb20KfHx0 +dy5uZXdzLnlhaG9vLmNvbQpwdWxzZS55YWhvby5jb20KfHxzZWFyY2gueWFob28u +Y29tCnVwY29taW5nLnlhaG9vLmNvbQp2aWRlby55YWhvby5jb20KfHx5YWhvby5j +b20uaGsKfHxkdWNrZHVja2dvLW93bmVkLXNlcnZlci55YWhvby5uZXQKCiEtLS0t +LS0tLS0tLS0tLS0tLS1OdW1lcmljcy0tLS0tLS0tLS0tLS0tLS0tLS0tLQp8fDAw +MHdlYmhvc3QuY29tCi4wMzBidXkuY29tCi4wcnoudHcKfGh0dHA6Ly8wcnoudHcK +MS1hcHBsZS5jb20udHcKfHwxLWFwcGxlLmNvbS50dwouMTAudHQKLjEwMGtlLm9y +ZwouMTAwMGdpcmkubmV0Cnx8MTAwMGdpcmkubmV0Cnx8MTBiZWFzdHMubmV0Ci4x +MGNvbmRpdGlvbnNvZmxvdmUuY29tCnx8MTBtdXN1bWUuY29tCjEyM3JmLmNvbQou +MTJiZXQuY29tCnx8MTJiZXQuY29tCi4xMnZwbi5jb20KLjEydnBuLm5ldAp8fDEy +dnBuLmNvbQp8fDEydnBuLm5ldAp8fDEzMzd4LnRvCi4xMzguY29tCjE0MWhvbmdr +b25nLmNvbS9mb3J1bQp8fDE0MWpqLmNvbQouMTQxdHViZS5jb20KLjE2ODguY29t +LmF1Ci4xNzNuZy5jb20KfHwxNzNuZy5jb20KLjE3N3BpYy5pbmZvCi4xN3QxN3Au +Y29tCnx8MThib2FyZC5jb20KfHwxOGJvYXJkLmluZm8KMThvbmx5Z2lybHMuY29t +Ci4xOHAycC5jb20KLjE4dmlyZ2luc2V4LmNvbQouMTk0OWVyLm9yZwp6aGFvLjE5 +ODQuY2l0eQp8fHpoYW8uMTk4NC5jaXR5CjE5ODRiYnMuY29tCnx8MTk4NGJicy5j +b20KIS0tfHwxOTg0YmxvZy5jb20KLjE5ODRiYnMub3JnCnx8MTk4NGJicy5vcmcK +LjE5OTF3YXkuY29tCnx8MTk5MXdheS5jb20KLjE5OThjZHAub3JnCi4xYmFvLm9y +Zwp8aHR0cDovLzFiYW8ub3JnCi4xZWV3LmNvbQouMW1vYmlsZS5jb20KfGh0dHA6 +Ly8qLjFtb2JpbGUudHcKfHwxcG9uZG8udHYKLjItaGFuZC5pbmZvCi4yMDAwZnVu +LmNvbS9iYnMKLjIwMDh4aWFuemhhbmcuaW5mbwp8fDIwMDh4aWFuemhhbmcuaW5m +bwp8fDIwMTcuaGsKfHwyMDIxaGtjaGFydGVyLmNvbQp8fDIwNDcubmFtZQoyMWFu +ZHkuY29tL2Jsb2cKLjIxam9pbi5jb20KLjIxcHJvbi5jb20KMjFzZXh0dXJ5LmNv +bQouMjI4Lm5ldC50dwp8fDIzM2FiYy5jb20KfHwyNGhycy5jYQoyNHNtaWxlLm9y +ZwoybGlwc3R1YmUuY29tCi4yc2hhcmVkLmNvbQozMGJveGVzLmNvbQouMzE1bHou +Y29tCnx8MzJyZWQuY29tCnx8MzZyYWluLmNvbQouM2E1YS5jb20KM2FyYWJ0di5j +b20KLjNib3lzMmdpcmxzLmNvbQouM3Byb3h5LnJ1Ci4zcmVuLmNhCi4zdHVpLm5l +dAp8fDQwNG11c2V1bS5jb20KfHw0Ymx1ZXN0b25lcy5iaXoKLjRjaGFuLmNvbQoh +LS18fDRjaGFuLm9yZwouNGV2ZXJwcm94eS5jb20KfHw0ZXZlcnByb3h5LmNvbQp8 +fDRyYnR2LmNvbQp8fDRzaGFyZWQuY29tCnRhaXdhbm5hdGlvbi41MHdlYnMuY29t +Cnx8NTEuY2EKfHw1MWphdi5vcmcKLjUxbHVvYmVuLmNvbQp8fDUxbHVvYmVuLmNv +bQp8fDUyNzguY2MKLjUyOTkudHYKNWFpbWlrdS5jb20KNWkwMS5jb20KLjVpc290 +b2k1Lm9yZwouNW1hb2RhbmcuY29tCnx8NjNpLmNvbQouNjRtdXNldW0ub3JnCjY0 +dGlhbndhbmcuY29tCjY0d2lraS5jb20KLjY2LmNhCjY2NmtiLmNvbQp8fDZkby5u +ZXdzCi42cGFyay5jb20KfHw2cGFyay5jb20KfHw2cGFya2Jicy5jb20KfHw2cGFy +a2VyLmNvbQp8fDZwYXJrbmV3cy5jb20KfHw3Y2FwdHVyZS5jb20KLjdjb3cuY29t +Cnx8Ny16aXAub3JnCi44LWQuY29tCnxodHRwOi8vOC1kLmNvbQo4NWNjLm5ldAou +ODVjYy51cwp8aHR0cDovLzg1Y2MudXMKfGh0dHA6Ly84NXN0LmNvbQouODgxOTAz +LmNvbS9wYWdlL3poLXR3Lwp8fDg4MTkwMy5jb20KLjg4OC5jb20KLjg4OHBva2Vy +LmNvbQo4OS42NC5jaGFydGVyLmNvbnN0aXR1dGlvbmFsaXNtLnNvbHV0aW9ucwo4 +OS02NC5vcmcKfHw4OS02NC5vcmcKfHw4OTY0bXVzZXVtLmNvbQouOG5ld3MuY29t +LnR3Ci44ejEubmV0Cnx8OHoxLm5ldAouOTAwMTcwMC5jb20KfGh0dHA6Ly85MDh0 +YWl3YW4ub3JnLwp8fDkxcG9ybi5jb20KfHw5MXZwcy5jbHViCi45MmNjYXYuY29t +Ci45OTEuY29tCnxodHRwOi8vOTkxLmNvbQouOTlidGdjMDEuY29tCnx8OTlidGdj +MDEuY29tCi45OWNuLmluZm8KfGh0dHA6Ly85OWNuLmluZm8KfHw5YmlzLmNvbQp8 +fDliaXMubmV0Cnx8OW5ld3MuY29tLmF1CgohLS0tLS0tLS0tLS0tLS0tLS0tLS1B +QS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLnRpYmV0LmEuc2UKfGh0dHA6Ly90 +aWJldC5hLnNlCnx8YS1ub3JtYWwtZGF5LmNvbQphNS5jb20ucnUKfGh0dHA6Ly9h +YW1hY2F1LmNvbQohLS18aHR0cDovL2NkbiouYWJjLmNvbS8KLmFiYy5jb20KLmFi +Yy5uZXQuYXUKfHxhYmMubmV0LmF1Ci5hYmNoaW5lc2UuY29tCmFiY2xpdGUubmV0 +CnxodHRwczovL3d3dy5hYmNsaXRlLm5ldAouYWJsd2FuZy5jb20KLmFib2x1b3dh +bmcuY29tCnx8YWJvbHVvd2FuZy5jb20KLmFib3V0Z2Z3LmNvbQouYWJzLmVkdQp8 +fGFjYXN0LmNvbQouYWNjaW0ub3JnCi5hY2Vyb3MtZGUtaGlzcGFuaWEuY29tCi5h +Y2V2cG4uY29tCnx8YWNldnBuLmNvbQouYWNnMTgubWUKfGh0dHA6Ly9hY2cxOC5t +ZQp8fGFjZ2JveC5vcmcKfHxhY2drai5jb20KfHxhY2dueC5zZQouYWNtZWRpYTM2 +NS5jb20KLmFjbncuY29tLmF1CmFjdGZvcnRpYmV0Lm9yZwphY3RpbWVzLmNvbS5h +dQphY3RpdnBuLmNvbQp8fGFjdGl2cG4uY29tCnx8YWN1bG8udXMKfHxhZGRpY3Rl +ZHRvY29mZmVlLmRlCnx8YWRkeW91dHViZS5jb20KLmFkZWxhaWRlYmJzLmNvbS9i +YnMKLmFkcGwub3JnLmhrCnxodHRwOi8vYWRwbC5vcmcuaGsKLmFkdWx0LXNleC1n +YW1lcy5jb20KfHxhZHVsdC1zZXgtZ2FtZXMuY29tCmFkdWx0ZnJpZW5kZmluZGVy +LmNvbQphZHVsdGtlZXAubmV0L3BlZXBzaG93L21lbWJlcnMvbWFpbi5odG0KfHxh +ZHZhbnNjZW5lLmNvbQp8fGFkdmVydGZhbi5jb20KLmFlLm9yZwp8fGFlbmhhbmNl +cnMuY29tCnx8YWYubWlsCi5hZmFudGliYnMuY29tCnxodHRwOi8vYWZhbnRpYmJz +LmNvbQp8fGFmci5jb20KLmFpLWthbi5uZXQKfHxhaS1rYW4ubmV0CmFpLXdlbi5u +ZXQKLmFpcGgubmV0Cnx8YWlwaC5uZXQKLmFpcmFzaWEuY29tCnx8YWlyY29uc29s +ZS5jb20KfGh0dHA6Ly9kb3dubG9hZC5haXJjcmFjay1uZy5vcmcKLmFpcnZwbi5v +cmcKfHxhaXJ2cG4ub3JnCi5haXNleC5jb20KfHxhaXQub3JnLnR3CmFpd2Vpd2Vp +LmNvbQouYWl3ZWl3ZWlibG9nLmNvbQp8fGFpd2Vpd2VpYmxvZy5jb20KfHx3d3cu +YWpzYW5kcy5jb20KCiEhLS0tQWthbWFpLS0tCmEyNDguZS5ha2FtYWkubmV0Cnx8 +YTI0OC5lLmFrYW1haS5uZXQKCnJmYWxpdmUxLmFrYWNhc3QuYWthbWFpc3RyZWFt +Lm5ldAp2b2EtMTEuYWthY2FzdC5ha2FtYWlzdHJlYW0ubmV0CgohIS0tNDAzCnx8 +YWJlbWF0di5ha2FtYWl6ZWQubmV0Cnx8bGluZWFyLWFiZW1hdHYuYWthbWFpemVk +Lm5ldAp8fHZvZC1hYmVtYXR2LmFrYW1haXplZC5uZXQKCnxodHRwczovL2ZiY2Ru +Ki5ha2FtYWloZC5uZXQvCiEtLXx8ZmJleHRlcm5hbC1hLmFrYW1haWhkLm5ldAoh +LS18fGZic3RhdGljLWEuYWthbWFpaGQubmV0CiEtLXxodHRwczovL2lnY2RuKi5h +a2FtYWloZC5uZXQKcnRoa2xpdmUyLWxoLmFrYW1haWhkLm5ldAoKLmFrYWRlbWl5 +ZS5vcmcvdWcKfGh0dHA6Ly9ha2FkZW1peWUub3JnL3VnCnx8YWtpYmEtb25saW5l +LmNvbQp8fGFrb3cub3JnCi5hbC1pc2xhbS5jb20KfHxhbC1xaW1tYWgubmV0Cnx8 +YWxhYm91dC5jb20KLmFsYW5ob3UuY29tCnxodHRwOi8vYWxhbmhvdS5jb20KLmFs +YXJhYi5xYQp8fGFsYXNiYXJyaWNhZGFzLm9yZwphbGV4bHVyLm9yZwp8fGFsZm9y +YXR0di5uZXQKLmFsaGF5YXQuY29tCi5hbGljZWphcGFuLmNvLmpwCmFsaWVuZ3Uu +Y29tCnx8YWxpdmUuYmFyCnx8YWxrYXNpci5jb20KfHxhbGw0bW9tLm9yZwp8fGFs +bGNvbm5lY3RlZC5jbwouYWxsZHJhd25zZXguY29tCnx8YWxsZHJhd25zZXguY29t +Ci5hbGxlcnZwbi5jb20KfHxhbGxmaW5lZ2lybHMuY29tCi5hbGxnaXJsbWFzc2Fn +ZS5jb20KYWxsZ2lybHNhbGxvd2VkLm9yZwouYWxsZ3JhdnVyZS5jb20KYWxsaWFu +Y2Uub3JnLmhrCi5hbGxpbmZhLmNvbQp8fGFsbGluZmEuY29tCi5hbGxqYWNrcG90 +c2Nhc2luby5jb20KfHxhbGxtb3ZpZS5jb20KfHxhbG1hc2Rhcm5ld3MuY29tCi5h +bHBoYXBvcm5vLmNvbQp8fGFsdGVybmF0ZS10b29scy5jb20KYWx0ZXJuYXRpdmV0 +by5uZXQvc29mdHdhcmUKYWx2aW5hbGV4YW5kZXIuY29tCmFsd2F5c2RhdGEuY29t +Cnx8YWx3YXlzZGF0YS5jb20KfHxhbHdheXNkYXRhLm5ldAouYWx3YXlzdnBuLmNv +bQp8fGFsd2F5c3Zwbi5jb20KfHxhbTczMC5jb20uaGsKYW1lYmxvLmpwCnx8YW1l +YmxvLmpwCnd3dzEuYW1lcmljYW4uZWR1L3RlZC9pY2UvdGliZXQKfHxhbWVyaWNh +bmdyZWVuY2FyZC5jb20KfHxhbWlibG9ja2Vkb3Jub3QuY29tCi5hbWlnb2Jicy5u +ZXQKLmFtaXRhYmhhZm91bmRhdGlvbi51cwp8aHR0cDovL2FtaXRhYmhhZm91bmRh +dGlvbi51cwouYW1uZXN0eS5vcmcKfHxhbW5lc3R5Lm9yZwp8fGFtbmVzdHkub3Jn +LmhrCi5hbW5lc3R5LnR3Ci5hbW5lc3R5dXNhLm9yZwp8fGFtbmVzdHl1c2Eub3Jn +Ci5hbW55ZW1hY2hlbi5vcmcKLmFtb2lpc3QuY29tCi5hbXRiLXRhaXBlaS5vcmcK +YW5kcm9pZHBsdXMuY28vYXBrCi5hbmR5Z29kLmNvbQp8aHR0cDovL2FuZHlnb2Qu +Y29tCmFubmF0YW0uY29tL2NoaW5lc2UKfHxhbmNob3IuZm0KfHxhbmNob3JmcmVl +LmNvbQohLS1HSFMKfHxhbmNzY29uZi5vcmcKfHxhbmRmYXJhd2F5Lm5ldAp8fGFu +ZHJvaWQteDg2Lm9yZwphbmdlbGZpcmUuY29tL2hpL2hheWFzaGkKfHxhbmd1bGFy +anMub3JnCmFuaW1lY3JhenkubmV0CmFuaXNjYXJ0dWpvLmNvbQp8fGFuaXNjYXJ0 +dWpvLmNvbQp8fGFub2JpaS5jb20KLmFub255bWl0eW5ldHdvcmsuY29tCi5hbm9u +eW1pemVyLmNvbQouYW5vbnltb3VzZS5vcmcKfHxhbm9ueW1vdXNlLm9yZwphbm9u +dGV4dC5jb20KLmFucG9wby5jb20KLmFuc3dlcmluZy1pc2xhbS5vcmcKfGh0dHA6 +Ly93d3cuYW50ZC5vcmcKfHxhbnRob255Y2FsemFkaWxsYS5jb20KLmFudGkxOTg0 +LmNvbQphbnRpY2hyaXN0ZW5kb20uY29tCi5hbnRpd2F2ZS5uZXQKfGh0dHA6Ly9h +bnRpd2F2ZS5uZXQKLmFueXBvcm4uY29tCi5hbnlzZXguY29tCnxodHRwOi8vYW55 +c2V4LmNvbQouYW8zLm9yZwp8fGFvMy5vcmcKfHxhb2JvLmNvbS5hdQouYW9mcmll +bmQuY29tCnxodHRwOi8vYW9mcmllbmQuY29tCi5hb2ZyaWVuZC5jb20uYXUKLmFv +amlhby5vcmcKfHxhb21pd2FuZy5jb20KdmlkZW8uYXAub3JnCnx8YXBhdDE5ODku +b3JnCi5hcGV0dWJlLmNvbQp8fGFwaWFyeS5pbwouYXBpZ2VlLmNvbQp8fGFwaWdl +ZS5jb20KfHxhcGsuc3VwcG9ydAp8fGFway1kbC5jb20KfHxhcGtjb21iby5jb20K +LmFwa21vbmsuY29tL2FwcAp8fGFwa21vbmsuY29tCnx8YXBrcGx6LmNvbQphcGtw +dXJlLmNvbQp8fGFwa3B1cmUuY29tCi5hcGx1c3Zwbi5jb20KIS0tfHxhcHBhbm5p +ZS5jb20KfHxhcHBicmFpbi5jb20KLmFwcGRvd25sb2FkZXIubmV0L0FuZHJvaWQK +LmFwcGxlZGFpbHkuY29tCnx8YXBwbGVkYWlseS5jb20KYXBwbGVkYWlseS5jb20u +aGsKfHxhcHBsZWRhaWx5LmNvbS5oawphcHBsZWRhaWx5LmNvbS50dwp8fGFwcGxl +ZGFpbHkuY29tLnR3Ci5hcHBzaG9wcGVyLmNvbQp8aHR0cDovL2FwcHNob3BwZXIu +Y29tCnx8YXBwc29ja3MubmV0Cnx8YXBwc3RvLnJlCi5hcHRvaWRlLmNvbQp8fGFw +dG9pZGUuY29tCnx8YXJjaGl2ZXMuZ292Ci5hcmNoaXZlLmZvCnx8YXJjaGl2ZS5m +bwouYXJjaGl2ZS5pcwp8fGFyY2hpdmUuaXMKLmFyY2hpdmUubGkKfHxhcmNoaXZl +LmxpCnx8YXJjaGl2ZS5vcmcKfHxhcmNoaXZlLnBoCmFyY2hpdmUudG9kYXkKfGh0 +dHBzOi8vYXJjaGl2ZS50b2RheQp8fGFyY2hpdmVvZm91cm93bi5jb20KfHxhcmNo +aXZlb2ZvdXJvd24ub3JnCi5hcmN0b3NpYS5jb20KfGh0dHA6Ly9hcmN0b3NpYS5j +b20KfHxhcmVjYS1iYWNrdXAub3JnCi5hcmV0aHVzYS5zdQp8fGFyZXRodXNhLnN1 +Cnx8YXJsaW5ndG9uY2VtZXRlcnkubWlsCnx8YXJteS5taWwKLmFydDR0aWJldDE5 +OTgub3JnCmFydG9mcGVhY2Vmb3VuZGF0aW9uLm9yZwphcnRzeS5uZXQKfHxhc2Fj +cC5vcmcKYXNkZmcuanAvZGFicgphc2cudG8KLmFzaWEtZ2FtaW5nLmNvbQouYXNp +YWhhcnZlc3Qub3JnCnx8YXNpYWhhcnZlc3Qub3JnCnx8YXNpYW5hZ2UuY29tCnx8 +YXNpYW5ld3MuaXQKfGh0dHA6Ly9qYXBhbmZpcnN0LmFzaWFuZnJlZWZvcnVtLmNv +bS8KfHxhc2lhbnNleGRpYXJ5LmNvbQp8fGFzaWFud29tZW5zZmlsbS5kZQp8fGFz +aWFvbmUuY29tCi5hc2lhdGdwLmNvbQouYXNpYXRvZGF5LnVzCnx8YXNrc3R1ZGVu +dC5jb20KLmFza3luei5uZXQKfHxhc2t5bnoubmV0Cnx8YXNwaS5vcmcuYXUKfHxh +c3Bpc3RyYXRlZ2lzdC5vcmcuYXUKfHxhc3NlbWJsYS5jb20KfHxhc3RyaWxsLmNv +bQp8fGF0Yy5vcmcuYXUKLmF0Y2hpbmVzZS5jb20KfGh0dHA6Ly9hdGNoaW5lc2Uu +Y29tCmF0Z2Z3Lm9yZwouYXRsYXNwb3N0LmNvbQp8fGF0bGFzcG9zdC5jb20KfHxh +dGRtdC5jb20KLmF0bGFudGExNjguY29tCnx8YXRsYW50YTE2OC5jb20KLmF0bmV4 +dC5jb20KfHxhdG5leHQuY29tCmljZS5hdWRpb25vdy5jb20KLmF2LmNvbQp8fGF2 +Lm1vdmllCi5hdi1lLWJvZHkuY29tCmF2YWF6Lm9yZwp8fGF2YWF6Lm9yZwohLS18 +fGF2YXN0LmNvbQouYXZib2R5LnR2Ci5hdmNpdHkudHYKLmF2Y29vbC5jb20KLmF2 +ZGIuaW4KfHxhdmRiLmluCi5hdmRiLnR2Cnx8YXZkYi50dgouYXZmYW50YXN5LmNv +bQp8fGF2Zy5jb20KLmF2Z2xlLmNvbQp8fGF2Z2xlLmNvbQp8fGF2aWRlbXV4Lm9y +Zwp8fGF2b2lzaW9uLmNvbQouYXZ5YWhvby5jb20KfHxheGlvcy5jb20KfHxheHVy +ZWZvcm1hYy5jb20KLmF6ZXJiYXljYW4udHYKYXplcmltaXguY29tCiEtLWJveHVu +LmF6dXJld2Vic2l0ZXMubmV0IGRvZXNuJ3QgZXhpc3QuCmJveHVuKi5henVyZXdl +YnNpdGVzLm5ldAp8fGJveHVuKi5henVyZXdlYnNpdGVzLm5ldAoKIS0tLS0tLS0t +LS0tLS0tLS0tLS0tQkItLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnx8Yi1vay5j +Ywpmb3J1bS5iYWJ5LWtpbmdkb20uY29tCnx8YmFieWxvbmJlZS5jb20KYmFieW5l +dC5jb20uaGsKYmFja2NoaW5hLmNvbQp8fGJhY2tjaGluYS5jb20KLmJhY2twYWNr +ZXJzLmNvbS50dy9mb3J1bQpiYWNrdG90aWFuYW5tZW4uY29tCi5iYWRpdWNhby5j +b20KfHxiYWRpdWNhby5jb20KLmJhZGpvam8uY29tCmJhZG9vLmNvbQp8aHR0cDov +LyoyLmJhaGFtdXQuY29tLnR3Cnx8YmFpZHUuanAKLmJhaWppZS5vcmcKfGh0dHA6 +Ly9iYWlqaWUub3JnCnx8YmFpbGFuZGFpbHkuY29tCnx8YmFpeGluZy5tZQp8fGJh +a2dlZWtob21lLnRrCi5iYW5hbmEtdnBuLmNvbQp8fGJhbmFuYS12cG4uY29tCi5i +YW5kLnVzCnx8YmFuZGNhbXAuY29tCi5iYW5kd2Fnb25ob3N0LmNvbQp8fGJhbmR3 +YWdvbmhvc3QuY29tCi5iYW5nYnJvc25ldHdvcmsuY29tCi5iYW5nY2hlbi5uZXQK +fGh0dHA6Ly9iYW5nY2hlbi5uZXQKfHxiYW5na29rcG9zdC5jb20KfHxiYW5neW91 +bGF0ZXIuY29tCmJhbm5lZGJvb2sub3JnCnx8YmFubmVkYm9vay5vcmcKLmJhbm5l +ZG5ld3Mub3JnCi5iYXJhbWFuZ2FvbmxpbmUuY29tCnxodHRwOi8vYmFyYW1hbmdh +b25saW5lLmNvbQouYmFyZW5ha2VkaXNsYW0uY29tCnx8YmFybmFidS5jby51awp8 +fGJhcnRvbi5kZQouYmFzdGlsbGVwb3N0LmNvbQp8fGJhc3RpbGxlcG9zdC5jb20K +YmF5dm9pY2UubmV0Cnx8YmF5dm9pY2UubmV0CmRhanVzaGEuYmF5d29yZHMuY29t +Cnx8YmJjaGF0LnR2Cnx8YmItY2hhdC50dgouYmJnLmdvdgouYmJrei5jb20vZm9y +dW0KLmJibnJhZGlvLm9yZwpiYnMtdHcuY29tCi5iYnNkaWdlc3QuY29tL3RocmVh +ZAp8fGJic2ZlZWQuY29tCmJic2xhbmQuY29tCi5iYnNtby5jb20KLmJic29uZS5j +b20KYmJ0b3lzdG9yZS5jb20KLmJjYXN0LmNvLm56Ci5iY2MuY29tLnR3L2JvYXJk +Ci5iY2NoaW5lc2UubmV0Ci5iY21vcm5pbmcuY29tCmJkc212aWRlb3MubmV0Ci5i +ZWFjb25ldmVudHMuY29tCi5iZWJvLmNvbQp8fGJlYm8uY29tCi5iZWV2cG4uY29t +Cnx8YmVldnBuLmNvbQouYmVoaW5ka2luay5jb20KfHxiZWlqaW5nMTk4OS5jb20K +fHxiZWlqaW5nMjAyMi5hcnQKYmVpamluZ3NwcmluZy5jb20KfHxiZWlqaW5nc3By +aW5nLmNvbQouYmVpamluZ3p4Lm9yZwp8aHR0cDovL2JlaWppbmd6eC5vcmcKLmJl +bGFtaW9ubGluZS5jb20KLmJlbGwud2lraQp8aHR0cDovL2JlbGwud2lraQpiZW15 +d2lmZS5jYwpiZXJpYy5tZQp8fGJlcmxpbmVyYmVyaWNodC5kZQouYmVybGludHdp +dHRlcndhbGwuY29tCnx8YmVybGludHdpdHRlcndhbGwuY29tCi5iZXJtLmNvLm56 +Ci5iZXN0Zm9yY2hpbmEub3JnCnx8YmVzdGZvcmNoaW5hLm9yZwouYmVzdGdvcmUu +Y29tCi5iZXN0cG9ybnN0YXJkYi5jb20KfHxiZXN0dnBuLmNvbQouYmVzdHZwbmFu +YWx5c2lzLmNvbQouYmVzdHZwbnNlcnZlci5jb20KLmJlc3R2cG5zZXJ2aWNlLmNv +bQouYmVzdHZwbnVzYS5jb20KfHxiZXQzNjUuY29tCi5iZXRmYWlyLmNvbQp8fGJl +dHRlcm5ldC5jbwouYmV0dGVydnBuLmNvbQp8fGJldHRlcnZwbi5jb20KLmJldHR3 +ZWVuLmNvbQp8fGJldHR3ZWVuLmNvbQp8fGJldHZpY3Rvci5jb20KLmJld3d3Lm5l +dAouYmV5b25kZmlyZXdhbGwuY29tCnx8YmZubi5vcmcKfHxiZnNoLmhrCi5iZ3Zw +bi5jb20KfHxiZ3Zwbi5jb20KLmJpYW5sZWkuY29tCkBAfHxiaWFubGVpLmNvbQpi +aWFudGFpbGFqaWFvLmNvbQpiaWFudGFpbGFqaWFvLmluCi5iaWJsZXNmb3JhbWVy +aWNhLm9yZwp8aHR0cDovL2JpYmxlc2ZvcmFtZXJpY2Eub3JnCi5iaWMyMDExLm9y +Zwp8fGJpZWRpYW4ubWUKYmlnZm9vbHMuY29tCnx8YmlnamFwYW5lc2VzZXguY29t +Ci5iaWduZXdzLm9yZwp8fGJpZ25ld3Mub3JnCi5iaWdzb3VuZC5vcmcKfHxiaWxk +LmRlCi5iaWxpd29ybGQuY29tCnxodHRwOi8vYmlsaXdvcmxkLmNvbQp8aHR0cDov +L2JpbGx5cGFuLmNvbS93aWtpCi5iaW51eC5tZQphaS5iaW53YW5nLm1lL2NvdXBs +ZXQKLmJpdC5kbwp8aHR0cDovL2JpdC5kbwouYml0Lmx5CnxodHRwOi8vYml0Lmx5 +CiEtLXx8Yml0YnVja2V0Lm9yZwp8fGJpdGNodXRlLmNvbQp8fGJpdGNvaW50YWxr +Lm9yZwouYml0c2hhcmUuY29tCnx8Yml0c2hhcmUuY29tCmJpdHNub29wLmNvbQou +Yml0dmlzZS5jb20KfHxiaXR2aXNlLmNvbQpiaXpoYXQuY29tCnx8YmwtZG91amlu +c291a28uY29tCi5iam5ld2xpZmUub3JnCi5ianMub3JnCmJqemMub3JnCnx8Ymp6 +Yy5vcmcKLmJsYWNrbG9naWMuY29tCi5ibGFja3Zwbi5jb20KfHxibGFja3Zwbi5j +b20KYmxld3Bhc3MuY29tCnRvci5ibGluZ2JsaW5nc3F1YWQubmV0Ci5ibGlua3gu +Y29tCnx8Ymxpbmt4LmNvbQpibGludy5jb20KLmJsaXAudHYKfHxibGlwLnR2Lwp8 +fGJsb2NrY2FzdC5pdAouYmxvY2tjbi5jb20KfHxibG9ja2NuLmNvbQp8fGJsb2Nr +ZWRieWhrLmNvbQp8fGJsb2NrbGVzcy5jb20KfHxibG9nLmRlCi5ibG9nLmpwCnxo +dHRwOi8vYmxvZy5qcApAQHx8anB1c2guY24KLmJsb2djYXRhbG9nLmNvbQp8fGJs +b2djYXRhbG9nLmNvbQp8fGJsb2djaXR5Lm1lCi5ibG9nZ2VyLmNvbQp8fGJsb2dn +ZXIuY29tCmJsb2dpbWcuanAKfHxibG9nLmthbmd5ZS5vcmcKLmJsb2dsaW5lcy5j +b20KfHxibG9nbGluZXMuY29tCnx8YmxvZ2xvdmluLmNvbQpyY29udmVyc2F0aW9u +LmJsb2dzLmNvbQpibG9ndGQubmV0Ci5ibG9ndGQub3JnCnxodHRwOi8vYmxvZ3Rk +Lm9yZwp8fGJsb29kc2hlZC5uZXQKIS0tNDAzCnx8YXNzZXRzLmJ3YnguaW8KCnx8 +Ymxvb21mb3J0dW5lLmNvbQpibHVlYW5nZWxsaXZlLmNvbQp8fGJsdWJycnkuY29t +Ci5ibWZpbm4uY29tCi5ibmV3cy5jbwp8fGJuZXdzLmNvCnx8Ym5leHQuY29tLnR3 +Cnx8Ym5ybWV0YWwuY29tCmJvYXJkcmVhZGVyLmNvbS90aHJlYWQKfHxib2FyZHJl +YWRlci5jb20KLmJvZC5hc2lhCnx8Ym9kLmFzaWEKLmJvZG9nODguY29tCi5ib2xl +aHZwbi5uZXQKfHxib2xlaHZwbi5uZXQKYm9uYm9ubWUuY29tCi5ib25ib25zZXgu +Y29tCi5ib25mb3VuZGF0aW9uLm9yZwouYm9uZ2FjYW1zLmNvbQp8fGJvb2JzdGFn +cmFtLmNvbQp8fGJvb2suY29tLnR3Cnx8Ym9va2RlcG9zaXRvcnkuY29tCmJvb2tl +cHViLmNvbQp8fGJvb2tzLmNvbS50dwp8fGJvcmdlbm1hZ2F6aW5lLmNvbQp8fGJv +dGFud2FuZy5jb20KLmJvdC5udQouYm93ZW5wcmVzcy5jb20KfHxib3dlbnByZXNz +LmNvbQp8fGFwcC5ib3guY29tCmRsLmJveC5uZXQKfHxkbC5ib3gubmV0Ci5ib3hw +bi5jb20KfHxib3hwbi5jb20KYm94dW4uY29tCnx8Ym94dW4uY29tCi5ib3h1bi50 +dgp8fGJveHVuLnR2CmJveHVuYmxvZy5jb20KfHxib3h1bmJsb2cuY29tCi5ib3h1 +bmNsdWIuY29tCmJveWFuZ3UuY29tCi5ib3lmcmllbmR0di5jb20KLmJveXNmb29k +LmNvbQp8fGJyLnN0Ci5icmFpbnlxdW90ZS5jb20vcXVvdGVzL2F1dGhvcnMvZC9k +YWxhaV9sYW1hCnx8YnJhbmRvbmh1dGNoaW5zb24uY29tCnx8YnJhdW1laXN0ZXIu +b3JnCnx8YnJhdmUuY29tCi5icmF2b3R1YmUubmV0Cnx8YnJhdm90dWJlLm5ldAou +YnJhenplcnMuY29tCnx8YnJhenplcnMuY29tCnx8YnJlYWNoZWQudG8KLmJyZWFr +LmNvbQp8fGJyZWFrLmNvbQpicmVha2dmdy5jb20KfHxicmVha2dmdy5jb20KYnJl +YWtpbmc5MTEuY29tCi5icmVha2luZ3R3ZWV0cy5jb20KfHxicmVha2luZ3R3ZWV0 +cy5jb20KfHxicmVha3dhbGwubmV0CmJyaWlhbi5jb20vNjUxMS9mcmVlZ2F0ZQou +YnJpZWZkcmVhbS5jb20vJUU3JUI0JUEwJUU2JUEzJUJBCnx8YnJpbGwuY29tCmJy +aXp6bHkuY29tCnx8YnJpenpseS5jb20KfHxicmttZC5jb20KYnJvYWRib29rLmNv +bQouYnJvYWRwcmVzc2luYy5jb20KfHxicm9hZHByZXNzaW5jLmNvbQpiYnMuYnJv +Y2tiYnMuY29tCnx8YnJvb2tpbmdzLmVkdQpicnVjZXdhbmcubmV0Ci5icnV0YWx0 +Z3AuY29tCnx8YnJ1dGFsdGdwLmNvbQouYnQybWFnLmNvbQp8fGJ0OTUuY29tCi5i +dGFpYS5jb20KLmJ0YnRhdi5jb20KfHxidGRpZy5jb20KfHxidGRpZ2cub3JnCi5i +dGt1Lm1lCnx8YnRrdS5tZQp8fGJ0a3Uub3JnCi5idHNwcmVhZC5jb20KLmJ0c3lu +Y2tleXMuY29tCi5idWRhZWR1Lm9yZwp8fGJ1ZGFlZHUub3JnCi5idWRkaGFuZXQu +Y29tLnR3L3pmcm9wL3RpYmV0Cnx8YnVmZmVyZWQuY29tCnx8YnVsbGd1YXJkLmNv +bQouYnVsbG9nLm9yZwp8fGJ1bGxvZy5vcmcKLmJ1bGxvZ2dlci5jb20KfHxidWxs +b2dnZXIuY29tCnx8YnVuYnVuaGsuY29tCi5idXNheWFyaS5jb20KfGh0dHA6Ly9i +dXNheWFyaS5jb20KfHxidXNpbmVzcy1odW1hbnJpZ2h0cy5vcmcKLmJ1c2luZXNz +aW5zaWRlci5jb20vYmluZy1jb3VsZC1iZS1jZW5zb3Jpbmctc2VhcmNoLXJlc3Vs +dHMtMjAxNAouYnVzaW5lc3NpbnNpZGVyLmNvbS9jaGluYS1iYW5rcy1wcmVwYXJp +bmctZm9yLWRlYnQtaW1wbG9zaW9uLTIwMTQKLmJ1c2luZXNzaW5zaWRlci5jb20v +aG9uZy1rb25nLWFjdGl2aXN0cy1kZWZ5LXBvbGljZS10ZWFyLWdhcy1hcy1wcm90 +ZXN0cy1jb250aW51ZS1vdmVybmlnaHQtMjAxNAouYnVzaW5lc3NpbnNpZGVyLmNv +bS9pbnRlcm5ldC1vdXRhZ2VzLXJlcG9ydGVkLWluLW5vcnRoLWtvcmVhLTIwMTQK +LmJ1c2luZXNzaW5zaWRlci5jb20vaXBob25lLTYtaXMtYXBwcm92ZWQtZm9yLXNh +bGUtaW4tY2hpbmEtMjAxNAouYnVzaW5lc3NpbnNpZGVyLmNvbS9uZmwtYW5ub3Vu +Y2Vycy1zdXJmYWNlLXRhYmxldHMtMjAxNAouYnVzaW5lc3NpbnNpZGVyLmNvbS9w +YW5hbWEtcGFwZXJzCi5idXNpbmVzc2luc2lkZXIuY29tL3VtYnJlbGxhLW1hbi1o +b25nLWtvbmctMjAxNAp8aHR0cDovL3d3dy5idXNpbmVzc2luc2lkZXIuY29tLmF1 +LyoKLmJ1c2luZXNzdG9kYXkuY29tLnR3Cnx8YnVzaW5lc3N0b2RheS5jb20udHcK +LmJ1c3Uub3JnL25ld3MKfGh0dHA6Ly9idXN1Lm9yZy9uZXdzCmJ1c3l0cmFkZS5j +b20KLmJ1dWdhYS5jb20KLmJ1enpoYW5kLmNvbQouYnV6emhhbmQubmV0Ci5idXp6 +b3JhbmdlLmNvbQp8fGJ1enpvcmFuZ2UuY29tCnx8YnZwbi5jb20KfHxid2gxLm5l +dApid3NqLmhrCnx8YngudGwKfHxieXBhc3NjZW5zb3JzaGlwLm9yZwoKIS0tLS0t +LS0tLS0tLS0tLS0tLS0tQ0MtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnx8Yy1z +cGFuLm9yZwouYy1zcGFudmlkZW8ub3JnCnx8Yy1zcGFudmlkZW8ub3JnCnx8Yy1l +c3Qtc2ltcGxlLmNvbQouYzEwMHRpYmV0Lm9yZwp8fGNhYmxlZ2F0ZXNlYXJjaC5u +ZXQKLmNhY2hpbmVzZS5jb20KLmNhY253LmNvbQp8aHR0cDovL2NhY253LmNvbQou +Y2FjdHVzdnBuLmNvbQp8fGNhY3R1c3Zwbi5jb20KLmNhZmVwcmVzcy5jb20KLmNh +aHIub3JnLnR3Ci5jYWlqaW5nbGVuZ3lhbi5jb20KfHxjYWlqaW5nbGVuZ3lhbi5j +b20KLmNhbGFtZW8uY29tL2Jvb2tzCi5jYWxnYXJ5Y2hpbmVzZS5jYQouY2FsZ2Fy +eWNoaW5lc2UuY29tCi5jYWxnYXJ5Y2hpbmVzZS5uZXQKfGh0dHA6Ly9ibG9nLmNh +bGlicmUtZWJvb2suY29tCmZhbHVuLmNhbHRlY2guZWR1Ci5pdHMuY2FsdGVjaC5l +ZHUvfmZhbHVuLwouY2FtNC5jb20KLmNhbTQuanAKLmNhbTQuc2cKLmNhbWZyb2cu +Y29tCnx8Y2FtZnJvZy5jb20KfHxjYW1wYWlnbmZvcnV5Z2h1cnMub3JnCnx8Y2Ft +cy5jb20KLmNhbXMub3JnLnNnCmNhbmFkYW1lZXQuY29tCi5jYW5hbHBvcm5vLmNv +bQp8aHR0cDovL2Jicy5jYW50b25lc2UuYXNpYS8KIS0taHR0cDovL3d3dy5jYW50 +b25lc2UuYXNpYS9hY3Rpb24tYmJzLmh0bWwKLmNhbnl1Lm9yZwp8fGNhbnl1Lm9y +ZwouY2FvLmltCi5jYW9iaWFuLmluZm8KfHxjYW9iaWFuLmluZm8KY2FvY2hhbmdx +aW5nLmNvbQp8fGNhb2NoYW5ncWluZy5jb20KLmNhcC5vcmcuaGsKfHxjYXAub3Jn +LmhrCi5jYXJhYmluYXN5cGlzdG9sYXMuY29tCmNhcmRpbmFsa3VuZ2ZvdW5kYXRp +b24ub3JnCnx8cG9zdHMuY2FyZWVyZW5naW5lLnVzCmNhcm1vdG9yc2hvdy5jb20K +fHxjYXJyZC5jbwpzcy5jYXJyeXpob3UuY29tCi5jYXJ0b29ubW92ZW1lbnQuY29t +Cnx8Y2FydG9vbm1vdmVtZW50LmNvbQouY2FzYWRlbHRpYmV0YmNuLm9yZwouY2Fz +YXRpYmV0Lm9yZy5teAp8aHR0cDovL2Nhc2F0aWJldC5vcmcubXgKLmNhcmkuY29t +Lm15Cnx8Y2FyaS5jb20ubXkKfHxjYXJpYmJlYW5jb20uY29tCi5jYXNpbm9raW5n +LmNvbQouY2FzaW5vcml2YS5jb20KfHxjYXRjaDIyLm5ldAouY2F0Y2hnb2QuY29t +CnxodHRwOi8vY2F0Y2hnb2QuY29tCnx8Y2F0ZmlnaHRwYXlwZXJ2aWV3Lnh4eAou +Y2F0aG9saWMub3JnLmhrCnx8Y2F0aG9saWMub3JnLmhrCmNhdGhvbGljLm9yZy50 +dwp8fGNhdGhvbGljLm9yZy50dwouY2F0aHZvaWNlLm9yZy50dwp8fGNhdG8ub3Jn +Cnx8Y2F0dHQuY29tCi5jYmMuY2EKfHxjYmMuY2EKLmNic25ld3MuY29tL3ZpZGVv +Ci5jYnRjLm9yZy5oawp8fHNvdXRocGFyay5jYy5jb20KIS0uY2NjLmRlCiEtfHxj +Y2MuZGUKfHxjY2NhdC5jYwp8fGNjY2F0LmNvCi5jY2R0ci5vcmcKfHxjY2R0ci5v +cmcKLmNjaGVyZS5jb20KfHxjY2hlcmUuY29tCi5jY2ltLm9yZwouY2NsaWZlLmNh +CmNjbGlmZS5vcmcKfHxjY2xpZmUub3JnCmNjbGlmZWZsLm9yZwp8fGNjbGlmZWZs +Lm9yZwouY2N0aGVyZS5jb20KfHxjY3RoZXJlLmNvbQp8fGNjdGhlcmUubmV0Ci5j +Y3Rtd2ViLm5ldAouY2N0b25nYmFvLmNvbS9hcnRpY2xlLzIwNzg3MzIKY2N1ZS5j +YQpjY3VlLmNvbQouY2N2b2ljZS5jYQouY2N3Lm9yZy50dwouY2dkZXBvdC5vcmcK +fGh0dHA6Ly9jZ2RlcG90Lm9yZwp8fGNkYm9vay5vcmcKLmNkY3BhcnR5LmNvbQou +Y2RlZi5vcmcKfHxjZGVmLm9yZwp8fGNkaWcuaW5mbwpjZGpwLm9yZwp8fGNkanAu +b3JnCiEtLS5jZG4tYXBwbGUuY29tCiEtLXx8Y2RuLWFwcGxlLmNvbQouY2RuZXdz +LmNvbS50dwpjZHAxOTg5Lm9yZwpjZHAxOTk4Lm9yZwp8fGNkcDE5OTgub3JnCmNk +cDIwMDYub3JnCnx8Y2RwMjAwNi5vcmcKLmNkcGEudXJsLnR3CmNkcGV1Lm9yZwpj +ZHB1c2Eub3JnCmNkcHdlYi5vcmcKfHxjZHB3ZWIub3JnCmNkcHd1Lm9yZwp8fGNk +cHd1Lm9yZwp8fGNkdy5jb20KLmNlY2MuZ292Cnx8Y2VjYy5nb3YKfHxjZWxsdWxv +LmluZm8KfHxjZW5ld3MuZXUKfHxjZW50ZXJmb3JodW1hbnJlcHJvZC5jb20KfHxj +ZW50cmFsbmF0aW9uLmNvbQouY2VudHVyeXMubmV0CnxodHRwOi8vY2VudHVyeXMu +bmV0Ci5jZmhrcy5vcmcuaGsKLmNmb3MuZGUKfHxjZnIub3JnCi5jZnRmYy5jb20K +LmNnc3QuZWR1Ci5jaGFuZ2Uub3JnCnx8Y2hhbmdlLm9yZwouY2hhbmdwLmNvbQp8 +fGNoYW5ncC5jb20KLmNoYW5nc2EubmV0CnxodHRwOi8vY2hhbmdzYS5uZXQKfHxj +aGFubmVsbmV3c2FzaWEuY29tCi5jaGFwbTI1LmNvbQouY2hhdHVyYmF0ZS5jb20K +fHxjaGF0dXJiYXRlLmNvbQouY2h1YW5nLXllbi5vcmcKfHxjaGVja2dmdy5jb20K +Y2hlbmdtaW5nbWFnLmNvbQouY2hlbmd1YW5nY2hlbmcuY29tCnx8Y2hlbmd1YW5n +Y2hlbmcuY29tCi5jaGVucG9rb25nLmNvbQp8fGNoZW5wb2tvbmcuY29tCi5jaGVu +cG9rb25nLm5ldAp8aHR0cDovL2NoZW5wb2tvbmcubmV0Cnx8Y2hlbnBva29uZ3Zp +cC5jb20KfHxjaGVycnlzYXZlLmNvbQouY2hob25nYmkub3JnCmNoaWNhZ29uY210 +di5jb20KfGh0dHA6Ly9jaGljYWdvbmNtdHYuY29tCi5jaGluYS13ZWVrLmNvbQpj +aGluYTEwMS5jb20KfHxjaGluYTEwMS5jb20KfHxjaGluYTE4Lm9yZwp8fGNoaW5h +MjEuY29tCmNoaW5hMjEub3JnCnx8Y2hpbmEyMS5vcmcKLmNoaW5hNTAwMC51cwpj +aGluYWFmZmFpcnMub3JnCnx8Y2hpbmFhZmZhaXJzLm9yZwp8fGNoaW5hYWlkLm1l +CmNoaW5hYWlkLnVzCmNoaW5hYWlkLm9yZwpjaGluYWFpZC5uZXQKfHxjaGluYWFp +ZC5uZXQKY2hpbmFjb21tZW50cy5vcmcKfHxjaGluYWNvbW1lbnRzLm9yZwouY2hp +bmFjaGFuZ2Uub3JnCnx8Y2hpbmFjaGFuZ2Uub3JnCmNoaW5hY2hhbm5lbC5oawp8 +fGNoaW5hY2hhbm5lbC5oawouY2hpbmFjaXR5bmV3cy5iZQouY2hpbmFkaWFsb2d1 +ZS5uZXQKLmNoaW5hZGlnaXRhbHRpbWVzLm5ldAp8fGNoaW5hZGlnaXRhbHRpbWVz +Lm5ldAouY2hpbmFlbGVjdGlvbnMub3JnCnx8Y2hpbmFlbGVjdGlvbnMub3JnCi5j +aGluYWV3ZWVrbHkuY29tCnx8Y2hpbmFld2Vla2x5LmNvbQp8fGNoaW5hZnJlZXBy +ZXNzLm9yZwouY2hpbmFnYXRlLmNvbQpjaGluYWdlZWtzLm9yZwpjaGluYWdmdy5v +cmcKfHxjaGluYWdmdy5vcmcKLmNoaW5hZ29uZXQuY29tCi5jaGluYWdyZWVucGFy +dHkub3JnCnx8Y2hpbmFncmVlbnBhcnR5Lm9yZwouY2hpbmFob3Jpem9uLm9yZwp8 +fGNoaW5haG9yaXpvbi5vcmcKLmNoaW5haHVzaC5jb20KLmNoaW5haW5wZXJzcGVj +dGl2ZS5jb20KfHxjaGluYWludGVyaW1nb3Yub3JnCmNoaW5hbGFib3J3YXRjaC5v +cmcKY2hpbmFsYXd0cmFuc2xhdGUuY29tCi5jaGluYXBvc3QuY29tLnR3L3RhaXdh +bi9uYXRpb25hbC9uYXRpb25hbC1uZXdzCmNoaW5heGNoaW5hLmNvbS9ob3d0bwpj +aGluYWxhd2FuZHBvbGljeS5jb20KLmNoaW5hbXVsZS5jb20KfHxjaGluYW11bGUu +Y29tCmNoaW5hbXoub3JnCi5jaGluYW5ld3NjZW50ZXIuY29tCnxodHRwczovL2No +aW5hbmV3c2NlbnRlci5jb20KLmNoaW5hcHJlc3MuY29tLm15Cnx8Y2hpbmFwcmVz +cy5jb20ubXkKLmNoaW5hLXJldmlldy5jb20udWEKfGh0dHA6Ly9jaGluYS1yZXZp +ZXcuY29tLnVhCi5jaGluYXJpZ2h0c2lhLm9yZwpjaGluYXNtaWxlLm5ldC9mb3J1 +bXMKY2hpbmFzb2NpYWxkZW1vY3JhdGljcGFydHkuY29tCnx8Y2hpbmFzb2NpYWxk +ZW1vY3JhdGljcGFydHkuY29tCmNoaW5hc291bC5vcmcKfHxjaGluYXNvdWwub3Jn +Ci5jaGluYXN1Y2tzLm5ldAp8fGNoaW5hdG9wc2V4LmNvbQouY2hpbmF0b3duLmNv +bS5hdQpjaGluYXR3ZWVwcy5jb20KY2hpbmF3YXkub3JnCi5jaGluYXdvcmtlci5p +bmZvCnx8Y2hpbmF3b3JrZXIuaW5mbwpjaGluYXlvdXRoLm9yZy5oawpjaGluYXl1 +YW5taW4ub3JnCnx8Y2hpbmF5dWFubWluLm9yZwouY2hpbmVzZS1oZXJtaXQubmV0 +CmNoaW5lc2UtbGVhZGVycy5vcmcKY2hpbmVzZS1tZW1vcmlhbC5vcmcKLmNoaW5l +c2VkYWlseS5jb20KfHxjaGluZXNlZGFpbHluZXdzLmNvbQouY2hpbmVzZWRlbW9j +cmFjeS5jb20KfHxjaGluZXNlZGVtb2NyYWN5LmNvbQp8fGNoaW5lc2VnYXkub3Jn +Ci5jaGluZXNlbi5kZQp8fGNoaW5lc2VuLmRlCi5jaGluZXNlbmV3cy5uZXQuYXUv +Ci5jaGluZXNlcGVuLm9yZwp8fGNoaW5lc2VyYWRpb3NlYXR0bGUuY29tCi5jaGlu +ZXNldGFsa3MubmV0L2NoCnx8Y2hpbmVzZXVwcmVzcy5jb20KLmNoaW5nY2hlb25n +LmNvbQp8fGNoaW5nY2hlb25nLmNvbQouY2hpbm1hbi5uZXQKfGh0dHA6Ly9jaGlu +bWFuLm5ldApjaGl0aHUub3JnCnx8Y25uZXdzLmNob3N1bi5jb20KLmNocmRuZXQu +Y29tCnxodHRwOi8vY2hyZG5ldC5jb20KLmNocmlzdGlhbmZyZWVkb20ub3JnCnx8 +Y2hyaXN0aWFuZnJlZWRvbS5vcmcKY2hyaXN0aWFuc3R1ZHkuY29tCnx8Y2hyaXN0 +aWFuc3R1ZHkuY29tCmNocmlzdHVzcmV4Lm9yZy93d3cxL3NkYwouY2h1Ym9sZC5j +b20KY2h1YnVuLmNvbQp8fGNocmlzdGlhbnRpbWVzLm9yZy5oawouY2hybGF3eWVy +cy5oawp8fGNocmxhd3llcnMuaGsKLmNodXJjaGluaG9uZ2tvbmcub3JnL2I1L2lu +ZGV4LnBocAp8aHR0cDovL2NodXJjaGluaG9uZ2tvbmcub3JnL2I1L2luZGV4LnBo +cAouY2h1c2hpZ2FuZ2RydWcuY2gKLmNpZW5lbi5jb20KLmNpbmVhc3RlbnRyZWZm +LmRlCi5jaXBmZy5vcmcKfHxjaXJjbGV0aGViYXlmb3J0aWJldC5vcmcKfHxjaXJv +c2FudGlsbGkuY29tCi5jaXRpemVuY24uY29tCnx8Y2l0aXplbmNuLmNvbQp8fGNp +dGl6ZW5sYWIuY2EKfHxjaXRpemVubGFiLm9yZwp8fGNpdGl6ZW5zY29tbWlzc2lv +bi5oawouY2l0aXplbmxhYi5vcmcKY2l0aXplbnNyYWRpby5vcmcKLmNpdHkzNjUu +Y2EKfGh0dHA6Ly9jaXR5MzY1LmNhCmNpdHk5eC5jb20KfHxjaXR5cG9wdWxhdGlv +bi5kZQouY2l0eXRhbGsudHcvZXZlbnQKLmNpdmljcGFydHkuaGsKfHxjaXZpY3Bh +cnR5LmhrCi5jaXZpbGRpc29iZWRpZW5jZW1vdmVtZW50Lm9yZwpjaXZpbGhyZnJv +bnQub3JnCnx8Y2l2aWxocmZyb250Lm9yZwouY2l2aWxpYW5ndW5uZXIuY29tCi5j +aXZpbG1lZGlhLnR3Cnx8Y2l2aWxtZWRpYS50dwpwc2lwaG9uLmNpdmlzZWMub3Jn +Cnx8dnBuLmNqYi5uZXQKLmNrMTAxLmNvbQp8fGNrMTAxLmNvbQouY2xhcmlvbnBy +b2plY3Qub3JnL25ld3MvaXNsYW1pYy1zdGF0ZS1pc2lzLWlzaWwtcHJvcGFnYW5k +YQp8fGNsYXNzaWNhbGd1aXRhcmJsb2cubmV0Ci5jbGIub3JnLmhrCmNsZWFyaGFy +bW9ueS5uZXQKY2xlYXJ3aXNkb20ubmV0Cnx8Y2xpbmljYS10aWJldC5ydQouY2xp +cGZpc2guZGUKY2xvYWtwb2ludC5jb20KfHxhcHAuY2xvdWRjb25lLmNvbQp8fGNs +b3VkZmxhcmUtaXBmcy5jb20KfHxjbHViMTA2OS5jb20KfHxjbHViaG91c2VhcGku +Y29tCmNtaS5vcmcudHcKfGh0dHA6Ly93d3cuY21vaW5jLm9yZwpjbXAuaGt1Lmhr +CmhrdXBvcC5oa3UuaGsKfHxjbXVsZS5jb20KfHxjbXVsZS5vcmcKfHxjbXMuZ292 +CnxodHRwOi8vdnBuLmNtdS5lZHUKfGh0dHA6Ly92cG4uc3YuY211LmVkdQouY242 +LmV1Ci5jbmEuY29tLnR3Cnx8Y25hLmNvbS50dwouY25hYmMuY29tCi5jbmQub3Jn +Cnx8Y25kLm9yZwpkb3dubG9hZC5jbmV0LmNvbQouY25leC5vcmcuY24KLmNuaW5l +dS5jb20Kd2lraS5jbml0dGVyLmNvbQouY25uLmNvbS92aWRlbwouY25wb2xpdGlj +cy5vcmcKfHxjbnBvbGl0aWNzLm9yZwouY24tcHJveHkuY29tCnxodHRwOi8vY24t +cHJveHkuY29tCi5jbnByb3h5LmNvbQpibG9nLmNueWVzLmNvbQpuZXdzLmNueWVz +LmNvbQp8fGNvYXQuY28uanAKLmNvY2hpbmEuY28KfHxjb2NoaW5hLmNvCnx8Y29j +aGluYS5vcmcKLmNvZGUxOTg0LmNvbS82NAp8aHR0cDovL2dvYWdlbnQuY29kZXBs +ZXguY29tCnx8Y29kZXNoYXJlLmlvCnx8Y29kZXNrdWxwdG9yLm9yZwp8fGNvbm9o +YS5qcAp8aHR0cDovL3Rvc2guY29tZWR5Y2VudHJhbC5jb20KY29tZWZyb21jaGlu +YS5jb20KfHxjb21lZnJvbWNoaW5hLmNvbQouY29taWMtbWVnYS5tZQpjb21tYW5k +YXJtcy5jb20KfHxjb21tZW50c2hrLmNvbQouY29tbXVuaXN0Y3JpbWVzLm9yZwp8 +fGNvbW11bmlzdGNyaW1lcy5vcmcKfHxjb21tdW5pdHljaG9pY2VjdS5jb20KfHxj +b21wYXJpdGVjaC5jb20KfHxjb21waWxlaGVhcnQuY29tCnx8Y29ub2hhLmpwCi5j +b250YWN0bWFnYXppbmUubmV0Ci5jb252aW8ubmV0Ci5jb29iYXkuY29tCnxodHRw +Oi8vd3d3LmNvb2wxOC5jb20vYmJzKi8KLmNvb2xhbGVyLmNvbQp8fGNvb2xhbGVy +LmNvbQpjb29sZGVyLmNvbQp8fGNvb2xkZXIuY29tCnx8Y29vbGxvdWQub3JnLnR3 +Ci5jb29sbmN1dGUuY29tCnx8Y29vbHN0dWZmaW5jLmNvbQpjb3J1bWNvbGxlZ2Uu +Y29tCi5jb3MtbW9lLmNvbQp8aHR0cDovL2Nvcy1tb2UuY29tCi5jb3NwbGF5amF2 +LnBsCnxodHRwOi8vY29zcGxheWphdi5wbAouY290d2VldC5jb20KfHxjb3R3ZWV0 +LmNvbQouY291cnNlaGVyby5jb20KfHxjb3Vyc2VoZXJvLmNvbQpjcGoub3JnCnx8 +Y3BqLm9yZwouY3E5OS51cwp8aHR0cDovL2NxOTkudXMKY3JhY2tsZS5jb20KfHxj +cmFja2xlLmNvbQouY3JhenlzLmNjCi5jcmF6eXNoaXQuY29tCnx8Y3JhenlzaGl0 +LmNvbQp8fGNyY2hpbmEub3JnCmNyZC1uZXQub3JnCmNyZWFkZXJzLm5ldAp8fGNy +ZWFkZXJzLm5ldAouY3JlYWRlcnNuZXQuY29tCnx8Y3Jpc3R5bGkuY29tCnx8Y3Jv +eHlwcm94eS5jb20KLmNyb2NvdHViZS5jb20KfGh0dHA6Ly9jcm9jb3R1YmUuY29t +Ci5jcm9zc3RoZXdhbGwubmV0Cnx8Y3Jvc3N0aGV3YWxsLm5ldAouY3Jvc3N2cG4u +bmV0Cnx8Y3Jvc3N2cG4ubmV0Cnx8Y3J1Y2lhbC5jb20KfHxibG9nLmNyeXB0b2dy +YXBoeWVuZ2luZWVyaW5nLmNvbQpjc2RwYXJ0eS5jb20KfHxjc2RwYXJ0eS5jb20K +fHxjc2lzLm9yZwp8fGNzbW9uaXRvci5jb20KfHxjc3VjaGVuLmRlCi5jc3cub3Jn +LnVrCi5jdC5vcmcudHcKfHxjdC5vcmcudHcKLmN0YW8ub3JnCi5jdGZyaWVuZC5u +ZXQKLmN0aXR2LmNvbS50dwp8fGN0b3djLm9yZwouY3RzLmNvbS50dwp8fGN0cy5j +b20udHcKfHxjdHdhbnQuY29tCnxodHRwOi8vbGlicmFyeS51c2MuY3Voay5lZHUu +aGsvCnxodHRwOi8vbWpsc2gudXNjLmN1aGsuZWR1LmhrLwouY3Voa2Fjcy5vcmcv +fmJlbm5nCi5jdWlodWEub3JnCnx8Y3VpaHVhLm9yZwouY3Vpd2VpcGluZy5uZXQK +fHxjdWl3ZWlwaW5nLm5ldAp8fGN1bHR1cmUudHcKLmN1bWxvdWRlci5jb20KfHxj +dW1sb3VkZXIuY29tCnx8Y3VydmVmaXNoLmNvbQp8fGN1c3AuaGsKLmN1c3UuaGsK +fHxjdXN1LmhrCi5jdXRzY2VuZXMubmV0Cnx8Y3V0c2NlbmVzLm5ldAouY3cuY29t +LnR3Cnx8Y3cuY29tLnR3CnxodHRwOi8vZm9ydW0uY3liZXJjdG0uY29tCmN5YmVy +Z2hvc3R2cG4uY29tCnx8Y3liZXJnaG9zdHZwbi5jb20KfHxjeW5zY3JpYmUuY29t +CmN5dG9kZS51cwp8fGlmYW4uY3ouY2MKfHxtaWtlLmN6LmNjCnx8bmljLmN6LmNj +CgohLS0tLS0tLS0tLS0tLS0tLS0tLS1ERC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0KLmQtZnVreXUuY29tCnxodHRwOi8vZC1mdWt5dS5jb20KY2wuZDB6Lm5ldAou +ZDEwMC5uZXQKfHxkMTAwLm5ldAouZDJiYXkuY29tCnxodHRwOi8vZDJiYXkuY29t +Ci5kYWJyLmNvLnVrCnx8ZGFici5jby51awpkYWJyLmV1CmRhYnIubW9iaQp8fGRh +YnIubW9iaQp8fGRhYnIubWUKZGFkYXppbS5jb20KfHxkYWRhemltLmNvbQouZGFk +aTM2MC5jb20KLmRhZmFiZXQuY29tCmRhZmFnb29kLmNvbQpkYWZhaGFvLmNvbQou +ZGFmb2gub3JnCi5kYWZ0cG9ybi5jb20KLmRhZ2VsaWprc2VzdGFuZGFhcmQubmwK +LmRhaWRvc3R1cC5ydQp8aHR0cDovL2RhaWRvc3R1cC5ydQouZGFpbGlkYWlsaS5j +b20KfHxkYWlsaWRhaWxpLmNvbQp8fGRhaWx5bWFpbC5jby51awouZGFpbHltb3Rp +b24uY29tCnx8ZGFpbHltb3Rpb24uY29tCnx8ZGFpbHlzYWJhaC5jb20KZGFpcGhh +cGluZm8ubmV0Ci5kYWppeXVhbi5jb20KfHxkYWppeXVhbi5kZQpkYWppeXVhbi5l +dQpkYWxhaWxhbWEuY29tCi5kYWxhaWxhbWEubW4KfGh0dHA6Ly9kYWxhaWxhbWEu +bW4KLmRhbGFpbGFtYS5ydQp8fGRhbGFpbGFtYS5ydQpkYWxhaWxhbWE4MC5vcmcK +LmRhbGFpbGFtYS1hcmNoaXZlcy5vcmcKLmRhbGFpbGFtYWNlbnRlci5vcmcKfGh0 +dHA6Ly9kYWxhaWxhbWFjZW50ZXIub3JnCmRhbGFpbGFtYWZlbGxvd3Mub3JnCi5k +YWxhaWxhbWFmaWxtLmNvbQouZGFsYWlsYW1hZm91bmRhdGlvbi5vcmcKLmRhbGFp +bGFtYWhpbmRpLmNvbQouZGFsYWlsYW1haW5hdXN0cmFsaWEub3JnCi5kYWxhaWxh +bWFqYXBhbmVzZS5jb20KLmRhbGFpbGFtYXByb3Rlc3RlcnMuaW5mbwouZGFsYWls +YW1hcXVvdGVzLm9yZwouZGFsYWlsYW1hdHJ1c3Qub3JnCi5kYWxhaWxhbWF2aXNp +dC5vcmcubnoKLmRhbGFpbGFtYXdvcmxkLmNvbQp8fGRhbGFpbGFtYXdvcmxkLmNv +bQpkYWxpYW5tZW5nLm9yZwp8fGRhbGlhbm1lbmcub3JnCi5kYWxpdWxpYW4ub3Jn +Cnx8ZGFsaXVsaWFuLm9yZwouZGFua2U0Y2hpbmEubmV0Cnx8ZGFua2U0Y2hpbmEu +bmV0CmRhb2xhbi5uZXQKZGFya3RveS5uZXQKfHxkYXJyZW5saXV3ZWkuY29tCnx8 +ZGFzdHJhc3NpLm9yZwp8fGRhdW0ubmV0Ci5kYXZpZC1raWxnb3VyLmNvbQp8aHR0 +cDovL2RhdmlkLWtpbGdvdXIuY29tCmRheGEuY24KfHxkYXhhLmNuCmNuLmRheWFi +b29rLmNvbQouZGF5bGlmZS5jb20vdG9waWMvZGFsYWlfbGFtYQp8fGRiLnR0Ci5k +YmMuaGsvbWFpbgp8fGRiZ2pkLmNvbQp8fGRjYXJkLnR3CmRjbWlsaXRhcnkuY29t +Ci5kZGMuY29tLnR3Ci5kZGh3LmluZm8KfHxkZS1zY2kub3JnCi5kZS1zY2kub3Jn +Cnx8ZGVhZGxpbmUuY29tCnx8ZGVjb2RldC5jbwoKIS0tT3JpZ2luOmNkbi1pMzAk +XwohLS1FeGNlcHRpb246IEhvbWVwYWdlIGFjY2VzcyB3aXRob3V0IHJzdAohLS1L +ZXl3b3JkIGlzICRfCi5kZWZpbmViYWJlLmNvbQoKfHxkZWxjYW1wLm5ldApkZWxp +Y2lvdXMuY29tL0dGV2Jvb2ttYXJrCi5kZW1vY3JhdHMub3JnCnx8ZGVtb2NyYXRz +Lm9yZwouZGVtb3Npc3RvLmhrCnx8ZGVtb3Npc3RvLmhrCnx8ZGVzYy5zZQp8fGRl +c3NjaS5jb20KLmRlc3Ryb3ktY2hpbmEuanAKfHxkZXV0c2NoZS13ZWxsZS5kZQp8 +fGRldmlhbnRhcnQuY29tCnx8ZGV2aWFudGFydC5uZXQKfHxkZXZpby51cwp8fGRl +dnBuLmNvbQp8fGRmYXMubWlsCmRmbi5vcmcKZGhhcm1ha2FyYS5uZXQKLmRoYXJh +bXNhbGFuZXQuY29tCi5kaWFveXVpc2xhbmRzLm9yZwp8fGRpYW95dWlzbGFuZHMu +b3JnCi5kaWZhbmd3ZW5nZS5vcmcKfGh0dHA6Ly9kaWdpbGFuZC50dy8KfHxkaWdp +dGFsbm9tYWRzcHJvamVjdC5vcmcKLmRpaWdvLmNvbQp8fGRpaWdvLmNvbQp8fGRp +bGJlci5zZQp8fGZ1cmwubmV0Ci5kaXBpdHkuY29tCnx8ZGlyZWN0Y3JlYXRpdmUu +Y29tCiEtLXx8ZGlzY29ncy5jb20KIS0tQEB8fGNkbi5kaXNjb2dzLmNvbQouZGlz +Y3Vzcy5jb20uaGsKfHxkaXNjdXNzLmNvbS5oawouZGlzY3VzczR1LmNvbQpkaXNw +LmNjCi5kaXNxdXMuY29tCnx8ZGlzcXVzLmNvbQouZGl0LWluYy51cwp8fGRpdC1p +bmMudXMKLmRpemhpZGl6aGkuY29tCnx8ZGl6aHV6aGlzaGFuZy5jb20KZGphbmdv +c25pcHBldHMub3JnCi5kam9yei5jb20KfHxkam9yei5jb20KfHxkbC1sYWJ5Lmpw +Cnx8ZGxpdmUudHYKfHxkbHNpdGUuY29tCnx8ZGx5b3V0dWJlLmNvbQp8fGRtYy5u +aWNvCnx8ZG1jZG4ubmV0Ci5kbnNjcnlwdC5vcmcKfHxkbnNjcnlwdC5vcmcKfHxk +bnMyZ28uY29tCnx8ZG5zc2VjLm5ldApkb2N0b3J2b2ljZS5vcmcKCiEtLURvZ0Zh +cnROZXR3b3JrCi5kb2dmYXJ0bmV0d29yay5jb20vdG91cgpnbG9yeWhvbGUuY29t +CgouZG9qaW4uY29tCi5kb2stZm9ydW0ubmV0Cnx8ZG9sYy5kZQp8fGRvbGYub3Jn +LmhrCnx8ZG9sbGYuY29tCi5kb21haW4uY2x1Yi50dwouZG9tYWludG9kYXkuY29t +LmF1CmNoaW5lc2UuZG9uZ2EuY29tCmRvbmd0YWl3YW5nLmNvbQp8fGRvbmd0YWl3 +YW5nLmNvbQouZG9uZ3RhaXdhbmcubmV0Cnx8ZG9uZ3RhaXdhbmcubmV0Ci5kb25n +eWFuZ2ppbmcuY29tCnxodHRwOi8vZGFuYm9vcnUuZG9ubWFpLnVzCi5kb250Zmls +dGVyLnVzCnx8ZG9udG1vdmV0b2NoaW5hLmNvbQouZG9yamVzaHVnZGVuLmNvbQou +ZG90cGxhbmUuY29tCnx8ZG90cGxhbmUuY29tCnx8ZG90c3ViLmNvbQouZG90dnBu +LmNvbQp8fGRvdHZwbi5jb20KLmRvdWIuaW8KfHxkb3ViLmlvCnx8ZG91Z3Njcmlw +dHMuY29tCnx8ZG91aG9rYW5rby5uZXQKfHxkb3VqaW5jYWZlLmNvbQpkb3dlaS5v +cmcKfGh0dHBzOi8vYmFydGVuZGVyLmRvd2pvbmVzLmNvbQpkcGhrLm9yZwpkcHAu +b3JnLnR3Cnx8ZHBwLm9yZy50dwp8fGRwci5pbmZvCnx8ZHJhZ29uc3ByaW5ncy5v +cmcKIS0tfHxkcmF3LmlvCi5kcmVhbWFtYXRldXJzLmNvbQouZHJlcHVuZy5vcmcK +fHxkcmdhbi5uZXQKLmRybWluZ3hpYS5vcmcKfGh0dHA6Ly9kcm1pbmd4aWEub3Jn +Cnx8ZHJvcGJvb2tzLnR2Cnx8ZHJvcGJveC5jb20KfHxhcGkuZHJvcGJveGFwaS5j +b20KfHxub3RpZnkuZHJvcGJveGFwaS5jb20KfHxkcm9wYm94dXNlcmNvbnRlbnQu +Y29tCmRyc3VuYWNhZGVteS5jb20KLmRydHViZXIuY29tCi5kc2NuLmluZm8KfGh0 +dHA6Ly9kc2NuLmluZm8KLmRzdGsuZGsKfGh0dHA6Ly9kc3RrLmRrCnx8ZHRpYmxv +Zy5jb20KfHxkdGljLm1pbAouZHR3YW5nLm9yZwouZHVhbnpoaWh1LmNvbQouZHVj +a2Rucy5vcmcKfGh0dHA6Ly9kdWNrZG5zLm9yZwouZHVja2R1Y2tnby5jb20KfHxk +dWNrZHVja2dvLmNvbQouZHVja2xvYWQuY29tL2Rvd25sb2FkCnx8ZHVja215bGlm +ZS5jb20KLmR1Z2EuanAKfGh0dHA6Ly9kdWdhLmpwCi5kdWlodWEub3JnCnx8ZHVp +aHVhLm9yZwp8fGR1aWh1YWhyam91cm5hbC5vcmcKLmR1bnlhYnVsdGVuaS5uZXQK +LmR1b3dlaXRpbWVzLmNvbQp8fGR1b3dlaXRpbWVzLmNvbQpkdXBpbmcubmV0Cnx8 +ZHVwbGljYXRpLmNvbQpkdXBvbGEuY29tCmR1cG9sYS5uZXQKLmR1c2hpLmNhCnx8 +ZHV5YW9zcy5jb20KfHxkdm9yYWsub3JnCi5kdy5jb20KfHxkdy5jb20KfHxkdy5k +ZQouZHctd29ybGQuY29tCnx8ZHctd29ybGQuY29tCi5kdy13b3JsZC5kZQp8aHR0 +cDovL2R3LXdvcmxkLmRlCnd3dy5kd2hlZWxlci5jb20KZHduZXdzLmNvbQp8fGR3 +bmV3cy5jb20KZHduZXdzLm5ldAp8fGR3bmV3cy5uZXQKeHlzLmR4aW9uZy5jb20K +fHxkeW5hd2ViaW5jLmNvbQp8fGR5c2Z6LmNjCi5kenplLmNvbQoKIS0tLS0tLS0t +LS0tLS0tLS0tLS0tRUUtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnx8ZS1jbGFz +c2ljYWwuY29tLnR3Cnx8ZS1nb2xkLmNvbQouZS1nb2xkLmNvbQouZS1oZW50YWku +b3JnCnx8ZS1oZW50YWkub3JnCi5lLWhlbnRhaWRiLmNvbQp8aHR0cDovL2UtaGVu +dGFpZGIuY29tCmUtaW5mby5vcmcudHcKLmUtdHJhZGVybGFuZC5uZXQvYm9hcmQK +LmUtem9uZS5jb20uaGsvZGlzY3V6CnxodHRwOi8vZS16b25lLmNvbS5oay9kaXNj +dXoKLmUxMjMuaGsKfHxlMTIzLmhrCi5lYXJseXRpYmV0LmNvbQp8aHR0cDovL2Vh +cmx5dGliZXQuY29tCi5lYXJ0aGNhbS5jb20KLmVhcnRodnBuLmNvbQp8fGVhcnRo +dnBuLmNvbQplYXN0ZXJuLWFyay5jb20KLmVhc3Rlcm5saWdodG5pbmcub3JnCi5l +YXN0dHVya2VzdGFuLmNvbQp8aHR0cDovL3d3dy5lYXN0dHVya2lzdGFuLm5ldC8K +LmVhc3R0dXJraXN0YW4tZ292Lm9yZwouZWFzdHR1cmtpc3RhbmNjLm9yZwouZWFz +dHR1cmtpc3RhbmdvdmVybm1lbnRpbmV4aWxlLnVzCnx8ZWFzdHR1cmtpc3Rhbmdv +dmVybm1lbnRpbmV4aWxlLnVzCi5lYXN5Y2EuY2EKLmVhc3lwaWMuY29tCnx8Zm5j +LmViYy5uZXQudHcKLmVib255LWJlYXV0eS5jb20KZWJvb2ticm93c2UuY29tCmVi +b29rZWUuY29tCnx8ZWNmYS5vcmcudHcKdXNodWFyZW5jaXR5LmVjaGFpbmhvc3Qu +Y29tCnx8ZWNpbWcudHcKZWNtaW5pc3RyeS5uZXQKLmVjb25vbWlzdC5jb20KYmJz +LmVjc3RhcnQuY29tCmVkZ2VjYXN0Y2RuLm5ldAp8fGVkZ2VjYXN0Y2RuLm5ldAov +dHdpbWdcLmVkZ2VzdWl0ZVwubmV0XC9cLz9hcHBsZWRhaWx5LwplZGljeXBhZ2Vz +LmNvbQouZWRtb250b25jaGluYS5jbgouZWRtb250b25zZXJ2aWNlLmNvbQplZG9v +cnMuY29tCi5lZHVicmlkZ2UuY29tCnx8ZWR1YnJpZGdlLmNvbQouZWR1cHJvLm9y +Zwp8fGVldnBuLmNvbQplZmNjLm9yZy5oawouZWZ1a3QuY29tCnxodHRwOi8vZWZ1 +a3QuY29tCnx8ZWljLWF2LmNvbQp8fGVpcmVpbmlrb3RhZXJ1a2FpLmNvbQouZWlz +YmIuY29tCi5la3Npc296bHVrLmNvbQp8fGVrc2lzb3psdWsuY29tCmVsZWN0aW9u +c21ldGVyLmNvbQp8fGVsZ29vZy5pbQouZWxsYXdpbmUub3JnCi5lbHBhaXMuY29t +Cnx8ZWxwYWlzLmNvbQouZWx0b25kaXNuZXkuY29tCi5lbWFnYS5jb20vaW5mby8z +NDA3CmVtaWx5bGF1Lm9yZy5oawouZW1hbm5hLmNvbS9jaGluZXNlVHJhZGl0aW9u +YWwKYml0Yy5ibWUuZW1vcnkuZWR1L35semhvdS9ibG9ncwouZW1wZmlsLmNvbQou +ZW11bGUtZWQyay5jb20KfGh0dHA6Ly9lbXVsZS1lZDJrLmNvbQouZW11bGVmYW5z +LmNvbQp8aHR0cDovL2VtdWxlZmFucy5jb20KLmVtdXBhcmFkaXNlLm1lCi5lbmFu +eWFuZy5teQohLS0uZW5hbnlhbmcubXkvbmV3cy8yMDE3MDUwMi8lRTclQkUlOEUl +RTUlOUIlQkQlRTQlQjklOEIlRTklOUYlQjMlRTUlQTQlQTclRTUlOUMlQjAlRTkl +OUMlODclRTMlODAlOEElRTglOEIlQjklRTYlOUUlOUMlRTMlODAlOEIlRTclOEIl +QUMlRTUlQUUlQjYKfHxlbmNyeXB0Lm1lCnx8ZW5ld3N0cmVlLmNvbQouZW5mYWwu +ZGUKfHxjaGluZXNlLmVuZ2FkZ2V0LmNvbQp8fGVuZ2FnZWRhaWx5Lm9yZwplbmds +aXNoZm9yZXZlcnlvbmUub3JnCnx8ZW5nbGlzaGZyb21lbmdsYW5kLmNvLnVrCmVu +Z2xpc2hwZW4ub3JnCi5lbmxpZ2h0ZW4ub3JnLnR3Cnx8ZW50ZXJtYXAuY29tCnx8 +YXBwLmV2b3ppLmNvbQouZXBpc2NvcGFsY2h1cmNoLm9yZwouZXBvY2hoay5jb20K +fHxlcG9jaGhrLmNvbQplcG9jaHRpbWVzLWJnLmNvbQp8fGVwb2NodGltZXMtYmcu +Y29tCmVwb2NodGltZXMtcm9tYW5pYS5jb20KfHxlcG9jaHRpbWVzLXJvbWFuaWEu +Y29tCmVwb2NodGltZXMuY28uaWwKfHxlcG9jaHRpbWVzLmNvLmlsCmVwb2NodGlt +ZXMuY28ua3IKfHxlcG9jaHRpbWVzLmNvLmtyCmVwb2NodGltZXMuY29tCnx8ZXBv +Y2h0aW1lcy5jb20KLmVwb2NodGltZXMuY3oKfHxlcG9jaHRpbWVzLmRlCnx8ZXBv +Y2h0aW1lcy5mcgp8fGVwb2NodGltZXMuaWUKfHxlcG9jaHRpbWVzLml0Cnx8ZXBv +Y2h0aW1lcy5qcAp8fGVwb2NodGltZXMucnUKfHxlcG9jaHRpbWVzLnNlCnx8ZXBv +Y2h0aW1lc3RyLmNvbQouZXBvY2h3ZWVrLmNvbQp8fGVwb2Nod2Vlay5jb20KfHxl +cG9jaHdlZWtseS5jb20KLmVwb3JuZXIuY29tCi5lcXVpbmVub3cuY29tCmVyYWJh +cnUubmV0Ci5lcmFjb20uY29tLnR3Ci5lcmF5c29mdC5jb20udHIKLmVyZXB1Ymxp +ay5jb20KLmVyaWdodHMubmV0Cnx8ZXJpZ2h0cy5uZXQKLmVya3R2LmNvbQp8aHR0 +cDovL2Vya3R2LmNvbQp8fGVybmVzdG1hbmRlbC5vcmcKfHxlcm9kYWl6ZW5zeXUu +Y29tCnx8ZXJvZG91amlubG9nLmNvbQp8fGVyb2RvdWppbndvcmxkLmNvbQp8fGVy +b21hbmdhLWtpbmdkb20uY29tCnx8ZXJvbWFuZ2Fkb3V6aW4uY29tCi5lcm9tb24u +bmV0CnxodHRwOi8vZXJvbW9uLm5ldAouZXJvcHJvZmlsZS5jb20KLmVyb3RpY3Nh +bG9vbi5uZXQKLmVzbGl0ZS5jb20KfHxlc2xpdGUuY29tCiEtLS5lc2xpdGUuY29t +L3Byb2R1Y3QKIS0tLmVzbGl0ZS5jb20vU2VhcmNoX0JXLmFzcHg/cQp3aWtpLmVz +dS5pbS8lRTglOUIlQTQlRTglOUIlQTQlRTglQUYlQUQlRTUlQkQlOTUKfHxlc3Uu +ZG9nCi5ldGFhLm9yZy5hdQouZXRhZHVsdC5jb20KZXRhaXdhbm5ld3MuY29tCnx8 +ZXRpemVyLm9yZwp8fGV0b2traS5jb20KfHxldHN5LmNvbQohLS0uZXR0b2RheS5u +ZXQKLmV0dG9kYXkubmV0L25ld3MvMjAxNTEyMTYvNjE0MDgxCmV0dm9ubGluZS5o +awouZXUub3JnCnx8ZXUub3JnCi5ldWNhc2luby5jb20KLmV1bGFtLmNvbQouZXVy +ZWthdnB0LmNvbQp8fGV1cmVrYXZwdC5jb20KLmV1cm9uZXdzLmNvbQp8fGV1cm9u +ZXdzLmNvbQplZWFzLmV1cm9wYS5ldS9kZWxlZ2F0aW9ucy9jaGluYS9wcmVzc19j +b3JuZXIvYWxsX25ld3MvbmV3cy8yMDE1LzIwMTUwNzE2X3poCmVlYXMuZXVyb3Bh +LmV1L3N0YXRlbWVudHMtZWVhcy8yMDE1LzE1MTAyMgouZXZzY2hvb2wubmV0Cnxo +dHRwOi8vZXZzY2hvb2wubmV0Cnx8ZXhibG9nLmpwCnx8YmxvZy5leGJsb2cuY28u +anAKQEB8fHd3dy5leGJsb2cuanAKLmV4Y2hyaXN0aWFuLmhrCnx8ZXhjaHJpc3Rp +YW4uaGsKfGh0dHA6Ly9ibG9nLmV4Y2l0ZS5jby5qcAp8fGV4aGVudGFpLm9yZwp8 +fGV4bW9ybW9uLm9yZwp8fGV4cGF0c2hpZWxkLmNvbQouZXhwZWN0aGltLmNvbQp8 +fGV4cGVjdGhpbS5jb20KZXhwZXJ0cy11bml2ZXJzLmNvbQp8fGV4cGxvYWRlci5u +ZXQKLmV4cHJlc3N2cG4uY29tCnx8ZXhwcmVzc3Zwbi5jb20KLmV4dHJlbWV0dWJl +LmNvbQpleWV2aW8uanAKfHxleWV2aW8uanAKLmV5bnkuY29tCnx8ZXlueS5jb20K +LmV6cGMudGsvY2F0ZWdvcnkvc29mdAouZXpwZWVyLmNvbQoKIS0tLS0tLS0tLS0t +LS0tLS0tLS0tRkYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnx8ZmFjZWJvb2tx +dW90ZXM0dS5jb20KLmZhY2VsZXNzLm1lCnx8ZmFjZWxlc3MubWUKfGh0dHA6Ly9m +YWNlc29mdGliZXRhbnNlbGZpbW1vbGF0b3JzLmluZm8KfHxmYWNlc29mbnlmdy5j +b20KfHxmYWN0cGVkaWEub3JnCi5mYWl0aDEwMC5vcmcKfGh0dHA6Ly9mYWl0aDEw +MC5vcmcKCiEtLUVuaGFuY2VtZW50OgohLS1odHRwOi8vZmFpdGhmdWxleWUuY29t +LmRldGFpbC53ZWJzaXRlLwohLS1odHRwOi8vZmFpdGhmdWxleWUuY29tLmlwYWRk +cmVzcy5jb20vCi5mYWl0aGZ1bGV5ZS5jb20KCnx8ZmFpdGh0aGVkb2cuaW5mbwou +ZmFra3UubmV0Cnx8ZmFsbGVuYXJrLmNvbQouZmFsc2VmaXJlLmNvbQp8fGZhbHNl +ZmlyZS5jb20KZmFsdW4tY28ub3JnCmZhbHVuYXJ0Lm9yZwp8fGZhbHVuYXNpYS5p +bmZvCnxodHRwOi8vZmFsdW5hdS5vcmcKLmZhbHVuYXoubmV0CmZhbHVuZGFmYS5v +cmcKZmFsdW5kYWZhLWRjLm9yZwp8fGZhbHVuZGFmYS1mbG9yaWRhLm9yZwp8fGZh +bHVuZGFmYS1uYy5vcmcKfHxmYWx1bmRhZmEtcGEubmV0Cnx8ZmFsdW5kYWZhLXNh +Y3JhbWVudG8ub3JnCmZhbHVuLW55Lm5ldAp8fGZhbHVuZGFmYWluZGlhLm9yZwpm +YWx1bmRhZmFtdXNldW0ub3JnCi5mYWx1bmdvbmcuY2x1YgouZmFsdW5nb25nLmRl +CmZhbHVuZ29uZy5vcmcudWsKfHxmYWx1bmhyLm9yZwpmYWx1bmluZm8uZGUKZmFs +dW5pbmZvLm5ldAouZmFsdW5waWxpcGluYXMubmV0Cnx8ZmFsdW53b3JsZC5uZXQK +ZmFtaWx5ZmVkLm9yZwouZmFuZ2VtaW5nLmNvbQp8fGZhbmdsaXpoaS5pbmZvCnx8 +ZmFuZ29uZy5vcmcKZmFuZ29uZ2hlaWtlLmNvbQp8fGZhbmhhb2xvdS5jb20KLmZh +bnFpYW5nLnRrCmZhbnFpYW5naG91LmNvbQp8fGZhbnFpYW5naG91LmNvbQouZmFu +cWlhbmd6aGUuY29tCnx8ZmFucWlhbmd6aGUuY29tCnx8ZmFudHYuaGsKZmFwZHUu +Y29tCmZhcHJveHkuY29tCiEtLS5mYXJ4aWFuLmNvbQouZmF3YW5naHVpaHVpLm9y +ZwpmYW5xaWFuZ3lha2V4aS5uZXQKZmFpbC5oawp8fGZhbXVuaW9uLmNvbQouZmFu +LXFpYW5nLmNvbQouZmFuZ2JpbnhpbmcuY29tCnx8ZmFuZ2JpbnhpbmcuY29tCmZh +bmdlbWluZy5jb20KLmZhbmdtaW5jbi5vcmcKfHxmYW5nbWluY24ub3JnCi5mYW5o +YW9kYW5nLmNvbQp8fGZhbnFpYW5nLm5ldHdvcmsKfHxmYW5zd29uZy5jb20KLmZh +bnl1ZS5pbmZvCi5mYXJ3ZXN0Y2hpbmEuY29tCgohLS1GYXN0bHkKZW4uZmF2b3R0 +ZXIubmV0CiEtLXx8cm53Lmdsb2JhbC5zc2wuZmFzdGx5Lm5ldAohLS18aHR0cHM6 +Ly8qZ2xvYmFsLnNzbC5mYXN0bHkubmV0LwpueXRpbWVzLm1hcC5mYXN0bHkubmV0 +Cnx8bnl0aW1lcy5tYXAuZmFzdGx5Lm5ldAp8fGZhc3Qud2lzdGlhLmNvbQoKfHxm +YXN0ZXN0dnBuLmNvbQp8fGZhc3Rzc2guY29tCnx8ZmFzdHN0b25lLm9yZwpmYXZz +dGFyLmZtCnx8ZmF2c3Rhci5mbQpmYXlkYW8uY29tL3dlYmxvZwp8fGZhei5uZXQK +LmZjMi5jb20KLmZjMmNoaW5hLmNvbQouZmMyY24uY29tCnx8ZmMyY24uY29tCmZj +MmJsb2cubmV0CnxodHRwOi8vdXlndXIuZmMyd2ViLmNvbS8KdmlkZW8uZmRib3gu +Y29tCi5mZGM2NC5kZQouZmRjNjQub3JnCi5mZGM4OS5qcAp8fGZvdXJmYWNlLm5v +ZGVzbm9vcC5jb20KIS0tZmVlZGJvb2tzLm1vYmkKfHxmZWVkZXIuY28KfHxmZWVs +c3NoLmNvbQpmZWVyLmNvbQouZmVpZmVpc3MuY29tCnxodHRwOi8vZmVpdGlhbmFj +YWRlbXkub3JnCi5mZWl0aWFuLWNhbGlmb3JuaWEub3JnCnx8ZmVpeGlhb2hhby5j +b20KfHxmZW1pbmlzdHRlYWNoZXIuY29tCi5mZW5nemhlbmdodS5jb20KfHxmZW5n +emhlbmdodS5jb20KLmZlbmd6aGVuZ2h1Lm5ldAp8fGZlbmd6aGVuZ2h1Lm5ldAou +ZmV2ZXJuZXQuY29tCnxodHRwOi8vZmYuaW0KZmZmZmYuYXQKZmZsaWNrLmNvbQou +ZmZ2cG4uY29tCmZnbXR2Lm5ldAouZmdtdHYub3JnCi5maHJlcG9ydHMubmV0Cnxo +dHRwOi8vZmhyZXBvcnRzLm5ldAouZmlncHJheWVyLmNvbQp8fGZpZ3ByYXllci5j +b20KLmZpbGVmbHllci5jb20KfHxmaWxlZmx5ZXIuY29tCnxodHRwOi8vZmVlZHMu +ZmlsZWZvcnVtLmNvbQouZmlsZXMybWUuY29tCi5maWxlc2VydmUuY29tL2ZpbGUK +ZmlsbHRoZXNxdWFyZS5vcmcKZmlsbWluZ2ZvcnRpYmV0Lm9yZwouZmlsdGhkdW1w +LmNvbQouZmluY2h2cG4uY29tCnx8ZmluY2h2cG4uY29tCiEtLWZpbmRib29rLnR3 +CmZpbmRtZXNwb3QuY29tCnx8ZmluZHlvdXR1YmUuY29tCnx8ZmluZHlvdXR1YmUu +bmV0Ci5maW5nZXJkYWlseS5jb20KZmlubGVyLm5ldAouZmlyZWFybXN3b3JsZC5u +ZXQKfGh0dHA6Ly9maXJlYXJtc3dvcmxkLm5ldAp8fHJlbGF5LmZpcmVmb3guY29t +Ci5maXJlb2ZsaWJlcnR5Lm9yZwp8fGZpcmVvZmxpYmVydHkub3JnCi5maXJldHdl +ZXQuaW8KfHxmaXJldHdlZXQuaW8KfHxmaXJzdHBvc3QuY29tCnx8Zmlyc3RyYWRl +LmNvbQohLS18fGZsYWdmb3gubmV0Ci5mbGFnc29ubGluZS5pdApmbGVzaGJvdC5j +b20KLmZsZXVyc2Rlc2xldHRyZXMuY29tCnxodHRwOi8vZmxldXJzZGVzbGV0dHJl +cy5jb20KfHxmbGdnLnVzCnx8ZmxnanVzdGljZS5vcmcKCiEtLXx8ZmFybTYuc3Rh +dGljZmxpY2tyLmNvbQohLS0uZmxpY2tyLmNvbS9waG90b3MvNDYyMzEwNzdATjA2 +CiEtLS5mbGlja3IuY29tL2dyb3Vwcy9haXdlaXdlaQohLS0uZmxpY2tyLmNvbS9w +aG90b3MvZGlnaXRhbGJveTEwMAohLS0uZmxpY2tyLmNvbS9waG90b3MvZnpoZW5n +aHUKIS0tLmZsaWNrci5jb20vcGhvdG9zL2xvbmVseWZveAohLS1mbGlja3IuY29t +L3Bob3Rvcy92YW52YW4vNTI5OTI1MTU3CiEtLS5mbGlja3IuY29tL3Bob3Rvcy93 +aW50ZXJrYW5hbAohLS0uZmxpY2tyLmNvbS9waG90b3Mvem9sYQp8fGZsaWNrci5j +b20KfHxzdGF0aWNmbGlja3IuY29tCgpmbGlja3JoaXZlbWluZC5uZXQKLmZsaWNr +cml2ZXIuY29tCi5mbGluZy5jb20KfHxmbGlwa2FydC5jb20KfHxmbG9nLnR3Ci5m +bHl2cG4uY29tCnx8Zmx5dnBuLmNvbQp8aHR0cDovL2NuLmZtbm5vdy5jb20KZm9m +bGRmcmFkaW8ub3JnCmJsb2cuZm9vbHNtb3VudGFpbi5jb20KLmZvcnVtNGhrLmNv +bQpmYW5nb25nLmZvcnVtcy1mcmVlLmNvbQpwaW9uZWVyLXdvcmtlci5mb3J1bXMt +ZnJlZS5jb20KIS0tZm91cnNxdWFyZS5jb20KIS0tfGh0dHA6Ly80c3EuY29tCnxo +dHRwczovL3NzKi40c3FpLm5ldAp2aWRlby5mb3hidXNpbmVzcy5jb20KfGh0dHA6 +Ly9mb3hnYXkuY29tCnx8ZnJpbmdlbmV0d29yay5jb20KfHxmbGVjaGVpbnRoZXBl +Y2hlLmZyCi5mb2Noay5vcmcKfHxmb2Noay5vcmcKfHxmb2N1c3RhaXdhbi50dwou +Zm9jdXN2cG4uY29tCnx8Zm9mZy5vcmcKLmZvZmctZXVyb3BlLm5ldAouZm9vb29v +LmNvbQp8fGZvb29vby5jb20KfHxmb3JlaWduYWZmYWlycy5jb20KLmZvdGlsZS5t +ZQp8fGZvdXJ0aGludGVybmF0aW9uYWwub3JnCnx8Zm94ZGllLnVzCnx8Zm94c3Vi +LmNvbQpmb3h0YW5nLmNvbQouZnBtdC5vcmcKfGh0dHA6Ly9mcG10Lm9yZwouZnBt +dC50dwouZnBtdC1vc2VsLm9yZwp8fGZwbXRtZXhpY28ub3JnCmZxb2sub3JnCnx8 +ZnFyb3V0ZXIuY29tCnx8ZnJhbmtsYy5jb20KLmZyZWFrc2hhcmUuY29tCnxodHRw +Oi8vZnJlYWtzaGFyZS5jb20KfHxmcmVlNHUuY29tLmFyCmZyZWUtZ2F0ZS5vcmcK +LmZyZWUtaGFkYS1ub3cub3JnCmZyZWUtcHJveHkuY3oKLmZyZWUuZnIvYWRzbApr +aW5lb3guZnJlZS5mcgp0aWJldGxpYnJlLmZyZWUuZnIKfHxmcmVlYWxpbS5jb20K +d2hpdGViZWFyLmZyZWViZWFyYmxvZy5vcmcKfHxmcmVlYnJvd3Nlci5vcmcKLmZy +ZWVjaGFsLmNvbQouZnJlZWRvbWNoaW5hLmluZm8KfHxmcmVlZG9tY2hpbmEuaW5m +bwouZnJlZWRvbWhvdXNlLm9yZwp8fGZyZWVkb21ob3VzZS5vcmcKLmZyZWVkb21z +aGVyYWxkLm9yZwp8fGZyZWVkb21zaGVyYWxkLm9yZwouZnJlZWZxLmNvbQouZnJl +ZWZ1Y2t2aWRzLmNvbQouZnJlZWdhby5jb20KfHxmcmVlZ2FvLmNvbQpmcmVlaWxo +YW10b2h0aS5vcmcKfHxmcmVla2F6YWtocy5vcmcKLmZyZWVrd29ucHlvbmcub3Jn +Cnx8c2F2ZWxpdXhpYW9iby5jb20KLmZyZWVsb3R0by5jb20KfHxmcmVlbG90dG8u +Y29tCmZyZWVtYW4yLmNvbQouZnJlZW9wZW52cG4uY29tCmZyZWVtb3Jlbi5jb20K +ZnJlZW1vcmVuZXdzLmNvbQpmcmVlbXVzZS5vcmcvYXJjaGl2ZXMvNzg5CmZyZWVu +ZXQtY2hpbmEub3JnCmZyZWVuZXdzY24uY29tCmNuLmZyZWVvbmVzLmNvbQouZnJl +ZW96Lm9yZy9iYnMKfHxmcmVlb3oub3JnCnx8ZnJlZXNzaC51cwpmcmVlNHUuY29t +LmFyCi5mcmVlLXNzaC5jb20KfHxmcmVlLXNzaC5jb20KfHxmcmVlYmVhY29uLmNv +bQouZnJlZWNoaW5hLm5ld3MKfHxmcmVlY2hpbmFmb3J1bS5vcmcKfHxmcmVlY2hp +bmF3ZWliby5jb20KLmZyZWVkb21jb2xsZWN0aW9uLm9yZy9pbnRlcnZpZXdzL3Jl +Yml5YV9rYWRlZXIKLmZyZWVmb3J1bXMub3JnCnx8ZnJlZW5ldHByb2plY3Qub3Jn +Ci5mcmVlb3oub3JnCi5mcmVldGliZXQubmV0Cnx8ZnJlZXRpYmV0Lm9yZwouZnJl +ZXRpYmV0YW5oZXJvZXMub3JnCnxodHRwOi8vZnJlZXRpYmV0YW5oZXJvZXMub3Jn +Cnx8ZnJlZXRyaWJlLm1lCi5mcmVldmlld21vdmllcy5jb20KLmZyZWV2cG4ubWUK +fGh0dHA6Ly9mcmVldnBuLm1lCnx8ZnJlZXdhbGxwYXBlcjQubWUKLmZyZWV3ZWJz +LmNvbQouZnJlZXdlY2hhdC5jb20KfHxmcmVld2VjaGF0LmNvbQpmcmVld2VpYm8u +Y29tCnx8ZnJlZXdlaWJvLmNvbQouZnJlZXhpbndlbi5jb20KLmZyZWV5b3V0dWJl +cHJveHkubmV0Cnx8ZnJlZXlvdXR1YmVwcm94eS5uZXQKZnJpZW5kZmVlZC5jb20K +ZnJpZW5kZmVlZC1tZWRpYS5jb20vZTk5YTRlYmUyZmI0YzE5ODVjMmE1ODc3NWVi +NDQyMjk2MWFhNWEyZQpmcmllbmRzLW9mLXRpYmV0Lm9yZwouZnJpZW5kc29mdGli +ZXQub3JnCnx8ZnJpZW5kc29mdGliZXQub3JnCmZyZWVjaGluYS5uZXQKfGh0dHA6 +Ly93d3cuemVuc3VyLmZyZWVyay5jb20vCmZyZWV2cG4ubmwKZnJlZXllbGxvdy5j +b20KaGsuZnJpZW5kZHkuY29tL2hrCnxodHRwOi8vYWR1bHQuZnJpZW5kZmluZGVy +LmNvbS8KLmZyaW5nLmNvbQp8fGZyaW5nLmNvbQouZnJvbWNoaW5hdG91c2EubmV0 +Cnx8ZnJvbW1lbC5uZXQKLmZyb250bGluZWRlZmVuZGVycy5vcmcKfHxmcm9udGxp +bmVkZWZlbmRlcnMub3JnCi5mcm9vdHZwbi5jb20KfHxmcm9vdHZwbi5jb20KfHxm +c2NrZWQub3JnCi5mc3VyZi5jb20KLmZ0di5jb20udHcKfHxmdHYuY29tLnR3Cnx8 +ZnR2bmV3cy5jb20udHcKZnVjZC5jb20KLmZ1Y2tjbm5pYy5uZXQKfHxmdWNrY25u +aWMubmV0CmZ1Y2tnZncub3JnCi5mdWxpb25lLmNvbQp8aHR0cHM6Ly9mdWxpb25l +LmNvbQp8fGZ1bGxlcmNvbnNpZGVyYXRpb24uY29tCmZ1bHVlLmNvbQouZnVuZi50 +dwpmdW5wLmNvbQouZnVxLmNvbQouZnVyaGhkbC5vcmcKfHxmdXJpbmthbi5jb20K +LmZ1dHVyZWNoaW5hZm9ydW0ub3JnCnx8ZnV0dXJlbWVzc2FnZS5vcmcKLmZ1eC5j +b20KLmZ1eWluLm5ldAouZnV5aW5kaWFudGFpLm9yZwouZnV5dS5vcmcudHcKfHxm +dy5jbQouZnhjbS1jaGluZXNlLmNvbQp8fGZ4Y20tY2hpbmVzZS5jb20KZnpoOTk5 +LmNvbQpmemg5OTkubmV0CmZ6bG0uY29tCgohLS0tLS0tLS0tLS0tLS0tLS0tLS1H +Ry0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLmc2aGVudGFpLmNvbQp8aHR0cDov +L2c2aGVudGFpLmNvbQp8fGctcXVlZW4uY29tCnx8Z2FiLmNvbQp8fGdhYm9jb3Jw +LmNvbQouZ2FlcHJveHkuY29tCi5nYWZvcnVtLm9yZwouZ2FnYW9vbGFsYS5jb20K +fHxnYWdhb29sYWxhLmNvbQouZ2FsYXh5bWFjYXUuY29tCnx8Z2FsZW53dS5jb20K +LmdhbHN0YXJzLm5ldAp8fGdhbWU3MzUuY29tCmdhbWViYXNlLmNvbS50dwpnYW1l +am9sdC5jb20KfGh0dHA6Ly93aWtpLmdhbWVycC5qcAp8fGdhbWVyLmNvbS50dwou +Z2FtZXIuY29tLnR3Ci5nYW1lei5jb20udHcKfHxnYW1lei5jb20udHcKLmdhbW91 +c2EuY29tCi5nYW9taW5nLm5ldAp8fGdhb21pbmcubmV0Cmdhbmdlcy5jb20KfHxn +YW5qaW5nd29ybGQuY29tCi5nYW9waS5uZXQKfGh0dHA6Ly9nYW9waS5uZXQKLmdh +b3poaXNoZW5nLm9yZwouZ2Fvemhpc2hlbmcubmV0CmdhcmRlbm5ldHdvcmtzLmNv +bQp8fGdhcmRlbm5ldHdvcmtzLm9yZwohLS1JUCBvZiBHYXJkZW4gTmV0d29yawo3 +Mi41Mi44MS4yMgp8fGdhcnRsaXZlLmNvbQp8fGdhdGUtcHJvamVjdC5jb20KfHxn +YXRoZXIuY29tCi5nYXRoZXJwcm94eS5jb20KZ2F0aS5vcmcudHcKLmdheWJ1YmJs +ZS5jb20KLmdheWNuLm5ldAouZ2F5aHViLmNvbQp8fGdheW1hcC5jYwouZ2F5bWVu +cmluZy5jb20KLmdheXR1YmUuY29tCiEtLXx8Z2F5dHViZS5jb20KfHxpbWFnZXMt +Z2F5dHViZS5jb20KLmdheXdhdGNoLmNvbQp8aHR0cDovL2dheXdhdGNoLmNvbQou +Z2F6b3R1YmUuY29tCnx8Z2F6b3R1YmUuY29tCnx8Z2NjLm9yZy5oawp8fGdjbG9v +bmV5LmNvbQp8fGdjbHVicy5jb20KfHxnY21hc2lhLmNvbQouZ2NwbmV3cy5jb20K +fGh0dHA6Ly9nY3BuZXdzLmNvbQouZ2RidC5uZXQvZm9ydW0KZ2R6Zi5vcmcKfHxn +ZWVrLWFydC5uZXQKZ2Vla2VyaG9tZS5jb20vMjAxMC8wMy94aXhpYW5nLXByb2pl +Y3QtY3Jvc3MtZ2Z3Cnx8Z2Vla2hlYXJ0LmluZm8KLmdla2lrYW1lLmNvbQp8aHR0 +cDovL2dla2lrYW1lLmNvbQouZ2VsYm9vcnUuY29tCnxodHRwOi8vZ2VsYm9vcnUu +Y29tCnx8Z2VuaXVzLmNvbQohLS18fGdlbnVpdGVjLmNvbQouZ2VvY2l0aWVzLmNv +LmpwCi5nZW9jaXRpZXMuY29tL1NpbGljb25WYWxsZXkvQ2lyY3VpdC81NjgzL2Rv +d25sb2FkLmh0bWwKaGsuZ2VvY2l0aWVzLmNvbQpnZW9jaXRpZXMuanAKfHxnZXBo +LmlvCi5nZXJlZm91bmRhdGlvbi5vcmcKfHxnZXRhc3RyaWxsLmNvbQouZ2V0Y2h1 +LmNvbQouZ2V0Y2xvYWsuY29tCnx8Z2V0Y2xvYWsuY29tCnx8Z2V0Zm94eXByb3h5 +Lm9yZwouZ2V0ZnJlZWR1ci5jb20KfHxnZXRnb20uY29tCi5nZXRpMnAubmV0Cnx8 +Z2V0aTJwLm5ldApnZXRpdG9uLmNvbQouZ2V0amV0c28uY29tL2ZvcnVtCi5nZXRs +YW50ZXJuLm9yZwp8fGdldGxhbnRlcm4ub3JnCnx8Z2V0bWFsdXMuY29tCi5nZXRz +b2NpYWxzY29wZS5jb20KfHxnZXRzeW5jLmNvbQp8fGdldHRyLmNvbQpnZmJ2LmRl +Ci5nZmdvbGQuY29tLmhrCi5nZnNhbGUuY29tCnx8Z2ZzYWxlLmNvbQpnZncub3Jn +LnVhCi5nZncucHJlc3MKfHxnZncucHJlc3MKfHxnZncucmVwb3J0Ci5nZ3NzbC5j +b20KfHxnZ3NzbC5jb20KIS0tfHxnaG9zdC5vcmcKLmdob3N0cGF0aC5jb20KfHxn +aG9zdHBhdGguY29tCnx8Z2h1dC5vcmcKLmdpYW50ZXNzbmlnaHQuY29tCnxodHRw +Oi8vZ2lhbnRlc3NuaWdodC5jb20KLmdpZnJlZS5jb20KfHxnaWdhLXdlYi5qcAp0 +dy5naWdhY2lyY2xlLmNvbQp8aHR0cDovL2NuLmdpZ2FuZXdzLmNvbS8KZ2lncG9y +bm8ucnUKfHxnaXJsYmFua2VyLmNvbQouZ2l0LmlvCnx8Z2l0LmlvCnxodHRwOi8v +c29mdHdhcmVkb3dubG9hZC5naXRib29rcy5pbwp8fHJhdy5naXRoYWNrLmNvbQoK +IS0tLUdpdEh1Yi0tLQp8fGdpdGh1Yi5ibG9nCnx8Z2l0aHViLmNvbQohLS1naXRo +dWIuY29tL2dldGxhbnRlcm4KIS0tfGh0dHBzOi8vZ2lzdC5naXRodWIuY29tCiEt +LWh0dHA6Ly9jdGhsby5naXRodWIuaW8vaGt0dgohLS1oYWhheGl4aS5naXRodWIu +aW8KIS0tfGh0dHBzOi8vaGFoYXhpeGkuZ2l0aHViLmlvCiEtLXx8aGFvZWwuZ2l0 +aHViLmlvCiEtLXxodHRwOi8vb25pb25oYWNrZXIuZ2l0aHViLmlvCiEtLXx8cmcz +LmdpdGh1Yi5pbwohLS18fHNpa2FvemhlMTk5Ny5naXRodWIuaW8KIS0tfHxzb2Rh +dGVhLmdpdGh1Yi5pbwohLS18fHRlcm1pbnVzMjA0OS5naXRodWIuaW8KIS0tfHx0 +b3V0eXJhdGVyLmdpdGh1Yi5pbwohLS13c2d6YW8uZ2l0aHViLmlvCiEtLXxodHRw +czovL3dzZ3phby5naXRodWIuaW8KLmdpdGh1Yi5pbwp8fGdpdGh1Yi5pbwp8fGdp +dGh1YnVzZXJjb250ZW50LmNvbQp8fGdpdGh1YmFzc2V0cy5jb20KCi5naXpsZW4u +bmV0Cnx8Z2l6bGVuLm5ldAouZ2pjenouY29tCnx8Z2pjenouY29tCmdsb2JhbGpp +aGFkLm5ldApnbG9iYWxtZWRpYW91dHJlYWNoLmNvbQpnbG9iYWxtdXNldW1vbmNv +bW11bmlzbS5vcmcKfHxnbG9iYWxyZXNjdWUubmV0Ci5nbG9iYWx0bS5vcmcKLmds +b2JhbHZvaWNlc29ubGluZS5vcmcKfHxnbG9iYWx2b2ljZXNvbmxpbmUub3JnCnx8 +Z2xvYmFsdnBuLm5ldAouZ2xvY2suY29tCmdsdWNrbWFuLmNvbS9EYWxhaUxhbWEK +fHxnbWdhcmQuY29tCnx8Z21oei5vcmcKfGh0dHA6Ly93d3cuZ21pZGRsZS5jb20K +fGh0dHA6Ly93d3cuZ21pZGRsZS5uZXQKLmdtbGwub3JnCnx8c3VjaGUuZ214Lm5l +dAp8fGduY2kub3JnLmhrCnx8Z25ld3Mub3JnCmdvLXBraS5jb20KfHxnb2FnZW50 +LmJpegp8fGdvYWdlbnRwbHVzLmNvbQpnb2JldC5jYwpnb2Rmb290c3RlcHMub3Jn +Cnx8Z29kZm9vdHN0ZXBzLm9yZwpnb2Rucy53b3JrCmdvZHNkaXJlY3Rjb250YWN0 +LmNvLnVrCi5nb2RzZGlyZWN0Y29udGFjdC5vcmcKZ29kc2RpcmVjdGNvbnRhY3Qu +b3JnLnR3Ci5nb2RzaW1tZWRpYXRlY29udGFjdC5jb20KfHxnb2Z1bmRtZS5jb20K +LmdvZ290dW5uZWwuY29tCnx8Z29oYXBweS5jb20udHcKLmdva2JheXJhay5jb20K +LmdvbGRiZXQuY29tCnx8Z29sZGJldHNwb3J0cy5jb20KfHxnb2xkZW4tYWdlcy5v +cmcKfHxnb2xkZW5leWV2YXVsdC5jb20KLmdvbGRlbmZyb2cuY29tCnx8Z29sZGVu +ZnJvZy5jb20KLmdvbGRqaXp6LmNvbQp8aHR0cDovL2dvbGRqaXp6LmNvbQouZ29s +ZHN0ZXAubmV0Cnx8Z29sZHdhdmUuY29tCmdvbmdtZW5nLmluZm8KZ29uZ20uaW4K +Z29uZ21pbmxpbGlhbmcuY29tCi5nb25nd3QuY29tCnxodHRwOi8vZ29uZ3d0LmNv +bQpibG9nLmdvby5uZS5qcC9kdWNrLXRhaWxfMjAwOQouZ29vZGF5Lnh5egp8fGdv +b2RheS54eXoKfHxnb29kaG9wZS5zY2hvb2wKLmdvb2RyZWFkcy5jb20KfHxnb29k +cmVhZHMuY29tCi5nb29kcmVhZGVycy5jb20KfHxnb29kcmVhZGVycy5jb20KLmdv +b2R0di5jb20udHcKLmdvb2R0di50dgp8fGdvb2ZpbmQuY29tCi5nb29nbGVzaWxl +LmNvbQouZ29wZXRpdGlvbi5jb20KfHxnb3BldGl0aW9uLmNvbQouZ29wcm94aW5n +Lm5ldAp8fGdvcmVmb3J1bS5jb20KLmdvdHJ1c3RlZC5jb20KfHxnb3RydXN0ZWQu +Y29tCnx8Z290dy5jYQp8fGdyYW1tYWx5LmNvbQpncmFuZHRyaWFsLm9yZwouZ3Jh +cGhpcy5uZS5qcAp8fGdyYXBoaXMubmUuanAKfHxncmFwaHFsLm9yZwp8fGdyYXZh +dGFyLmNvbQpncmVhdGZpcmV3YWxsLmJpegp8fGdyZWF0ZmlyZXdhbGxvZmNoaW5h +Lm5ldAouZ3JlYXRmaXJld2FsbG9mY2hpbmEub3JnCnx8Z3JlYXRmaXJld2FsbG9m +Y2hpbmEub3JnCnx8Z3JlZW5maWVsZGJvb2tzdG9yZS5jb20uaGsKLmdyZWVucGFy +dHkub3JnLnR3Cnx8Z3JlZW5wZWFjZS5vcmcKLmdyZWVucmVhZGluZ3MuY29tL2Zv +cnVtCmdyZWF0LWZpcmV3YWxsLmNvbQpncmVhdC1yb2Mub3JnCmdyZWF0cm9jLm9y +ZwpncmVhdHpob25naHVhLm9yZwouZ3JlZW5wZWFjZS5jb20udHcKLmdyZWVudnBu +Lm5ldAp8fGdyZWVudnBuLm5ldAouZ3JlZW52cG4ub3JnCnx8Z3JvdHR5LW1vbmRh +eS5jb20KZ3MtZGlzY3Vzcy5jb20KfHxnc2VhcmNoLm1lZGlhCnx8Z3RyaWNrcy5j +b20KZ3VhbmNoYS5vcmcKZ3VhbmVyeXUuY29tCi5ndWFyZHN0ZXIuY29tCi5ndW4t +d29ybGQubmV0Cmd1bnNhbmRhbW1vLmNvbQp8fGd1dHRlcnVuY2Vuc29yZWQuY29t +Cnx8Z3ZtLmNvbS50dwp8fGd3aW5zLm9yZwouZ3ptLnR2Cnx8Z3pvbmUtYW5pbWUu +aW5mbwoKIS0tLS0tLS0tLS0tLS1HSFMtLS0tLQohLXx8ZmVlZHMuY2JzbmV3cy5j +b20KIS18fHd3dy5jaGluZXNlYWxidW1hcnQuY29tCnx8Y2xlbWVudGluZS1wbGF5 +ZXIub3JnCiEtfHxjbGVtZXNoYS5vcmcKIS18fHd3dy5jbG91ZGdpcmxmcmllbmQu +Y29tCiEtfHxjb2NvYXdpdGhsb3ZlLmNvbQohLXx8YmxvZy5jb250cm9sc3BhY2Uu +b3JnCiEtRAohLXx8d3d3LmRhaWx5Z3lhbi5jb20KIS18fGRhaWx5dG9kby5vcmcK +IS18fGJsb2cuZGFubWFybmVyLmNvbQohLXx8Z2l0aHViLmRhbm1hcm5lci5jb20K +IS18fGRlc2lnbi1zZWVkcy5jb20KIS18fGRlc2lnbmVycy1hcnRpc3RzLmNvbQoh +LXx8bWFpbC5kaXlhbmcub3JnCiEtfHxibG9nLmRvdWdoZWxsbWFubi5jb20KIS18 +fGRvd25mb3JldmVyeW9uZW9yanVzdG1lLmNvbQohLXx8ZHJvaWRzZWN1cml0eS5j +b20KIS18fHd3dy5kcm9wbW9ja3MuY29tCiEtfHxkdW1ibGl0dGxlbWFuLmNvbQoh +LUUKZWNob2Zvbi5jb20KIS18fGVjaG9mb24uY29tCiEtfHxlcGMtamF2LmNvbQoh +LXx8ZXZlcmRhcmsuaW5mbwohLXx8ZXZoZWFkLmNvbQohLUYKIS18fGZhY2lsZWxv +Z2luLmNvbQohLXx8Ki5mYXRkdWNrLm9yZwohLXx8YmxvZy5mZGNuLm9yZwohLXx8 +ZmZ0b2dvLmNvbQohLXx8ZmxpZ2h0c2ltdGFsay5jb20KIS18fG1jbGVlLmZvb2xt +ZS5uZXQKIS18fHd3dy5mcmllbmRkZWNrLmNvbQohLXx8ZnJpbmdlc3BvaWxlcnMu +Y29tCiEtfHxmcmluZ2V0ZWxldmlzaW9uLmNvbQohLXx8ZnVucGVhLmNvbQohLUcK +IS18fGJsb2cuZ2F0ZWluLm9yZwohLXx8ZmVlZHMuZ2F3a2VyLmNvbQohLXx8Z2Vl +a3RhbmcuY29tCiEtfHxnZW9ob3QudXMKIS18fGdldGFyb3VuZC5jb20KIS18fGdt +ZXIubmV0CiEtfHx3d3cuZ21vdGUub3JnCiEtfHxibG9nLmdvMndlYjIwLm5ldAoh +LXx8Z29vZ2xlLW1lbGFuZ2UuY29tCiEtfHxmYW1lLmdvbnpvbGFicy5vcmcKIS18 +fGdvdmVjbi5vcmcKIS18fGdxdWV1ZXMuY29tCiEtfHxncmFwaHljYWxjLmNvbQp8 +fGdyZWFzZXNwb3QubmV0CiEtfHxibG9nLmdyb3dsZm9yd2luZG93cy5jb20KIS1I +CiEtfHxoY20uY29tLnR3CiEtfHxibG9nLmhlYWRpdXMuY29tCiEtfHxob2diYXlz +b2Z0d2FyZS5jb20KIS18fGJsb2cuaG90b3Qub3JnCiEtfHxmZWVkcy5ob3dzdHVm +ZndvcmtzLmNvbQohLXx8aHVoYWl0YWkuY29tCiEtfHxibG9nLmh1bWFucmlnaHRz +Zmlyc3Qub3JnCiEtSQohLXx8c2l0ZS5pY3UtcHJvamVjdC5vcmcKIS18fGlnb3J3 +YXJlLmNvbQohLXx8aWhhczEzMzdjb2RlLmNvbQohLXx8aW5rbm91dmVhdS5jb20K +IS18fGlub3RlLnR3CiEtfHxpcm9uaGVsbWV0LmNvbQohLXx8aXdmd2NmLmNvbQoh +LUoKIS18fGJsb2cuamFuZ210LmNvbQohLXx8YmxvZy5qYXlmaWVsZHMuY29tCiEt +fHxibG9nLmpvaW50Lm5ldAohLXx8YmxvZy5qc3F1YXJlZGphdmFzY3JpcHQuY29t +CiEtfHxibG9nLmp0YndvcmxkLmNvbQohLUsKIS18fGthdGh5c2Nod2FsYmUuY29t +CiEtfHx0b21hdG92cG4ua2VpdGhtb3llci5jb20KIS18fHd3dy5rZWl0aG1veWVy +LmNvbQohLXx8a2VuZGFsdmFuZHlrZS5jb20KIS18fGJsb2cua2VuZ2FvLnR3CiEt +fHxsb2cua2Vzby5jbgohLXx8d3d3LmtoYW5hY2FkZW15Lm9yZwp8fHd3dy5rbGlw +Lm1lCiEtfHx1c2Jsb2FkZXJneC5rb3VyZWlvLm5ldAohLXx8YmxvZy5rb3dhbGN6 +eWsuaW5mbwohLUwKIS18fGxhYnlyaW50aDIuY29tCiEtfHxsYXJzZ2VvcmdlLmNv +bQohLXx8YmxvZy5sYXN0cGFzcy5jb20KIS18fGRvY3MubGF0ZXhsYWIub3JnCiEt +fHxsZWFuZXNzYXlzLmNvbQohLXx8YmxvZy5saWRhb2JpbmcuaW5mbwohLXx8bG9n +LmxpZ2h0b3J5Lm5ldAohLXx8ZmVlZHMubGltaS5uZXQKIS18fHd3dy5saXRlYXBw +bGljYXRpb25zLmNvbQohLXx8YmxvZy5saXVrYW5neHUuaW5mbwohLXx8dHdpdHRl +ci5saXVrYW5neHUuaW5mbwohLXx8b2FzaXNuZXdzcm9vbS5saXZlNGV2ZXIudXMK +IS18fHd3dy5sb2NrZXJnbm9tZS5jb20KIS18fGxvY3FsLmNvbQpAQHx8c2l0ZS5s +b2NxbC5jb20KIS18fGZlZWRzLmxvaWNsZW1ldXIuY29tCiEtfHxibG9nLmxvdWlz +Z3JheS5jb20KIS1NCiEtfHxtYWRlYnlzb2ZhLmNvbQohLXx8bWFkZW1vaXNlbGxl +cm9ib3QuY29tCiEtfHxtYXNhbWl4ZXMuY29tCiEtfHx3d3cubWV0YW11c2UubmV0 +CiEtfHxibG9nLm1ldGFzcGxvaXQuY29tCiEtfHxtaWxhemkuY29tCiEtfHx3d3cu +bWluaXdlYXRoZXIuY29tCiEtfHx0d2l0dGVyLm1pc3NpdS5jb20KIS18fHBsdXJr +dG9wLWJ1dHRvbi5tbWRheXMuY29tCiEtfHxmZWVkcy5tb2JpbGVyZWFkLmNvbQoh +LXx8d3d3Lm1vZGVybml6ci5jb20KIS18fHd3dy5tb2RrLml0CiEtfHxteXR3aXNo +aXJ0LmNvbQohLU4KIS18fGJsb2cubmV0ZmxpeC5jb20KIS18fGJsb2cubmloaWxv +Z2ljLmRrCiEtfHxudGxrLm9yZwohLXx8bnZxdWFuLm9yZwohLXx8bm9nb29kYXRj +b2RpbmcuY29tCiEtfHxibG9nLm5vdGRvdC5uZXQKIS18fHd3dy5ub3RpZnkuaW8K +IS1PCiEtfHxibG9nLm9idmlvdXMuY29tCiEtfHxvbmViaWdmbHVrZS5jb20KIS18 +fG92ZXJzdGltdWxhdGUuY29tCiEtUAohLXx8cGNnZWVrYmxvZy5jb20KIS18fGZl +ZWRzLnBkZmNobS5uZXQKIS18fGZlZWRzLnBlb3BsZS5jb20KIS18fGJsb2cucGVy +c2lzdGVudC5pbmZvCiEtfHxjaHJvbWUucGxhbnRzdnN6b21iaWVzLmNvbQohLXx8 +cG9ydGFibGVzb2Z0Lm9yZy5ydQohLXx8cHJhc2FubmF0ZWNoLm5ldAohLXx8dGFs +ay5uZXdzLnB0cy5vcmcudHcKIS18fHB5dGhvbi1leGNlbC5vcmcKIS1RCiEtUgoh +LXx8ci1jaGFydC5jb20KIS18fHJhbWVzaHN1YnJhbWFuaWFuLm9yZwohLXx8cmFw +aWQucGsKIS18fGJsb2cucmVuYW5zZS5jb20KIS18fHJvYmVydG1hby5jb20KIS18 +fHd3dy5yb21lby1mb3h0cm90LmNvbQohLVMKIS18fHNhbG1peXVjay5jb20KIS18 +fHNhbXNhbC5jb20KIS18fGJsb2cuc2VlbWluZ2xlZS5jb20KIS18fGJsb2cuc2Zs +b3cuY29tCiEtfHxibG9nLnNpZ2ZwZS5jb20KIS18fHNpbXBsZXRleHQud3MKIS18 +fHd3dy5za3VscHQub3JnCiEtfHxyc3Muc2xhc2hkb3Qub3JnCiEtfHxzbmlwcGV0 +c2FwcC5jb20KIS18fHcuc25zLmx5CiEtfHx3d3cuc29jaWFsbm1vYmlsZS5jb20K +IS18fHd3dy5zb2NpYWx3aG9pcy5jb20KIS18fHNwaXJpdGpiLm9yZwohLXx8c3Ni +b29rLmNvbQohLXx8c3NoZm9yd2FyZGluZy5jb20KIS18fHN0YXRpb25lcmlhLmNv +bQp8fHN0ZXBoYW5pZXJlZC5jb20KIS18fHN1bmppZG9uZy5uZXQKIS18fHN5bml1 +bXNvZnR3YXJlLmNvbQpAQHx8ZG93bmxvYWQuc3luaXVtc29mdHdhcmUuY29tCiEt +VAohLXx8dGFneGVkby5jb20KIS18fGJsb2cudGF0b2ViYS5vcmcKIS18fHd3dy50 +ZWNoZm9iLmNvbQohLXx8dGVhY2hwYXJlbnRzdGVjaC5vcmcKIS18fHRoZThwZW4u +Y29tCiEtfHx0aGVpcGhvbmV3aWtpLmNvbQohLXx8YmxvZy50aGVzaWxlbnRudW1i +ZXIubWUKIS18fHRoZXNwb250eS5jb20KIS18fHRoZXVsdHJhbGlueC5jb20KIS18 +fGJsb2cudGhpbmstYXN5bmMuY29tCiEtfHx0b3JuYWRvd2ViLm9yZwohLXx8dHJh +bnNwYXJlbnR1cHRpbWUuY29tCiEtfHx0cmlhbmd1bGF0aW9uYmxvZy5jb20KIS18 +fGJsb2cudHN1bmFuZXQubmV0CiEtfHxlbi50dXhlcm8uY29tCiEtfHx0d2F6enVw +LmNvbQohLXx8dHdlZXRzd2VsbC5jb20KIS18fHR3aWJlcy5jb20KIS18fGFydC50 +d2dnLm9yZwohLXx8dHdpdmVydC5jb20KIS1VCnxodHRwOi8vdWIwLmNjCiEtfHxq +b25ueS51YnVudHUtdHcubmV0CiEtfHxibG9nLnVtb25rZXkubmV0CiEtVgohLXx8 +dHAudmJhcC5jb20uYXUKIS18fHd3dy52aXJ0dW91c3JvbS5jb20KIS18fGJsb2cu +dmlzaWJvdGVjaC5jb20KIS1XCiEtfHx3YXZlcHJvdG9jb2wub3JnCiEtfHx3d3cu +d2F2ZXNhbmRib3guY29tCiEtfHx3ZWJmZWUub3JnLnJ1CiEtfHxibG9nLndlYm1w +cm9qZWN0Lm9yZwohLXx8d2VidXBkOC5vcmcKIS18fHd3dy53aGF0YnJvd3Nlci5v +cmcKIS18fHd3dy53aGVyZWRveW91Z28ubmV0CiEtfHx3aWxsaGFpbnMuY29tCiEt +fHxmZWVkcy53aXJlZC5jb20KIS18fHdpc2VtYXBwaW5nLm9yZwp3b3p5LmluCiEt +fHx3b3p5LmluLwohLXx8YmxvZy53dW5kZXJjb3VudGVyLmNvbQohLVgKIS18fHhk +ZWx0YS5vcmcKIS18fHhpYW9nYW96aS5vcmcKIS18fHhpbG91LnVzCiEtfHx4enku +b3JnLnJ1CiEtWQohLXx8eW9vcGVyLmJlCiEtfHx0c29uZy55dW54aS5uZXQKIS1a +Cgpnb3NwZWxoZXJhbGQuY29tCnx8Z29zcGVsaGVyYWxkLmNvbQp8aHR0cDovL2hr +LmdyYWRjb25uZWN0aW9uLmNvbS8KfHxncmFuZ29yei5vcmcKZ3JlYXRmaXJlLm9y +Zwp8fGdyZWF0ZmlyZS5vcmcKZ3JlYXRmaXJld2FsbG9mY2hpbmEub3JnCnx8Z3Jl +YXRyb2MudHcKLmd0cy12cG4uY29tCnxodHRwOi8vZ3RzLXZwbi5jb20KfHxndHYu +b3JnCnx8Z3R2MS5vcmcKLmd1LWNodS1zdW0ub3JnCnxodHRwOi8vZ3UtY2h1LXN1 +bS5vcmcKLmd1YWd1YXNzLmNvbQp8aHR0cDovL2d1YWd1YXNzLmNvbQouZ3VhZ3Vh +c3Mub3JnCnxodHRwOi8vZ3VhZ3Vhc3Mub3JnCi5ndWFuZ21pbmcuY29tLm15Cmd1 +aXNoYW4ub3JnCnx8Z3Vpc2hhbi5vcmcKLmd1bXJvYWQuY29tCnx8Z3Vtcm9hZC5j +b20KfHxndW5zYW1lcmljYS5jb20KZ3VydW9ubGluZS5oawp8aHR0cDovL2d2bGli +LmNvbQouZ3lhbHdhcmlucG9jaGUuY29tCi5neWF0c29zdHVkaW8uY29tCgohLS0t +LS0tLS0tLS0tLS0tLS0tLS1ISC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLmg1 +MjguY29tCi5oNWRtLmNvbQouaDVnYWxnYW1lLm1lCnx8aC1jaGluYS5vcmcKLmgt +bW9lLmNvbQp8aHR0cDovL2gtbW9lLmNvbQpoMW4xY2hpbmEub3JnCi5oYWNnLmNs +dWIKfHxoYWNnLmNsdWIKLmhhY2cuaW4KfGh0dHA6Ly9oYWNnLmluCi5oYWNnLmxp +CnxodHRwOi8vaGFjZy5saQouaGFjZy5tZQp8aHR0cDovL2hhY2cubWUKLmhhY2cu +cmVkCnxodHRwOi8vaGFjZy5yZWQKLmhhY2tlbi5jYy9iYnMKLmhhY2tlci5vcmcK +fHxoYWNrbWQuaW8KfHxoYWNrdGhhdHBob25lLm5ldApoYWhsby5jb20KfHxoYWtr +YXR2Lm9yZy50dwouaGFuZGNyYWZ0ZWRzb2Z0d2FyZS5vcmcKfGh0dHA6Ly9iYnMu +aGFubWluenUub3JnLwouaGFudW55aS5jb20KLmhhby5uZXdzL25ld3MKfGh0dHA6 +Ly9hZS5oYW8xMjMuY29tCnxodHRwOi8vYXIuaGFvMTIzLmNvbQp8aHR0cDovL2Jy +LmhhbzEyMy5jb20KfGh0dHA6Ly9lbi5oYW8xMjMuY29tCnxodHRwOi8vaWQuaGFv +MTIzLmNvbQp8aHR0cDovL2pwLmhhbzEyMy5jb20KfGh0dHA6Ly9tYS5oYW8xMjMu +Y29tCnxodHRwOi8vbXguaGFvMTIzLmNvbQp8aHR0cDovL3NhLmhhbzEyMy5jb20K +fGh0dHA6Ly90aC5oYW8xMjMuY29tCnxodHRwOi8vdHcuaGFvMTIzLmNvbQp8aHR0 +cDovL3ZuLmhhbzEyMy5jb20KfGh0dHA6Ly9oay5oYW8xMjNpbWcuY29tCnxodHRw +Oi8vbGQuaGFvMTIzaW1nLmNvbQp8fGhhcHB5LXZwbi5jb20KLmhhcHJveHkub3Jn +Cnx8aGFyZHNleHR1YmUuY29tCi5oYXJ1bnlhaHlhLmNvbQp8aHR0cDovL2hhcnVu +eWFoeWEuY29tCmJicy5oYXNpLndhbmcKaGF2ZTguY29tCkBAfHxoYXlnby5jb20K +LmhjbGlwcy5jb20KfHxoZGx0Lm1lCnx8aGR0dmIubmV0Ci5oZHpvZy5jb20KfGh0 +dHA6Ly9oZHpvZy5jb20KfHxvcmRucy5oZS5uZXQKfHxoZWFydHlpdC5jb20KLmhl +YXZ5LXIuY29tCi5oZWMuc3UKfGh0dHA6Ly9oZWMuc3UKLmhlY2FpdG91Lm5ldAp8 +fGhlY2FpdG91Lm5ldAouaGVjaGFqaS5jb20KfHxoZWNoYWppLmNvbQp8fGhlZWFj +dC5lZHUudHcKLmhlZ3JlLWFydC5jb20KfGh0dHA6Ly9oZWdyZS1hcnQuY29tCnx8 +Y2RuLmhlbGl4c3R1ZGlvcy5uZXQKfHxoZWxwbGluZmVuLmNvbQp8fGhlbHB1eWdo +dXJzbm93Lm9yZwp8fGhlbGxvYW5kcm9pZC5jb20KfHxoZWxsb3F1ZWVyLmNvbQou +aGVsbG9zcy5wdwpoZWxsb3R4dC5jb20KfHxoZWxsb3R4dC5jb20KLmhlbnRhaS50 +bwouaGVsbG91ay5vcmcvZm9ydW0vbG9maXZlcnNpb24KLmhlbHBlYWNocGVvcGxl +LmNvbQp8fGhlbHBlYWNocGVvcGxlLmNvbQp8fGhlbHBzdGVyLmRlCi5oZWxwemh1 +bGluZy5vcmcKaGVudGFpdHViZS50dgouaGVudGFpdmlkZW93b3JsZC5jb20KCiEj +IyMjIyMjIyMjIy0tSGVyb2t1LS0jIyMjIyMjIyMjCiEtLXx8Z2V0Y2xvdWRhcHAu +Y29tCiEtLXx8Y2wubHkKIS0tQEB8fGYuY2wubHkKIS0tRUMyIEROUyBQb2lzb25l +ZAp8fGlkLmhlcm9rdS5jb20KCmhlcWluZ2xpYW4ubmV0Cnx8aGVxaW5nbGlhbi5u +ZXQKfHxoZXJpdGFnZS5vcmcKfHxoZXVuZ2tvbmdkaXNjdXNzLmNvbQouaGV4aWVz +aGUuY29tCnx8aGV4aWVzaGUuY29tCnx8aGV4aWVzaGUueHl6CiEtLUdvb2dsZSBl +bXBsb3llZSB3aXRoaW4gR29vZ2xlIElQCnx8aGV4eGVoLm5ldAp8fGhleXVlZGku +Y29tCmFwcC5oZXl3aXJlLmNvbQouaGV5em8uY29tCi5oZ3NlYXYuY29tCi5oaGRj +YjNvZmZpY2Uub3JnCi5oaHRoZXNha3lhdHJpemluLm9yZwpoaS1vbi5vcmcudHcK +aGlkZGVuLWFkdmVudC5vcmcKfHxoaWRkZW4tYWR2ZW50Lm9yZwpoaWRlY2xvdWQu +Y29tL2Jsb2cvMjAwOC8wNy8yOS9mdWNrLWJlaWppbmctb2x5bXBpY3MuaHRtbAp8 +fGhpZGUubWUKLmhpZGVpbi5uZXQKLmhpZGVpcHZwbi5jb20KfHxoaWRlaXB2cG4u +Y29tCi5oaWRlbWFuLm5ldAp8fGhpZGVtYW4ubmV0CmhpZGVtZS5ubAp8fGhpZGVt +eS5uYW1lCi5oaWRlbXlhc3MuY29tCnx8aGlkZW15YXNzLmNvbQpoaWRlbXljb21w +LmNvbQp8fGhpZGVteWNvbXAuY29tCi5oaWhpZm9ydW0uY29tCi5oaWhpc3Rvcnku +bmV0Cnx8aGloaXN0b3J5Lm5ldAouaGlnZncuY29tCmhpZ2hwZWFrc3B1cmVlYXJ0 +aC5jb20KfHxoaWdocm9ja21lZGlhLmNvbQp8fGhpaXRjaC5jb20KfHxoaWtpbmdn +Zncub3JnCi5oaWxpdmUudHYKLmhpbWFsYXlhbi1mb3VuZGF0aW9uLm9yZwp8fGhp +bWFsYXlhbi1mb3VuZGF0aW9uLm9yZwpoaW1hbGF5YW5nbGFjaWVyLmNvbQouaGlt +ZW1peC5jb20KfHxoaW1lbWl4LmNvbQouaGltZW1peC5uZXQKdGltZXMuaGluZXQu +bmV0Ci5oaXRvbWkubGEKfGh0dHA6Ly9oaXRvbWkubGEKLmhpd2lmaS5jb20KQEB8 +fGhpd2lmaS5jb20KaGl6YnV0dGFocmlyLm9yZwpoaXpiLXV0LXRhaHJpci5pbmZv +CmhpemItdXQtdGFocmlyLm9yZwouaGpjbHViLmluZm8KLmhrLXB1Yi5jb20vZm9y +dW0KfGh0dHA6Ly9oay1wdWIuY29tCi5oazAxLmNvbQp8fGhrMDEuY29tCi5oazMy +MTY4LmNvbQp8fGhrMzIxNjguY29tCnx8aGthY2cuY29tCnx8aGthY2cubmV0Ci5o +a2F0dm5ld3MuY29tCmhrYmMubmV0Ci5oa2JmLm9yZwouaGtib29rY2l0eS5jb20K +fHxoa2Jvb2tjaXR5LmNvbQp8fGhrY2hyb25pY2xlcy5jb20KLmhrY2h1cmNoLm9y +Zwpoa2NpLm9yZy5oawouaGtjbWkuZWR1Cnx8aGtjbmV3cy5jb20KfHxoa2NvYy5j +b20KfHxoa2N0dS5vcmcuaGsKaGtkYXkubmV0Ci5oa2RhaWx5bmV3cy5jb20uaGsv +Y2hpbmEucGhwCnx8aGtkYy51cwpoa2RmLm9yZwouaGtlai5jb20KLmhrZXBjLmNv +bS9mb3J1bS92aWV3dGhyZWFkLnBocD90aWQ9MTE1MzMyMgp8fGhrZXQuY29tCnx8 +aGtmYWEuY29tCmhrZnJlZXpvbmUuY29tCmhrZnJvbnQub3JnCm0uaGtnYWxkZW4u +Y29tCnxodHRwczovL20uaGtnYWxkZW4uY29tCi5oa2dyZWVucmFkaW8ub3JnL2hv +bWUKfHxoa2dwYW8uY29tCi5oa2hlYWRsaW5lLmNvbSpibG9nCi5oa2hlYWRsaW5l +LmNvbS9pbnN0YW50bmV3cwpoa2hraGsuY29tCmhraHJjLm9yZy5oawpoa2hybS5v +cmcuaGsKfHxoa2lwLm9yZy51awoxOTg5cmVwb3J0LmhramEub3JnLmhrCmhramMu +Y29tCi5oa2pwLm9yZwouaGtsZnQuY29tCi5oa2x0cy5vcmcuaGsKfHxoa2x0cy5v +cmcuaGsKfHxoa21hcC5saXZlCnx8aGtvcGVudHYuY29tCnx8aGtwZWFudXQuY29t +CmhrcHR1Lm9yZwouaGtyZXBvcnRlci5jb20KfHxoa3JlcG9ydGVyLmNvbQp8aHR0 +cDovL2hrdXBvcC5oa3UuaGsvCi5oa3VzdS5uZXQKfHxoa3VzdS5uZXQKLmhrdndl +dC5jb20KLmhrd2NjLm9yZy5oawp8fGhrem9uZS5vcmcKLmhtb25naG90LmNvbQp8 +aHR0cDovL2htb25naG90LmNvbQouaG12LmNvLmpwLwpobmpoai5jb20KfHxobmpo +ai5jb20KLmhubnR1YmUuY29tCnx8aG9sYS5jb20KfHxob2xhLm9yZwpob2x5bW91 +bnRhaW5jbi5jb20KaG9seXNwaXJpdHNwZWFrcy5vcmcKfHxob2x5c3Bpcml0c3Bl +YWtzLm9yZwp8fGRlcmVraHN1LmhvbWVpcC5uZXQKLmhvbWVwZXJ2ZXJzaW9uLmNv +bQp8aHR0cDovL2hvbWVzZXJ2ZXJzaG93LmNvbQp8aHR0cDovL29sZC5ob25leW5l +dC5vcmcvc2NhbnMvc2NhbjMxL3N1Yi9kb3VnX2VyaWMvc3BhbV90cmFuc2xhdGlv +bi5odG1sCi5ob25na29uZ2ZwLmNvbQp8fGhvbmdrb25nZnAuY29tCmhvbmdtZWlt +ZWkuY29tCnx8aG9uZ3poaS5saQp8fGhvbnZlbi54eXoKLmhvb3RzdWl0ZS5jb20K +fHxob290c3VpdGUuY29tCnx8aG9vdmVyLm9yZwouaG9wZWRpYWxvZ3VlLm9yZwp8 +aHR0cDovL2hvcGVkaWFsb2d1ZS5vcmcKLmhvcHRvLm9yZwouaG9ybnlnYW1lci5j +b20KLmhvcm55dHJpcC5jb20KfGh0dHA6Ly9ob3JueXRyaXAuY29tCnx8aG9ycm9y +cG9ybi5jb20KfHxob3N0bG9jLmNvbQp8fGhvdGFpci5jb20KLmhvdGF2LnR2Ci5o +b3RlbHMuY24KaG90ZnJvZy5jb20udHcKaG90Z29vLmNvbQouaG90cG9ybnNob3cu +Y29tCmhvdHBvdC5oawouaG90c2hhbWUuY29tCnx8aG90c3BvdHNoaWVsZC5jb20K +fHxob3R0Zy5jb20KLmhvdHZwbi5jb20KfHxob3R2cG4uY29tCnx8aG91Z2FpZ2Uu +Y29tCnx8aG93dG9mb3JnZS5jb20KfHxob3h4LmNvbQouaHFjZHAub3JnCnx8aHFj +ZHAub3JnCnx8aHFqYXBhbmVzZXNleC5jb20KaHFtb3ZpZXMuY29tCi5ocmNpci5j +b20KLmhyY2NoaW5hLm9yZwouaHJlYS5vcmcKLmhyaWNoaW5hLm9yZwp8fGhyaWNo +aW5hLm9yZwouaHJ0c2VhLmNvbQouaHJ3Lm9yZwp8fGhydy5vcmcKaHJ3ZWIub3Jn +Cnx8aHNqcC5uZXQKfHxoc3NlbGl0ZS5jb20KfGh0dHA6Ly9oc3QubmV0LnR3Ci5o +c3Rlcm4ubmV0Ci5oc3R0Lm5ldAouaHRrb3UubmV0Cnx8aHRrb3UubmV0Ci5odWEt +eXVlLm5ldAouaHVhZ2xhZC5jb20KfHxodWFnbGFkLmNvbQouaHVhbmdodWFnYW5n +Lm9yZwp8fGh1YW5naHVhZ2FuZy5vcmcKLmh1YW5neWl5dS5jb20KLmh1YXJlbi51 +cwp8fGh1YXJlbi51cwouaHVhcmVuNHVzLmNvbQouaHVhc2hhbmduZXdzLmNvbQp8 +aHR0cDovL2h1YXNoYW5nbmV3cy5jb20KYmJzLmh1YXNpbmcub3JnCmh1YXhpYS1u +ZXdzLmNvbQpodWF4aWFiYW8ub3JnCmh1YXhpbi5waAp8fGh1YXl1d29ybGQub3Jn +Ci5odWZmaW5ndG9ucG9zdC5jb20vcmViaXlhLWthZGVlcgp8fGh1Z29yb3kuZXUK +fHxodWhhaXRhaS5jb20KfHxodWhhbWhpcmUuY29tCi5odWhhbmdmZWkuY29tCnx8 +aHVoYW5nZmVpLmNvbQpodWl5aS5pbgouaHVsa3NoYXJlLmNvbQp8fGh1bmcteWEu +Y29tCnx8aHVuZ2Vyc3RyaWtlZm9yYWlkcy5vcmcKfHxodXBpbmcubmV0Cmh1cmdv +a2JheXJhay5jb20KLmh1cnJpeWV0LmNvbS50cgouaHV0Mi5ydQp8fGh1dGlhbnlp +Lm5ldApodXRvbmc5Lm5ldApodXlhbmRleC5jb20KLmh3YWR6YW4udHcKfHxod2F5 +dWUub3JnLnR3Cnx8aHdpbmZvLmNvbQp8fGh4d2sub3JnCmh4d3Eub3JnCnx8aHlw +ZXJyYXRlLmNvbQplYm9vay5oeXJlYWQuY29tLnR3Cnx8ZWJvb2suaHlyZWFkLmNv +bS50dwoKIS0tLS0tLS0tLS0tLS0tLS0tLS0tSUktLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tCnx8aTEuaGsKfHxpMnAyLmRlCnx8aTJydW5uZXIuY29tCnx8aTgxOGhr +LmNvbQouaS1jYWJsZS5jb20KLmktcGFydC5jb20udHcKLmlhbXRvcG9uZS5jb20K +aWFzay5jYQp8fGlhc2suY2EKaWFzay5iegp8fGlhc2suYnoKLmlhdjE5LmNvbQpp +YmlibGlvLm9yZy9wdWIvcGFja2FnZXMvY2NpYwp8fGliaXQuYW0KLmlibGlzdC5j +b20KfHxpYmxvZ3NlcnYtZi5uZXQKaWJyb3Mub3JnCnxodHRwOi8vY24uaWJ0aW1l +cy5jb20KLmlidnBuLmNvbQp8fGlidnBuLmNvbQppY2Ftcy5jb20KYmxvZ3MuaWNl +cm9ja2V0LmNvbS90YWcKLmljaWoub3JnCnx8aWNpai5vcmcKfHxpY2wtZmkub3Jn +Ci5pY29jby5jb20KfHxpY29jby5jb20KCiEtLTM4LjEwMy4xNjUuNTAKfHxmdXJi +by5vcmcKIS0tfHxpY29uZmFjdG9yeS5jb20KfHx3YXJibGVyLmljb25mYWN0b3J5 +Lm5ldAoKfHxpY29ucGFwZXIub3JnCiEtLSBHb29nbGUgUGFnZXMKfHxpY3UtcHJv +amVjdC5vcmcKdy5pZGFpd2FuLmNvbS9mb3J1bQppZGVtb2NyYWN5LmFzaWEKLmlk +ZW50aS5jYQp8fGlkZW50aS5jYQp8fGlkaW9tY29ubmVjdGlvbi5jb20KfGh0dHA6 +Ly93d3cuaWRsY295b3RlLmNvbQouaWRvdWdhLmNvbQouaWRyZWFteC5jb20KZm9y +dW0uaWRzYW0uY29tCi5pZHYudHcKLmllYXN5NS5jb20KfGh0dHA6Ly9pZWFzeTUu +Y29tCi5pZWQyay5uZXQKLmllbmVyZ3kxLmNvbQp8fGllcGwudXMKfHxpZnQudHQK +aWZhbnFpYW5nLmNvbQouaWZjc3Mub3JnCnx8aWZjc3Mub3JnCmlmamMub3JnCi5p +ZnQudHQKfGh0dHA6Ly9pZnQudHQKfHxpZnJlZXdhcmVzLmNvbQp8fGlnY2QubmV0 +Ci5pZ2Z3Lm5ldAp8fGlnZncubmV0Ci5pZ2Z3LnRlY2gKfHxpZ2Z3LnRlY2gKLmln +bWcuZGUKfHxpZ25pdGVkZXRyb2l0Lm5ldAouaWdvdG1haWwuY29tLnR3Cnx8aWd2 +aXRhLmNvbQp8fGloYWtrYS5uZXQKLmloYW8ub3JnL2R6NQp8fGlpY25zLmNvbQou +aWtzdGFyLmNvbQp8fGlsaGFtdG9odGlpbnN0aXR1dGUub3JnCnx8aWxsdXNpb25m +YWN0b3J5LmNvbQp8fGlsb3ZlODAuYmUKfHxpbS50dgpAQHx8bXl2bG9nLmltLnR2 +Cnx8aW04OC50dwouaW1nY2hpbGkubmV0CnxodHRwOi8vaW1nY2hpbGkubmV0Ci5p +bWFnZWFiLmNvbQouaW1hZ2VmYXAuY29tCnx8aW1hZ2VmYXAuY29tCnx8aW1hZ2Vm +bGVhLmNvbQppbWFnZXNoYWNrLnVzCnx8aW1hZ2V2ZW51ZS5jb20KfHxpbWFnZXpp +bGxhLm5ldAouaW1iLm9yZwp8aHR0cDovL2ltYi5vcmcKCiEtLUlNREIKfGh0dHA6 +Ly93d3cuaW1kYi5jb20vbmFtZS9ubTA0ODI3MzAKLmltZGIuY29tL3RpdGxlL3R0 +MDgxOTM1NAouaW1kYi5jb20vdGl0bGUvdHQxNTQwMDY4Ci5pbWRiLmNvbS90aXRs +ZS90dDQ5MDg2NDQKCi5pbWcubHkKfHxpbWcubHkKLmltZ3VyLmNvbQp8fGltZ3Vy +LmNvbQouaW1rZXYuY29tCnx8aW1rZXYuY29tCi5pbWxpdmUuY29tCi5pbW1vcmFs +LmpwCmltcGFjdC5vcmcuYXUKaW1wcC5tbgp8aHR0cDovL3RlY2gyLmluLmNvbS92 +aWRlby8KaW45OS5vcmcKaW4tZGlzZ3Vpc2UuY29tCi5pbmNhcGRucy5uZXQKLmlu +Y2xvYWsuY29tCnx8aW5jbG9hay5jb20KfHxpbmNyZWRpYm94LmZyCnx8aW5kZXBl +bmRlbnQuY28udWsKfHxpbmRpYWJsb29tcy5jb20KfHxpbmRpYW5kZWZlbnNlbmV3 +cy5pbgp8fGluZGlhbmFycmF0aXZlLmNvbQp8fHRpbWVzb2ZpbmRpYS5pbmRpYXRp +bWVzLmNvbQouaW5kaWVtZXJjaC5jb20KfHxpbmRpZW1lcmNoLmNvbQppbmZvLWdy +YWYuZnIKd2Vic2l0ZS5pbmZvcm1lci5jb20KfHxpbml0aWF0aXZlc2ZvcmNoaW5h +Lm9yZwouaW5rdWkuY29tCi5pbm1lZGlhaGsubmV0Cnx8aW5tZWRpYWhrLm5ldAp8 +fGlubmVybW9uZ29saWEub3JnCnx8aW5vcmVhZGVyLmNvbQouaW5vdGUudHcKLmlu +c2VjYW0ub3JnCnxodHRwOi8vaW5zZWNhbS5vcmcKfHxpbnNpZGUuY29tLnR3Cnx8 +aW5zaWRldm9hLmNvbQouaW5zdGl0dXQtdGliZXRhaW4ub3JnCnxodHRwOi8vaW50 +ZXJuZXQub3JnLwppbnRlcm5ldGRlZmVuc2VsZWFndWUub3JnCmludGVybmV0ZnJl +ZWRvbS5vcmcKIS0tfHxpbnRlcnBvbC5pbnQKfHxpbnRlcm5ldHBvcGN1bHR1cmUu +Y29tCi5pbnRoZW5hbWVvZmNvbmZ1Y2l1c21vdmllLmNvbQp8fGludGhlbmFtZW9m +Y29uZnVjaXVzbW92aWUuY29tCmlueGlhbi5jb20KfHxpbnhpYW4uY29tCmlwYWx0 +ZXIuY29tCiEtLXx8aXBjZi5vcmcudHcKLmlwZmlyZS5vcmcKfHxpcGhvbmU0aG9u +Z2tvbmcuY29tCnx8aXBob25laGFja3MuY29tCnx8aXBob25ldGFpd2FuLm9yZwp8 +fGlwaG9uaXguZnIKfHxpcGljdHVyZS5ydQouaXBqZXRhYmxlLm5ldAp8fGlwamV0 +YWJsZS5uZXQKLmlwb2Jhci5jb20vcmVhZC5waHA/Cmlwb29jay5jb20vaW1nCi5p +cG9ydGFsLm1lCnxodHRwOi8vaXBvcnRhbC5tZQp8fGlwcG90di5jb20KLmlwcmVk +YXRvci5zZQp8fGlwcmVkYXRvci5zZQouaXB0di5jb20udHcKfHxpcHR2YmluLmNv +bQp8fGlwdmFuaXNoLmNvbQppcmVkbWFpbC5vcmcKY2hpbmVzZS5pcmliLmlyCnx8 +aXJvbmJpZ2Zvb2xzLmNvbXB5dGhvbi5uZXQKfHxpcm9ucHl0aG9uLm5ldAouaXJv +bnNvY2tldC5jb20KfHxpcm9uc29ja2V0LmNvbQouaXMuZ2QKLmlzbGFoaGFiZXIu +bmV0Ci5pc2xhbS5vcmcuaGsKfGh0dHA6Ly9pc2xhbS5vcmcuaGsKLmlzbGFtYXdh +cmVuZXNzLm5ldC9Bc2lhL0NoaW5hCi5pc2xhbWhvdXNlLmNvbQp8fGlzbGFtaG91 +c2UuY29tCi5pc2xhbWljaXR5LmNvbQouaXNsYW1pY3BsdXJhbGlzbS5vcmcKLmlz +bGFtdG9kYXkubmV0Ci5pc2FhY21hby5jb20KfHxpc2FhY21hby5jb20KfHxpc2dy +ZWF0Lm9yZwp8fGlzbWFlbGFuLmNvbQouaXNtYWxsdGl0cy5jb20KfHxpc21wcm9m +ZXNzaW9uYWwubmV0Cmlzb2h1bnQuY29tCnx8aXNyYWJveC5jb20KLmlzc3V1LmNv +bQp8fGlzc3V1LmNvbQouaXN0YXJzLmNvLm56Cm92ZXJzZWEuaXN0YXJzaGluZS5j +b20KfHxvdmVyc2VhLmlzdGFyc2hpbmUuY29tCmJsb2cuaXN0ZWYuaW5mby8yMDA3 +LzEwLzIxL215ZW50dW5uZWwKLmlzdGlxbGFsaGV3ZXIuY29tCi5pc3RvY2twaG90 +by5jb20KaXN1bmFmZmFpcnMuY29tCmlzdW50di5jb20KfHxpc3VwcG9ydHV5Z2h1 +cnMub3JnCml0YWJvby5pbmZvCnx8aXRhYm9vLmluZm8KLml0YWxpYXRpYmV0Lm9y +Zwpkb3dubG9hZC5pdGhvbWUuY29tLnR3Cml0aGVscC5pdGhvbWUuY29tLnR3Cnx8 +aXRzaGlkZGVuLmNvbQouaXRza3kuaXQKLml0d2VldC5uZXQKfGh0dHA6Ly9pdHdl +ZXQubmV0Ci5pdTQ1LmNvbQouaXVocmRmLm9yZwp8fGl1aHJkZi5vcmcKLml1a3Nr +eS5jb20KLml2YWN5LmNvbQp8fGl2YWN5LmNvbQouaXZlcnljZC5jb20KLml2cG4u +bmV0CiEtLXx8aXZwbi5uZXQKfHxpeHF1aWNrLmNvbQouaXh4eC5jb20KLml5b3Vw +b3J0LmNvbQp8fGl5b3Vwb3J0LmNvbQp8fGl5b3Vwb3J0Lm9yZwouaXphb2Jhby51 +cwp8fGdtb3pvbWcuaXppaG9zdC5vcmcKLml6bGVzLm5ldAouaXpsZXNlbS5vcmcK +CiEtLS0tLS0tLS0tLS0tLS0tLS0tLUpKLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LQp8fGoubXAKfHxqYWJsZS50dgpibG9nLmphY2tqaWEuY29tCmphbWFhdC5vcmcK +fHxqYW1lc3Rvd24ub3JnCi5qYW15YW5nbm9yYnUuY29tCnxodHRwOi8vamFteWFu +Z25vcmJ1LmNvbQouamFuZHl4LmNvbQp8fGphbndvbmdwaG90by5jb20KfHxqYXBh +bi13aG9yZXMuY29tCi5qYXYuY29tCi5qYXYxMDEuY29tCi5qYXYyYmUuY29tCnx8 +amF2MmJlLmNvbQouamF2NjgudHYKLmphdmFraWJhLm9yZwp8aHR0cDovL2phdmFr +aWJhLm9yZwouamF2YnVzLmNvbQp8fGphdmJ1cy5jb20KfHxqYXZmb3IubWUKLmph +dmhkLmNvbQouamF2aGlwLmNvbQouamF2bW9iaWxlLm5ldAp8aHR0cDovL2phdm1v +YmlsZS5uZXQKLmphdm1vby5jb20KLmphdnNlZW4uY29tCnxodHRwOi8vamF2c2Vl +bi5jb20KamJ0YWxrcy5jYwpqYnRhbGtzLmNvbQpqYnRhbGtzLm15Ci5qZHdzeS5j +b20KamVhbnlpbS5jb20KfHxqZnF1MzYuY2x1Ygp8fGpmcXUzNy54eXoKfHxqZ29v +ZGllcy5jb20KLmppYW5nd2VpcGluZy5jb20KfHxqaWFuZ3dlaXBpbmcuY29tCnx8 +amlhb3lvdTguY29tCnx8amljaGFuZ3RqLmNvbQouamllaHVhLmN6Cnx8aGsuamll +cGFuZy5jb20KfHx0dy5qaWVwYW5nLmNvbQpqaWVzaGliYW9iYW8uY29tCi5qaWdn +bGVnaWZzLmNvbQo1NmN1bjA0LmppZ3N5LmNvbQpqaWdvbmcxMDI0LmNvbQpkYW9k +dTE0LmppZ3N5LmNvbQpzcGVjeGluemwuamlnc3kuY29tCndsY25ldy5qaWdzeS5j +b20KLmppaGFkb2xvZ3kubmV0CnxodHRwOi8vamloYWRvbG9neS5uZXQKamluYnVz +aGUub3JnCnx8amluYnVzaGUub3JnCi5qaW5nc2ltLm9yZwp6aGFvLmppbmhhaS5k +ZQpqaW5ncGluLm9yZwp8fGppbmdwaW4ub3JnCmppbnBpYW53YW5nLmNvbQouamlu +cm91a29uZy5jb20KYWMuamlydWFuLm5ldAp8fGppdG91Y2guY29tCi5qaXp6dGhp +cy5jb20KampnaXJscy5jb20KLmprYi5jYwp8aHR0cDovL2prYi5jYwpqa2ZvcnVt +Lm5ldAp8fGptYS5nby5qcApyZXNlYXJjaC5qbXNjLmhrdS5oay9zb2NpYWwKd2Vp +Ym9zY29wZS5qbXNjLmhrdS5oawouam1zY3VsdC5jb20KfGh0dHA6Ly9qbXNjdWx0 +LmNvbQp8fGpvYWNoaW1zLm9yZwp8fGpvYnNvLnR2Ci5zdW53aW5pc20uam9pbmJi +cy5uZXQKfHxqb2luY2x1YmhvdXNlLmNvbQp8fGpvcm5hbGRhY2lkYWRlb25saW5l +LmNvbS5icgouam91cm5hbGNocmV0aWVuLm5ldAp8fGpvdXJuYWxvZmRlbW9jcmFj +eS5vcmcKLmpveW1paWh1Yi5jb20KLmpveW91cnNlbGYuY29tCmpwb3Bmb3J1bS5u +ZXQKfHxqc2RlbGl2ci5uZXQKfHxmaWRkbGUuanNoZWxsLm5ldAouanVidXNob3Vz +aGVuLmNvbQp8fGp1YnVzaG91c2hlbi5jb20KIS0tRG9hbWluIHBhcmtpbmcKLmp1 +aHVhcmVuLmNvbQp8fGp1bGllcmV5Yy5jb20KfHxqdW5hdXphLmNvbQouanVuZTRj +b21tZW1vcmF0aW9uLm9yZwouanVuZWZvdXJ0aC0yMC5uZXQKfHxqdW5lZm91cnRo +LTIwLm5ldAp8fGJicy5qdW5nbG9iYWwubmV0Ci5qdW9hYS5jb20KfGh0dHA6Ly9q +dW9hYS5jb20KanVzdGZyZWV2cG4uY29tCnx8anVzdGhvc3QucnUKLmp1c3RpY2Vm +b3J0ZW56aW4ub3JnCmp1c3RwYXN0ZS5pdAp8fGp1c3RteXNvY2tzMS5uZXQKanVz +dHRyaXN0YW4uY29tCmp1eXVhbmdlLm9yZwpqdXppeXVlLmNvbQp8fGp1eml5dWUu +Y29tCnx8andtdXNpYy5vcmcKQEB8fG11c2ljLmp3bXVzaWMub3JnCi5qeXhmLm5l +dAoKIS0tLS0tLS0tLS0tLS0tLS0tLS0tS0stLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tCnx8ay1kb3VqaW4ubmV0Cnx8a2Etd2FpLmNvbQp8fGthZG9rYXdhLmNvLmpw +Ci5rYWd5dS5vcmcKfHxrYWd5dS5vcmcuemEKLmthZ3l1bW9ubGFtLm9yZwoua2Fn +eXVuZXdzLmNvbS5oawoua2FneXVvZmZpY2Uub3JnCnx8a2FneXVvZmZpY2Uub3Jn +Cnx8a2FneXVvZmZpY2Uub3JnLnR3Ci5rYWl5dWFuLmRlCi5rYWthby5jb20KfHxr +YWthby5jb20KLmthbGFjaGFrcmFsdWdhbm8ub3JnCi5rYW5rYW4udG9kYXkKLmth +bm5ld3lvcmsuY29tCnx8a2FubmV3eW9yay5jb20KLmthbnNoaWZhbmcuY29tCnx8 +a2Fuc2hpZmFuZy5jb20KfHxrYW50aWUub3JnCmthbnpob25nZ3VvLmNvbQprYW56 +aG9uZ2d1by5ldQoua2FvdGljLmNvbQp8fGthb3RpYy5jb20KfHxrYXJheW91LmNv +bQprYXJraHVuZy5jb20KLmthcm1hcGEub3JnCi5rYXJtYXBhLXRlYWNoaW5ncy5v +cmcKfHxrYXdhc2UuY29tCi5rYmEtdHgub3JnCi5rY29vbG9ubGluZS5jb20KLmtl +YnJ1bS5jb20KfHxrZWJydW0uY29tCi5rZWNoYXJhLmNvbQoua2VlcGFuZHNoYXJl +LmNvbS92aXNpdC92aXNpdF9wYWdlLnBocD9pPTY4ODE1NAohLS18fGtlZXB2aWQu +Y29tCi5rZWV6bW92aWVzLmNvbQoua2VuZGluY29zLm5ldAoua2VuZW5nYmEuY29t +Cnx8a2VuZW5nYmEuY29tCnx8a2VvbnRlY2gubmV0Ci5rZXBhcmQuY29tCnx8a2Vw +YXJkLmNvbQp3aWtpLmtlc28uY24vSG9tZQp8fGtleWNkbi5jb20KLmtoYWJkaGEu +b3JnCi5raG11c2ljLmNvbS50dwp8fGtpY2hpa3UtZG91amlua28uY29tCi5raWsu +Y29tCnx8a2lrLmNvbQpiYnMua2lteS5jb20udHcKLmtpbmRsZXJlbi5jb20KfGh0 +dHA6Ly9raW5kbGVyZW4uY29tCnxodHRwOi8vd3d3LmtpbmRsZXJlbi5jb20KLmtp +bmdkb21zYWx2YXRpb24ub3JnCnx8a2luZ2RvbXNhbHZhdGlvbi5vcmcKa2luZ2hv +c3QuY29tCiEtLS5raW5nc3RvbmUuY29tLnR3L2Jvb2svCnx8a2luZ3N0b25lLmNv +bS50dwoua2luay5jb20KLmtpbm9rdW5peWEuY29tCnx8a2lub2t1bml5YS5jb20K +a2lsbHdhbGwuY29tCnx8a2lsbHdhbGwuY29tCnx8a2lubWVuLnRyYXZlbAoua2ly +LmpwCi5raXNzYmJhby5jbgp8aHR0cDovL2tpd2kua3oKfHxray13aHlzLmNvLmpw +CiEtLXx8a210Lm9yZy50dwoua211aC5vcmcudHcKLmtub3dsZWRnZXJ1c2guY29t +L2tyL2VuY3ljbG9wZWRpYQp8fGtub3d5b3VybWVtZS5jb20KLmtvYm8uY29tCnx8 +a29iby5jb20KLmtvYm9ib29rcy5jb20KfHxrb2JvYm9va3MuY29tCnx8a29kaW5n +ZW4uY29tCkBAfHx3d3cua29kaW5nZW4uY29tCnx8a29tcG96ZXIubmV0Ci5rb25h +Y2hhbi5jb20KfHxrb25hY2hhbi5jb20KLmtvbmUuY29tCnx8a29vbHNvbHV0aW9u +cy5jb20KLmtvb3Juay5jb20KfHxrb29ybmsuY29tCnx8a29yYW5tYW5kYXJpbi5j +b20KLmtvcmVuYW4yLmNvbQp8fGtxZXMubmV0CnxodHRwOi8vZ29qZXQua3J0Y28u +Y29tLnR3Ci5rc2RsLm9yZwoua3NuZXdzLmNvbS50dwp8fGt0emhrLmNvbQoua3Vp +Lm5hbWUvZXZlbnQKfHxrdWt1a3UudWsKa3VuLmltCi5rdXJhc2hzdWx0YW4uY29t +Cnx8a3VydG11bmdlci5jb20Ka3Vzb2NpdHkuY29tCnx8a3djZy5jYQp8fGt3b2s3 +LmNvbQoua3dvbmd3YWguY29tLm15Cnx8a3dvbmd3YWguY29tLm15Ci5reHN3Lmxp +ZmUKfHxreHN3LmxpZmUKLmt5b2Z1bi5jb20Ka3lvaGsubmV0Cnx8a3lveXVlLmNv +bQoua3l6eWhlbGxvLmNvbQp8fGt5enloZWxsby5jb20KLmt6ZW5nLmluZm8KfHxr +emVuZy5pbmZvCgohLS0tLS0tLS0tLS0tLS0tLS0tLS1MTC0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0KbGEtZm9ydW0ub3JnCmxhZGJyb2tlcy5jb20KfHxsYWJpZW5u +YWxlLm9yZwoubGFncmFuZXBvY2EuY29tCnx8bGFncmFuZXBvY2EuY29tCnx8bGFs +YS5pbQoubGFsdWxhbHUuY29tCi5sYW1hLmNvbS50dwp8fGxhbWEuY29tLnR3Ci5s +YW1heWVzaGUuY29tCnxodHRwOi8vbGFtYXllc2hlLmNvbQp8aHR0cDovL3d3dy5s +YW1lbmh1LmNvbQoubGFtbmlhLmNvLnVrCnx8bGFtbmlhLmNvLnVrCmxhbXJpbS5j +b20KfHxsYW5kb2Zob3BlLnR2Ci5sYW50ZXJuY24uY24KfGh0dHA6Ly9sYW50ZXJu +Y24uY24KLmxhbnRvc2ZvdW5kYXRpb24ub3JnCi5sYW9kLmNuCnxodHRwOi8vbGFv +ZC5jbgpsYW9nYWkub3JnCnx8bGFvZ2FpLm9yZwp8fGxhb2dhaXJlc2VhcmNoLm9y +ZwpsYW9taXUuY29tCi5sYW95YW5nLmluZm8KfGh0dHA6Ly9sYW95YW5nLmluZm8K +fHxsYXB0b3Bsb2NrZG93bi5jb20KLmxhcWluZ2Rhbi5uZXQKfHxsYXFpbmdkYW4u +bmV0Cnx8bGFyc2dlb3JnZS5jb20KLmxhc3Rjb21iYXQuY29tCnxodHRwOi8vbGFz +dGNvbWJhdC5jb20KfHxsYXN0Zm0uZXMKbGF0ZWxpbmVuZXdzLmNvbQp8fGxhdXNh +bi5oawp8fGxlLXZwbi5jb20KLmxlYWZ5dnBuLm5ldAp8fGxlYWZ5dnBuLm5ldAps +ZWVhby5jb20uY24vYmJzL2ZvcnVtLnBocAohLS18fGxlZWNoZXVreWFuLm9yZwps +ZWZvcmEuY29tCnx8bGVmdDIxLmhrCi5sZWdhbHBvcm5vLmNvbQoubGVnc2phcGFu +LmNvbQp8aHR0cDovL2xlaXJlbnR2LmNhCmxlaXN1cmVjYWZlLmNhCnx8bGVtYXRp +bi5jaAoubGVtb25kZS5mcgp8fGxlbndoaXRlLmNvbQp8fGxlb3JvY2t3ZWxsLmNv +bQpsZXJvc3VhLm9yZwp8fGxlcm9zdWEub3JnCmJsb2cubGVzdGVyODUwLmluZm8K +fHxsZXNvaXIuYmUKLmxldG91LmNvbQpsZXRzY29ycC5uZXQKfHxsZXRzY29ycC5u +ZXQKfHxvY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcKfHxzcy5sZXZ5aHN1LmNv +bQohNjkuMTYuMTc1LjQyCnx8Y2RuLmFzc2V0cy5sZnBjb250ZW50LmNvbQoubGhh +a2FyLm9yZwp8aHR0cDovL2xoYWthci5vcmcKLmxoYXNvY2lhbHdvcmsub3JnCi5s +aWFuZ3lvdS5uZXQKfHxsaWFuZ3lvdS5uZXQKLmxpYW55dWUubmV0Cnx8bGlhb3dh +bmd4aXphbmcubmV0Ci5saWFvd2FuZ3hpemFuZy5uZXQKfHxsaWJlcmFsLm9yZy5o +awoubGliZXJ0eXRpbWVzLmNvbS50dwpibG9ncy5saWJyYXJ5aW5mb3JtYXRpb250 +ZWNobm9sb2d5LmNvbS9qeHl6Cnx8bGlicmVkZC5pdAoubGlnaHRlbi5vcmcudHcK +LmxpZ2h0bm92ZWwuY24KQEB8aHR0cHM6Ly93d3cubGlnaHRub3ZlbC5jbgpsaW1p +YW8ubmV0Cmxpbmt1c3dlbGwuY29tCmFiaXRuby5saW5waWUuY29tL3VzZS1pcHY2 +LXRvLWZ1Y2stZ2Z3Cnx8bGluZS5tZQp8fGxpbmUtYXBwcy5jb20KLmxpbmdsaW5n +ZmEuY29tCnx8bGluZ3ZvZGljcy5jb20KLmxpbmstby1yYW1hLmNvbQp8aHR0cDov +L2xpbmstby1yYW1hLmNvbQp8fGxpbmtlZGluLmNvbQoubGlua2lkZW8uY29tCnx8 +YXBpLmxpbmtzYWxwaGEuY29tCnx8YXBpZG9jcy5saW5rc2FscGhhLmNvbQp8fHd3 +dy5saW5rc2FscGhhLmNvbQp8fGhlbHAubGlua3NhbHBoYS5jb20KfHxsaW51eC5v +cmcuaGsKbGludXh0b3kub3JnL2FyY2hpdmVzL2luc3RhbGxpbmctd2VzdC1jaGFt +YmVyLW9uLXVidW50dQoubGlvbnNyb2FyLmNvbQoubGlwdW1hbi5jb20KfHxsaXF1 +aWR2cG4uY29tCnx8Z3JlYXRmaXJlLnVzNy5saXN0LW1hbmFnZS5jb20KfHxsaXN0 +ZW5ub3Rlcy5jb20KfHxsaXN0ZW50b3lvdXR1YmUuY29tCmxpc3RvcmlvdXMuY29t +Ci5saXUteGlhb2JvLm9yZwp8fGxpdWRlanVuLmNvbQoubGl1aGFueXUuY29tCi5s +aXVqaWFuc2h1LmNvbQp8fGxpdWppYW5zaHUuY29tCi5saXV4aWFvYm8ubmV0Cnxo +dHRwOi8vbGl1eGlhb2JvLm5ldApsaXV4aWFvdG9uZy5jb20KfHxsaXV4aWFvdG9u +Zy5jb20KLmxpdmVkb29yLmpwCi5saXZlbGVhay5jb20KfHxsaXZlbGVhay5jb20K +fHxsaXZlbWludC5jb20KLmxpdmVzdGF0aW9uLmNvbQpsaXZlc3RyZWFtLmNvbQp8 +fGxpdmVzdHJlYW0uY29tCnx8bGl2aW5nb25saW5lLnVzCnx8bGl2aW5nc3RyZWFt +LmNvbQp8fGxpdmV2aWRlby5jb20KLmxpdmV2aWRlby5jb20KLmxpd2FuZ3lhbmcu +Y29tCmxpemhpemh1YW5nYmkuY29tCmxrY24ubmV0Ci5sbHNzLm1lLwp8fGxuY24u +b3JnCi5sb2FkLnRvCi5sb2JzYW5nd2FuZ3lhbC5jb20KLmxvY2FsZG9tYWluLndz +Cnx8bG9jYWxkb21haW4ud3MKbG9jYWxwcmVzc2hrLmNvbQp8fGxvY2tlc3Rlay5j +b20KbG9nYm90Lm5ldAp8fGxvZ2lxeC5jb20Kc2VjdXJlLmxvZ21laW4uY29tCnx8 +c2VjdXJlLmxvZ21laW4uY29tCnx8bG9nb3MuY29tLmhrCi5sb25kb25jaGluZXNl +LmNhCi5sb25naGFpci5oawpsb25nbXVzaWMuY29tCnx8bG9uZ3Rlcm1seS5uZXQK +fHxsb29rcGljLmNvbQoubG9va3Rvcm9udG8uY29tCnxodHRwOi8vbG9va3Rvcm9u +dG8uY29tCi5sb3RzYXdhaG91c2Uub3JnL3RpYmV0YW4tbWFzdGVycy9mb3VydGVl +bnRoLWRhbGFpLWxhbWEKLmxvdHVzbGlnaHQub3JnLmhrCi5sb3R1c2xpZ2h0Lm9y +Zy50dwpoa3JlcG9ydGVyLmxvdmVkLmhrCiEtLTQwMz8KfHxscHNnLmNvbQp8fGxy +ZnouY29tCi5scmlwLm9yZwp8fGxyaXAub3JnCi5sc2Qub3JnLmhrCnx8bHNkLm9y +Zy5oawpsc2ZvcnVtLm5ldAoubHNtLm9yZwp8fGxzbS5vcmcKLmxzbWNoaW5lc2Uu +b3JnCnx8bHNtY2hpbmVzZS5vcmcKLmxzbWtvcmVhbi5vcmcKfHxsc21rb3JlYW4u +b3JnCi5sc21yYWRpby5jb20vcmFkX2FyY2hpdmVzCi5sc213ZWJjYXN0LmNvbQou +bHRuLmNvbS50dwp8fGx0bi5jb20udHcKfHxsdWNreWRlc2lnbmVyLnNwYWNlCi5s +dWtlNTQuY29tCi5sdWtlNTQub3JnCi5sdXBtLm9yZwp8fGx1cG0ub3JnCnx8bHVz +aHN0b3JpZXMuY29tCmx1eGViYy5jb20KbHZoYWkub3JnCnx8bHZoYWkub3JnCnx8 +bHZ2Mi5jb20KLmx5ZmhrLm5ldAp8aHR0cDovL2x5ZmhrLm5ldAp8fGx6anNjcmlw +dC5jb20KLmx6bXRuZXdzLm9yZwp8fGx6bXRuZXdzLm9yZwoKIS0tLS0tLS0tLS0t +LS0tLS0tLS0tTU0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmh0dHA6Ly8qLm0t +dGVhbS5jYwohLS1tLXRlYW0uY2MvZm9ydW0KLm1hY3JvdnBuLmNvbQptYWN0cy5j +b20udHcKfHxtYWQtYXIuY2gKfHxtYWRyYXUuY29tCnx8bWFkdGh1bWJzLmNvbQp8 +fG1hZ2ljLW5ldC5pbmZvCm1haGFib2RoaS5vcmcKbXkubWFpbC5ydQoubWFpcGx1 +cy5jb20KfGh0dHA6Ly9tYWlwbHVzLmNvbQoubWFpemhvbmcub3JnCm1ha2thaG5l +d3NwYXBlci5jb20KLm1hbWluZ3poZS5jb20KbWFuaWN1cjRpay5ydQp8fG1hbnl2 +b2ljZXMubmV3cwoubWFwbGV3LmNvbQp8aHR0cDovL21hcGxldy5jb20KfHxtYXJj +LmluZm8KbWFyZ3Vlcml0ZS5zdQp8fG1hcnRpbmNhcnRvb25zLmNvbQptYXNrZWRp +cC5jb20KLm1haWlvLm5ldAoubWFpbC1hcmNoaXZlLmNvbQoubWFsYXlzaWFraW5p +LmNvbQp8fG1ha2VteW1vb2QuY29tCi5tYW5jaHVrdW8ubmV0Ci5tYW5pYXNoLmNv +bQp8aHR0cDovL21hbmlhc2guY29tCi5tYW5zaW9uLmNvbQoubWFuc2lvbnBva2Vy +LmNvbQohLS18fG1hcmluZXMubWlsCiEtLW1hcmttYWlsLm9yZyptZXNzYWdlCnx8 +bWFydGF1LmNvbQp8aHR0cDovL2Jsb2cubWFydGlub2VpLmNvbQoubWFydHNhbmdr +YWd5dW9mZmljaWFsLm9yZwp8aHR0cDovL21hcnRzYW5na2FneXVvZmZpY2lhbC5v +cmcKbWFydXRhLmJlL2ZvcmdldAoubWFyeGlzdC5jb20KfHxtYXJ4aXN0Lm5ldAou +bWFyeGlzdHMub3JnL2NoaW5lc2UKIS0tfHxtYXNoYWJsZS5jb20KfHxtYXRhaW5q +YS5jb20KfHxtYXRoYWJsZS5pbwp8fG1hdGhpZXctYmFkaW1vbi5jb20KfHxtYXRy +aXgub3JnCnx8bWF0c3VzaGltYWthZWRlLmNvbQp8aHR0cDovL21hdHVyZWpwLmNv +bQptYXlpbWF5aS5jb20KLm1heGluZy5qcAoubWNhZi5lZQp8aHR0cDovL21jYWYu +ZWUKfHxtY2FkZm9ydW1zLmNvbQptY2ZvZy5jb20KbWNyZWFzaXRlLmNvbQoubWQt +dC5vcmcKfHxtZC10Lm9yZwp8fG1lYW5zeXMuY29tCi5tZWRpYS5vcmcuaGsKLm1l +ZGlhY2hpbmVzZS5jb20KfHxtZWRpYWNoaW5lc2UuY29tCi5tZWRpYWZpcmUuY29t +Lz8KLm1lZGlhZmlyZS5jb20vZG93bmxvYWQKLm1lZGlhZnJlYWtjaXR5LmNvbQp8 +fG1lZGlhZnJlYWtjaXR5LmNvbQoubWVkaXVtLmNvbQp8fG1lZGl1bS5jb20KLm1l +ZXRhdi5jb20KfHxtZWV0dXAuY29tCm1lZmVlZGlhLmNvbQpqaWhhZGludGVsLm1l +Zm9ydW0ub3JnCnx8bWVnYS5jby5uegp8fG1lZ2EuaW8KfHxtZWdhLm56Cnx8bWVn +YXByb3h5LmNvbQp8fG1lZ2Fyb3RpYy5jb20KbWVnYXZpZGVvLmNvbQp8fG1lZ3Vy +aW5lbHVrYS5jb20KfHxtZWl6aG9uZy5ibG9nCnx8bWVpemhvbmcucmVwb3J0Ci5t +ZWx0b2RheS5jb20KLm1lbWVoay5jb20KfHxtZW1laGsuY29tCm1lbW9yeWJicy5j +b20KLm1lbXJpLm9yZwoubWVtcmlqdHRtLm9yZwp8fG1lcmNkbi5uZXQKLm1lcmN5 +cHJvcGhldC5vcmcKfHxtZXJjeXByb3BoZXQub3JnCnx8bWVyZ2Vyc2FuZGlucXVp +c2l0aW9ucy5vcmcKLm1lcmlkaWFuLXRydXN0Lm9yZwp8fG1lcmlkaWFuLXRydXN0 +Lm9yZwoubWVyaXBldC5iaXoKfHxtZXJpcGV0LmJpegoubWVyaXBldC5jb20KfHxt +ZXJpcGV0LmNvbQp8fG1lcml0LXRpbWVzLmNvbS50dwptZXNocmVwLmNvbQoubWVz +b3R3LmNvbS9iYnMKbWV0YWNhZmUuY29tL3dhdGNoCnx8bWV0YWZpbHRlci5jb20K +fHxtZXRlb3JzaG93ZXJzb25saW5lLmNvbQp8fG1ldHJvLnRhaXBlaQoubWV0cm9o +ay5jb20uaGsvP2NtZD1kZXRhaWwmY2F0ZWdvcnlJRD0yCnx8bWV0cm9saWZlLmNh +Ci5tZXRyb3JhZGlvLmNvbS5oawp8aHR0cDovL21ldHJvcmFkaW8uY29tLmhrCnx8 +bWV3ZS5jb20KbWV5b3UuanAKLm1leXVsLmNvbQp8fG1nb29uLmNvbQp8fG1nc3Rh +Z2UuY29tCnx8bWg0dS5vcmcKbWhyYWRpby5vcmcKfGh0dHA6Ly9taWNoYWVsYW50 +aS5jb20KfHxtaWNoYWVsbWFya2V0bC5jb20KfGh0dHA6Ly9iYnMubWlrb2Nvbi5j +b20KLm1pY3JvdnBuLmNvbQp8aHR0cDovL21pY3JvdnBuLmNvbQptaWRkbGUtd2F5 +Lm5ldAoubWloay5oay9mb3J1bQoubWloci5jb20KbWlodWEub3JnCiEtLUlQCnx8 +bWlrZXNvbHR5cy5jb20KLm1pbHBoLm5ldAp8aHR0cDovL21pbHBoLm5ldAoubWls +c3VycHMuY29tCm1pbWlhaS5uZXQKLm1pbWl2aXAuY29tCi5taW1pdnYuY29tCi5t +aW5kcm9sbGluZy5vcmcKfGh0dHA6Ly9taW5kcm9sbGluZy5vcmcKfHxtaW5nZGVt +ZWRpYS5vcmcKLm1pbmdodWkub3Iua3IKfGh0dHA6Ly9taW5naHVpLm9yLmtyCm1p +bmdodWkub3JnCnx8bWluZ2h1aS5vcmcKbWluZ2h1aS1hLm9yZwptaW5naHVpLWIu +b3JnCm1pbmdodWktc2Nob29sLm9yZwoubWluZ2ppbmdsaXNoaS5jb20KfHxtaW5n +amluZ2xpc2hpLmNvbQptaW5namluZ25ld3MuY29tCnx8bWluZ2ppbmd0aW1lcy5j +b20KLm1pbmdwYW8uY29tCnx8bWluZ3Bhby5jb20KLm1pbmdwYW9jYW5hZGEuY29t +Ci5taW5ncGFvbW9udGhseS5jb20KfGh0dHA6Ly9taW5ncGFvbW9udGhseS5jb20K +bWluZ3Bhb25ld3MuY29tCi5taW5ncGFvbnkuY29tCi5taW5ncGFvc2YuY29tCi5t +aW5ncGFvdG9yLmNvbQoubWluZ3Bhb3Zhbi5jb20KLm1pbmdzaGVuZ2Jhby5jb20K +Lm1pbmhodWUubmV0Ci5taW5pZm9ydW0ub3JnCi5taW5pc3RyeWJvb2tzLm9yZwou +bWluemh1aHVhLm5ldAp8fG1pbnpodWh1YS5uZXQKbWluemh1emhhbnhpYW4uY29t +Cm1pbnpodXpob25nZ3VvLm9yZwp8fG1pcm9ndWlkZS5jb20KbWlycm9yYm9va3Mu +Y29tCnx8bWlycm9ybWVkaWEubWcKLm1pc3QudmlwCnx8dGhlY2VudGVyLm1pdC5l +ZHUKfHxzY3JhdGNoLm1pdC5lZHUKLm1pdGFvLmNvbS50dwoubWl0YmJzLmNvbQp8 +fG1pdGJicy5jb20KbWl0YmJzYXUuY29tCi5taXhlcm8uY29tCnx8bWl4ZXJvLmNv +bQp8fG1peGkuanAKbWl4cG9kLmNvbQoubWl4eC5jb20KfHxtaXh4LmNvbQp8fG1p +enptb25hLmNvbQoubWs1MDAwLmNvbQoubWxjb29sLmNvbQp8fG1senMud29yawou +bW0tY2cuY29tCnx8bW1hYXh4LmNvbQoubW1tY2EuY29tCm1uZXdzdHYuY29tCnx8 +bW9iYXRlay5uZXQKLm1vYmlsZTAxLmNvbQp8fG1vYmlsZTAxLmNvbQp8fG1vYmls +ZXdheXMuZGUKLm1vYnlwaWN0dXJlLmNvbQp8aHR0cDovL21vYnkudG8KfHxtb2Qu +aW8KfHxtb2Rlcm5jaGluYXN0dWRpZXMub3JnCnx8bW9lZXJvbGlicmFyeS5jb20K +d2lraS5tb2VnaXJsLm9yZwoubW9mYXhpZWh1aS5jb20KLm1vZm9zLmNvbQp8fG1v +Zy5jb20KfHxtb2h1LnJvY2tzCm1vbGlodWEub3JnCnx8bW9uZGV4Lm9yZwoubW9u +ZXktbGluay5jb20udHcKfGh0dHA6Ly9tb25leS1saW5rLmNvbS50dwp8aHR0cDov +L3d3dy5tb25sYW1pdC5vcmcKfHxtb29uLmZtCi5tb29uYmJzLmNvbQp8fG1vb25i +YnMuY29tCnx8bW9wdHQudHcKfHxtb25pdG9yY2hpbmEub3JnCmJicy5tb3JiZWxs +LmNvbQp8fG1vcm5pbmdzdW4ub3JnCnx8bW9yb25ldGEuY29tCi5tb3RoZXJsZXNz +LmNvbQp8aHR0cDovL21vdGhlcmxlc3MuY29tCm1vdG9yNGlrLnJ1Ci5tb3VzZWJy +ZWFrZXIuY29tCiEtLXx8bW92YWJsZXR5cGUuY29tCi5tb3ZlbWVudHMub3JnCnx8 +bW92ZW1lbnRzLm9yZwp8fG1vdmllZmFwLmNvbQp8fHd3dy5tb3p0dy5vcmcKLm1w +M2J1c2NhZG9yLmNvbQp8fG1wZXR0aXMuY29tCi5tcGZpbmFuY2UuY29tCnx8bXBm +aW5hbmNlLmNvbQoubXBpbmV3cy5jb20KfHxtcGluZXdzLmNvbQptcG9ubGluZS5o +awoubXF4ZC5vcmcKfGh0dHA6Ly9tcXhkLm9yZwptcnR3ZWV0LmNvbQp8fG1ydHdl +ZXQuY29tCm5ld3MuaGsubXNuLmNvbQpuZXdzLm1zbi5jb20udHcKbXNndWFuY2hh +LmNvbQoubXN3ZTEub3JnCnxodHRwOi8vbXN3ZTEub3JnCnx8bXRocnVmLmNvbQp8 +fG11YmkuY29tCm11Y2hvc3Vja28uY29tCnx8bXVsdGlwbHkuY29tCm11bHRpcHJv +eHkub3JnCm11bHRpdXBsb2FkLmNvbQoubXVsbHZhZC5uZXQKfHxtdWxsdmFkLm5l +dAoubXVtbXlzZ29sZC5jb20KLm11cm11ci50dwp8aHR0cDovL211cm11ci50dwou +bXVzaWNhZGUubmV0Ci5tdXNsaW12aWRlby5jb20KfHxtdXppLmNvbQp8fG11emku +bmV0Cnx8bXg5ODEuY29tCi5teS1mb3Jtb3NhLmNvbQoubXktcHJveHkuY29tCi5t +eS1wcml2YXRlLW5ldHdvcmsuY28udWsKfHxteS1wcml2YXRlLW5ldHdvcmsuY28u +dWsKZm9ydW0ubXk5MDMuY29tCi5teWFjdGltZXMuY29tL2FjdGltZXMKfHxteWFu +bml1LmNvbQoubXlhdWRpb2Nhc3QuY29tCnx8bXlhdWRpb2Nhc3QuY29tCi5teWF2 +LmNvbS50dy9iYnMKLm15YmJzLnVzCi5teWNhMTY4LmNvbQoubXljYW5hZGFub3cu +Y29tCnx8YmJzLm15Y2hhdC50bwp8fG15Y2hpbmFteWhvbWUuY29tCi5teWNoaW5h +bXlob21lLmNvbQoubXljaGluYW5ldC5jb20KLm15Y2hpbmFuZXdzLmNvbQp8fG15 +Y2hpbmFuZXdzLmNvbQoubXljaGluZXNlLm5ld3MKfHxteWNubmV3cy5jb20KfHxt +eWtvbWljYS5vcmcKbXljb3VsZC5jb20vZGlzY3V6Ci5teWVhc3l0di5jb20KfHxt +eWVjbGlwc2VpZGUuY29tCi5teWZvcnVtLmNvbS5oawp8fG15Zm9ydW0uY29tLmhr +Cnx8bXlmb3J1bS5jb20udWsKLm15ZnJlZWNhbXMuY29tCi5teWZyZWVwYXlzaXRl +LmNvbQoubXlmcmVzaG5ldC5jb20KLm15aXBoaWRlLmNvbQp8fG15aXBoaWRlLmNv +bQpmb3J1bS5teW1hamkuY29tCm15bWVkaWFyb20uY29tL2ZpbGVzL2JveAp8fG15 +bW9lLm1vZQp8fG15bXVzaWMubmV0LnR3Cnx8bXlwYXJhZ2xpZGluZy5jb20KfHxt +eXBvcGVzY3UuY29tCm15cmFkaW8uaGsvcG9kY2FzdAoubXlyZWFkaW5nbWFuZ2Eu +aW5mbwpteXNpbmFibG9nLmNvbQoubXlzcGFjZS5jb20KIS0tLmJsb2dzLm15c3Bh +Y2UuY29tCiEtLXx8YmxvZ3MubXlzcGFjZS5jb20KIS0tdmlkcy5teXNwYWNlLmNv +bS9pbmRleC5jZm0/ZnVzZWFjdGlvbj12aWRzLgohLS12aWV3bW9yZXBpY3MubXlz +cGFjZS5jb20KfHxteXNwYWNlY2RuLmNvbQoubXl0YWxrYm94LmNvbQoubXl0aXpp +LmNvbQoKIS0tLS0tLS0tLS0tLS0tLS0tLS0tTk4tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0tCnx8bmFhY29hbGl0aW9uLm9yZwpvbGQubmFiYmxlLmNvbQp8fG5haXRp +ay5uZXQKLm5ha2lkby5jb20KfHxuYWtpZG8uY29tCi5uYWt1ei5jb20vYmJzCnx8 +bmFsYW5kYWJvZGhpLm9yZwp8fG5hbGFuZGF3ZXN0Lm9yZwoubmFtZ3lhbC5vcmcK +bmFtZ3lhbG1vbmFzdGVyeS5vcmcKfHxuYW1zaXNpLmNvbQoubmFueWFuZy5jb20K +fHxuYW55YW5nLmNvbQoubmFueWFuZ3Bvc3QuY29tCnx8bmFueWFuZ3Bvc3QuY29t +Ci5uYW56YW8uY29tCiEtLS5uYW56YW8uY29tL3NjL2NoaW5hLzIwMjIzCiEtLS5u +YW56YW8uY29tL3NjL2hrLW1hY2F1LXR3Ci5uYW9sLmNhCi5uYW9sLmNjCnVpZ2h1 +ci5uYXJvZC5ydQoubmF0Lm1vZQp8fG5hdC5tb2UKY3liZXJnaG9zdC5uYXRhZG8u +Y29tCnx8bmF0aW9uYWwtbG90dGVyeS5jby51awp8fG5hdGlvbmFsYXdha2VuaW5n +Lm9yZwp8fG5hdGlvbmFsaW50ZXJlc3Qub3JnCm5ld3MubmF0aW9uYWxnZW9ncmFw +aGljLmNvbS9uZXdzLzIwMTQvMDYvMTQwNjAzLXRpYW5hbm1lbi1zcXVhcmUKfHxu +YXRpb25hbHJldmlldy5jb20KLm5hdGlvbnNvbmxpbmUub3JnL29uZXdvcmxkL3Rp +YmV0Cnx8bGluZS5uYXZlci5qcAp8fG5hdnlmYW1pbHkubmF2eS5taWwKfHxuYXZ5 +cmVzZXJ2ZS5uYXZ5Lm1pbAp8fG5rby5uYXZ5Lm1pbAp8fHVzbm8ubmF2eS5taWwK +bmF3ZWVrbHl0aW1lcy5jb20KfHxuYmNuZXdzLmNvbQoubmJ0dnBuLmNvbQp8aHR0 +cDovL25idHZwbi5jb20KbmNjd2F0Y2gub3JnLnR3Ci5uY2guY29tLnR3Ci5uY24u +b3JnCnx8bmNocmQub3JnCnx8bmNuLm9yZwp8fGV0b29scy5uY29sLmNvbQoubmRl +LmRlCnx8bmRpLm9yZwoubmRyLmRlCi5uZWQub3JnCnx8bmVrb3Nsb3Zha2lhLm5l +dAp8fG5lb3dpbi5uZXQKfHxuZXB1c29rdS5jb20KfHxuZXQtZml0cy5wcm8KfHxu +ZXRhbGVydC5tZQohLS1iYnNuZXcubmV0YmlnLmNvbQpiYnMubmV0YmlnLmNvbQou +bmV0YmlyZHMuY29tCm5ldGNvbG9ueS5jb20KYm9saW4ubmV0ZmlybXMuY29tCnx8 +bmV0Zmxhdi5jb20KfHxuZXRtZS5jYwp8fG5ldHNhcmFuZy5jb20KbmV0c25lYWsu +Y29tCi5uZXR3b3JrNTQuY29tCm5ldHdvcmtlZGJsb2dzLmNvbQoubmV0d29ya3R1 +bm5lbC5uZXQKbmV2ZXJmb3JnZXQ4OTY0Lm9yZwpuZXctM2x1bmNoLm5ldAoubmV3 +LWFraWJhLmNvbQoubmV3OTYuY2EKLm5ld2NlbnR1cnltYy5jb20KfGh0dHA6Ly9u +ZXdjZW50dXJ5bWMuY29tCm5ld2NlbnR1cnluZXdzLmNvbQp8fG5ld2NoZW4uY29t +Ci5uZXdjaGVuLmNvbQoubmV3Z3JvdW5kcy5jb20KfHxuZXdoaWdobGFuZHZpc2lv +bi5jb20KbmV3aXBub3cuY29tCi5uZXdsYW5kbWFnYXppbmUuY29tLmF1Ci5uZXdu +ZXdzLmNhCm5ld3MxMDAuY29tLnR3Cm5ld3NjaGluYWNvbW1lbnQub3JnCi5uZXdz +Y24ub3JnCnx8bmV3c2NuLm9yZwpuZXdzcGVhay5jYy9zdG9yeQoubmV3c2FuY2Fp +LmNvbQp8fG5ld3NhbmNhaS5jb20KLm5ld3NkZXRveC5jYQoubmV3c2RoLmNvbQp8 +fG5ld3NtYXguY29tCnx8bmV3c3RhbWFnby5jb20KfHxuZXdzdGFwYS5vcmcKfHxu +ZXdzdGF0ZXNtYW4uY29tCm5ld3N0YXJuZXQuY29tCnx8bmV3c3dlZWsuY29tCi5u +ZXd0YWl3YW4uY29tLnR3Cm5ld3RhbGsudHcKfHxuZXd0YWxrLnR3Cnx8bmV3eW9y +a2VyLmNvbQpuZXd5b3JrdGltZXMuY29tCnx8bmV4b24uY29tCi5uZXh0MTEuY28u +anAKfHxuZXh0ZGlnaXRhbC5jb20uaGsKLm5leHRtYWcuY29tLnR3CgohLS1oayou +bmV4dG1lZGlhLmNvbQohLS10dyoubmV4dG1lZGlhLmNvbQohLS1zdGF0aWMqLm5l +eHRtZWRpYS5jb20KLm5leHRtZWRpYS5jb20KCnx8bmV4dG9uLW5ldC5qcApuZXh0 +dHYuY29tLnR3Ci5uZmp0eWQuY29tCnx8Y28ubmcubWlsCnx8bmdhLm1pbApuZ2Vu +c2lzLmNvbQp8fG5nb2R1cGRvbmdjaHVuZy5jb20KLm5oZW50YWkubmV0CnxodHRw +Oi8vbmhlbnRhaS5uZXQKLm5oay1vbmRlbWFuZC5qcAoubmljb3ZpZGVvLmpwL3dh +dGNoCnx8bmljb3ZpZGVvLmpwCnx8bmlnaG9zdC5vcmcKYXYubmlnaHRsaWZlMTQx +LmNvbQpuaW5lY29tbWVudGFyaWVzLmNvbQoubmluamFjbG9hay5jb20KfHxuaW5q +YXByb3h5Lm5pbmphCm5pbnRlbmRpdW0uY29tCnRhaXdhbnllcy5uaW5nLmNvbQp1 +c21ndGNnLm5pbmcuY29tL2ZvcnVtCnx8bml1c25ld3MuY29tCnx8bmphY3RiLm9y +ZwpuanVpY2UuY29tCnx8bmp1aWNlLmNvbQp8fG5sZnJlZXZwbi5jb20KfHxubXNs +LndlYnNpdGUKfHxubmV3cy5ldQoKIS0tbm8taXAuY29tI05PSVAKLmRkbnMubmV0 +LwouZ29vZGRucy5pbmZvCnx8Z290ZG5zLmNoCi5tYWlsZG5zLnh5egoubm8taXAu +b3JnCi5vcGVuZG4ueHl6Ci5zZXJ2ZWh0dHAuY29tCnN5dGVzLm5ldAoud2hvZG5z +Lnh5egouemFwdG8ub3JnCnxodHRwOi8vZHludXBkYXRlLm5vLWlwLmNvbS8KCnx8 +bm9iZWwuc2UKIS0tLm5vYmVscHJpemUub3JnCiEtLXxodHRwOi8vbm9iZWxwcml6 +ZS5vcmcKbm9iZWxwcml6ZS5vcmcvbm9iZWxfcHJpemVzL3BlYWNlL2xhdXJlYXRl +cy8xOTg5Cm5vYmVscHJpemUub3JnL25vYmVsX3ByaXplcy9wZWFjZS9sYXVyZWF0 +ZXMvMjAxMApub2JvZHljYW5zdG9wLnVzCnx8bm9ib2R5Y2Fuc3RvcC51cwp8fG5v +a29naXJpLm9yZwp8fG5va29sYS5jb20Kbm9vZGxldnBuLmNvbQoubm9yYnVsaW5n +a2Eub3JnCm5vcmR2cG4uY29tCnx8bm9yZHZwbi5jb20KfHxub3RlcGFkLXBsdXMt +cGx1cy5vcmcKfHxub3ZlbGFzaWEuY29tCi5uZXdzLm5vdy5jb20KfGh0dHA6Ly9u +ZXdzLm5vdy5jb20KIS0tfGh0dHA6Ly9uZXdzLm5vdy5jb20vaG9tZSoKbmV3cy5u +b3cuY29tJTJGaG9tZQp8fG5vd25ld3MuY29tCi5ub3d0b3JyZW50cy5jb20KLm5v +eXBmLmNvbQp8fG5veXBmLmNvbQp8fG5wYS5nby5qcAoubnBudC5tZQp8aHR0cDov +L25wbnQubWUKLm5wcy5nb3YKLm5yYWRpby5tZQp8aHR0cDovL25yYWRpby5tZQou +bnJrLm5vCnx8bnJrLm5vCi5udGQudHYKfHxudGQudHYKLm50ZHR2LmNvbQp8fG50 +ZHR2LmNvbQp8fG50ZHR2LmNvbS50dwoubnRkdHYuY28ua3IKbnRkdHYuY2EKbnRk +dHYub3JnCm50ZHR2LnJ1Cm50ZHR2bGEuY29tCi5udHJmdW4uY29tCnx8Y2JzLm50 +dS5lZHUudHcKfHxtZWRpYS5udS5ubAoubnViaWxlcy5uZXQKfHxudWV4cG8uY29t +Ci5udWtpc3RyZWFtLmNvbQp8fG51cmdvLXNvZnR3YXJlLmNvbQp8fG51dGFrdS5u +ZXQKfHxudXRzdnBuLndvcmsKLm51dmlkLmNvbQp8fG52ZHN0LmNvbQpudXpjb20u +Y29tCi5udnF1YW4ub3JnCi5udnRvbmd6aGlzaGVuZy5vcmcKfGh0dHA6Ly9udnRv +bmd6aGlzaGVuZy5vcmcKLm53dGNhLm9yZwp8aHR0cDovL255YWEuZXUKfHxueWFh +LnNpCnx8bnlib29rcy5jb20KLm55ZHVzLmNhCm55bG9uLWFuZ2VsLmNvbQpueWxv +bnN0b2NraW5nc29ubGluZS5jb20KfHxueXBvc3QuY29tCiEtLW55c2luZ3Rhby5j +b20KLm56Y2hpbmVzZS5jb20KfHxuemNoaW5lc2UubmV0Lm56CgohLS0tLS0tLS0t +LS0tLS0tLS0tLS1PTy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KfHxvYW5uLmNv +bQpvYnNlcnZlY2hpbmEubmV0Ci5vYnV0dS5jb20Kb2Nhc3Byby5jb20Kb2NjdXB5 +dGlhbmFubWVuLmNvbQpvY2xwLmhrCi5vY3JlYW1waWVzLmNvbQp8fG9jdG9iZXIt +cmV2aWV3Lm9yZwp8fG9keXNlZS5jb20Kb2ZmYmVhdGNoaW5hLmNvbQp8fG9mZmlj +ZW9mdGliZXQuY29tCnxodHRwOi8vb2ZpbGUub3JnCnx8b2dhb2dhLm9yZwp0d3Ry +MnNyYy5vZ2FvZ2Eub3JnCi5vZ2F0ZS5vcmcKfHxvZ2F0ZS5vcmcKd3d3Mi5vaGNo +ci5vcmcvZW5nbGlzaC9ib2RpZXMvY2F0L2RvY3Mvbmdvcy9JSV9DaGluYV80MS5w +ZGYKfHxvaG15cnNzLmNvbQoub2lrb3MuY29tLnR3L3Y0Ci5vaWt0di5jb20Kb2l6 +b2Jsb2cuY29tCi5vay5ydQp8fG9rLnJ1Ci5va2F5ZnJlZWRvbS5jb20KfHxva2F5 +ZnJlZWRvbS5jb20KfHxva2sudHcKfGh0dHA6Ly9maWxteS5vbGFibG9nYS5wbC9w +bGF5ZXIKb2xkLWNhdC5uZXQKfHxvbGV2b2QuY29tCnx8b2x1bXBvLmNvbQoub2x5 +bXBpY3dhdGNoLm9yZwp8fG9tY3Qub3JnCm9tZ2lsaS5jb20KfHxvbW5pdGFsay5j +b20KfHxvbW5pdGFsay5vcmcKfHxvbW55LmZtCmNsaW5nLm9teS5zZwpmb3J1bS5v +bXkuc2cKbmV3cy5vbXkuc2cKc2hvd2Jpei5vbXkuc2cKfHxvbi5jYwp8fG9uZWRy +aXZlLmxpdmUuY29tCnx8b25pb24uY2l0eQp8fG9uaW9uLmx5Ci5vbmxpbmVjaGEu +Y29tCnx8b25saW5leW91dHViZS5jb20KfHxvbmx5Z2F5dmlkZW8uY29tCi5vbmx5 +dHdlZXRzLmNvbQp8aHR0cDovL29ubHl0d2VldHMuY29tCm9ubW9vbi5uZXQKb25t +b29uLmNvbQoub250aGVodW50LmNvbQp8aHR0cDovL29udGhlaHVudC5jb20KLm9v +cHNmb3J1bS5jb20Kb3Blbi5jb20uaGsKb3BlbmFsbHdlYi5jb20Kb3BlbmRlbW9j +cmFjeS5uZXQKfHxvcGVuZGVtb2NyYWN5Lm5ldAoub3BlbmVydnBuLmluCm9wZW5p +ZC5uZXQKfHxvcGVuaWQubmV0Ci5vcGVubGVha3Mub3JnCnx8b3BlbmxlYWtzLm9y +Zwp8fG9wZW5zdHJlZXRtYXAub3JnCnx8b3BlbnRlY2guZnVuZApvcGVudnBuLm5l +dAp8fG9wZW52cG4ubmV0Cnx8b3BlbndlYnN0ZXIuY29tCi5vcGVud3J0Lm9yZy5j +bgpAQHx8b3BlbndydC5vcmcuY24KbXkub3BlcmEuY29tL2RhaGVtYQp8fGRlbW8u +b3BlcmEtbWluaS5uZXQKLm9wdXMtZ2FtaW5nLmNvbQp8aHR0cDovL29wdXMtZ2Ft +aW5nLmNvbQp3d3cub3JjaGlkYmJzLmNvbQoub3JnYW5jYXJlLm9yZy50dwpvcmdh +bmhhcnZlc3RpbnZlc3RpZ2F0aW9uLm5ldAoub3JnYXNtLmNvbQoub3JnZnJlZS5j +b20KfHxvcmljb24uY28uanAKfHxvcmllbnQtZG9sbC5jb20Kb3JpZW50YWxkYWls +eS5jb20ubXkKfHxvcmllbnRhbGRhaWx5LmNvbS5teQohLS1vcmllbnRhbGRhaWx5 +Lm9uLmNjCnx8b3JuLmpwCnQub3J6ZHJlYW0uY29tCnx8dC5vcnpkcmVhbS5jb20K +dHVpLm9yemRyZWFtLmNvbQp8fG9yemlzdGljLm9yZwp8fG9zZm9vcmEuY29tCi5v +dG5kLm9yZwp8fG90bmQub3JnCnx8b3R0by5kZQp8fG91cmRlYXJhbXkuY29tCm91 +cnNvZ28uY29tCi5vdXJzdGVwcy5jb20uYXUKfHxvdXJzdGVwcy5jb20uYXUKLm91 +cnN3ZWIubmV0Cnx8b3VydHYuaGsKeGlucWltZW5nLm92ZXItYmxvZy5jb20KfHxv +dmVyY2FzdC5mbQp8fG92ZXJkYWlseS5vcmcKfHxvdmVycGxheS5uZXQKc2hhcmUu +b3ZpLmNvbS9tZWRpYQp8fG92cG4uY29tCnxodHRwOi8vb3dsLmxpCnxodHRwOi8v +aHQubHkKfGh0dHA6Ly9odGwubGkKfGh0dHA6Ly9tYXNoLnRvCnd3dy5vd2luZC5j +b20KfHxvd2x0YWlsLmNvbQp8fG94Zm9yZHNjaG9sYXJzaGlwLmNvbQp8aHR0cDov +L3d3dy5veGlkLml0Cm95YXguY29tCm95Z2hhbi5jb20vd3BzCi5vemNoaW5lc2Uu +Y29tL2Jicwp8fG93Lmx5CmJicy5vemNoaW5lc2UuY29tCi5venZvaWNlLm9yZwp8 +fG96dm9pY2Uub3JnCi5venh3LmNvbQoub3p5b3lvLmNvbQoKIS0tLS0tLS0tLS0t +LS0tLS0tLS0tUFAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnx8cGFjaG9zdGlu +Zy5jb20KLnBhY2lmaWNwb2tlci5jb20KLnBhY2tldGl4Lm5ldAp8fHBhY29wYWNv +bWFtYS5jb20KLnBhZG1hbmV0LmNvbQpwYWdlMnJzcy5jb20KfHxwYWdvZGFib3gu +Y29tCi5wYWxhY2Vtb29uLmNvbQpmb3J1bS5wYWxtaXNsaWZlLmNvbQp8fGVyaXZl +cnNvZnQuY29tCi5wYWxkZW5neWFsLmNvbQpwYWxqb3JwdWJsaWNhdGlvbnMuY29t +Ci5wYWx0YWxrLmNvbQohLS18fHBhbmdjaS5uZXQKfHxwYW5kYXBvdy5jbwoucGFu +ZGFwb3cubmV0Ci5wYW5kYXZwbi1qcC5jb20KfHxwYW5kYXZwbi1qcC5jb20KfHxw +YW5kYXZwbnByby5jb20KLnBhbmx1YW4ubmV0Cnx8cGFubHVhbi5uZXQKfHxwYW8t +cGFvLm5ldApwYXBlci5saQpwYXBlcmIudXMKLnBhcmFkaXNlaGlsbC5jYwoucGFy +YWRpc2Vwb2tlci5jb20KfHxwYXJsZXIuY29tCnx8cGFyc2V2aWRlby5jb20KLnBh +cnR5Y2FzaW5vLmNvbQoucGFydHlwb2tlci5jb20KLnBhc3Npb24uY29tCnx8cGFz +c2lvbi5jb20KLnBhc3Npb250aW1lcy5oawpwYXN0ZWJpbi5jb20KLnBhc3RpZS5v +cmcKfHxwYXN0aWUub3JnCnx8YmxvZy5wYXRodG9zaGFyZXBvaW50LmNvbQp8fHBh +dHJlb24uY29tCnBicy5vcmcvd2diaC9wYWdlcy9mcm9udGxpbmUvdGFua21hbgpw +YnMub3JnL3dnYmgvcGFnZXMvZnJvbnRsaW5lL3RpYmV0CnZpZGVvLnBicy5vcmcK +CiEtLVBid2lraQpwYndpa2kuY29tCnx8cGJ3b3Jrcy5jb20KfHxkZXZlbG9wZXJz +LmJveC5uZXQKfHx3aWtpLm9hdXRoLm5ldAp8fHdpa2kucGhvbmVnYXAuY29tCnx8 +d2lraS5qcXVlcnl1aS5jb20KCnx8cGJ4ZXMuY29tCnx8cGJ4ZXMub3JnCnBjZHZk +LmNvbS50dwoucGNob21lLmNvbS50dwp8aHR0cDovL3BjaWoub3JnCi5wY3N0b3Jl +LmNvbS50dwp8fHBjdC5vcmcudHcKcGRldGFpbHMuY29tCnx8cGRwcm94eS5jb20K +fHxwZWFjZS5jYQpwZWFjZWZpcmUub3JnCnBlYWNlaGFsbC5jb20KfHxwZWFjZWhh +bGwuY29tCnxodHRwOi8vcGVhcmxoZXIub3JnCi5wZWVhc2lhbi5jb20KfHxwZWlu +Zy5uZXQKLnBla2luZ2R1Y2sub3JnCnx8cGVraW5nZHVjay5vcmcKLnBlbXVsaWhh +bi5vci5pZAp8aHR0cDovL3BlbXVsaWhhbi5vci5pZAp8fHBlbi5pbwpwZW5jaGlu +ZXNlLmNvbQp8fHBlbmNoaW5lc2UubmV0Ci5wZW5jaGluZXNlLm5ldApwZW5neXVs +b25nLmNvbQpwZW5pc2JvdC5jb20KfHxibG9nLnBlbnRhbG9naWMubmV0Ci5wZW50 +aG91c2UuY29tCi5wZW50b3kuaGsvJUU0JUI4JUFEJUU1JTlDJThCCi5wZW50b3ku +aGsvJUU2JTk5JTgyJUU0JUJBJThCCi5wZW9wbGVib29rY2FmZS5jb20KLnBlb3Bs +ZW5ld3MudHcKfHxwZW9wbGVuZXdzLnR3Ci5wZW9wby5vcmcKfHxwZW9wby5vcmcK +LnBlcmN5LmluCi5wZXJmZWN0Z2lybHMubmV0Cnx8cGVyZmVjdC1wcml2YWN5LmNv +bQoucGVyc2VjdXRpb25ibG9nLmNvbQoucGVyc2lhbmtpdHR5LmNvbQpwaGFwbHVh +bi5vcmcKLnBoYXl1bC5jb20KfHxwaGF5dWwuY29tCnBoaWxib3JnZXMuY29tCnBo +aWxseS5jb20KfHxwaG5jZG4uY29tCnx8cGhvdG9kaGFybWEubmV0Cnx8cGhvdG9m +b2N1cy5jb20KfHxwaHVxdW9jc2VydmljZXMuY29tCnx8cGljYWNvbWljY24uY29t +Ci5waWNpZGFlLm5ldAp8fGltZyoucGljdHVyZWRpcC5jb20KcGljdHVyZXNvY2lh +bC5jb20KfHxwaW4tY29uZy5jb20KLnBpbjYuY29tCnx8cGluNi5jb20KLnBpbmcu +Zm0KfHxwaW5nLmZtCnx8cGluaW1nLmNvbQoucGlua3JvZC5jb20KfHxwaW5veS1u +LmNvbQp8fHBpbnRlcmVzdC5hdAp8fHBpbnRlcmVzdC5jYQp8fHBpbnRlcmVzdC5j +by5rcgp8fHBpbnRlcmVzdC5jby51awoucGludGVyZXN0LmNvbQp8fHBpbnRlcmVz +dC5jb20KfHxwaW50ZXJlc3QuY29tLm14Cnx8cGludGVyZXN0LmRlCnx8cGludGVy +ZXN0LmRrCnx8cGludGVyZXN0LmZyCnx8cGludGVyZXN0LmpwCnx8cGludGVyZXN0 +Lm5sCnx8cGludGVyZXN0LnNlCi5waXBpaS50dgoucGlwb3NheS5jb20KcGlyYWF0 +dGlsYWh0aS5vcmcKLnBpcmluZy5jb20KfHxwaXhlbHFpLmNvbQp8fGNzcy5waXhu +ZXQuaW4KfHxwaXhuZXQubmV0Ci5waXhuZXQubmV0Ci5way5jb20KfHxwbGFjZW1p +eC5jb20KIS0tLnBsYW5ldHN1enkub3JnCnxodHRwOi8vcGljdHVyZXMucGxheWJv +eS5jb20KfHxwbGF5Ym95LmNvbQoucGxheWJveXBsdXMuY29tCnx8cGxheWJveXBs +dXMuY29tCnx8cGxheWVyLmZtCi5wbGF5bm8xLmNvbQp8fHBsYXlubzEuY29tCnx8 +cGxheXBjZXNvci5jb20KcGxheXMuY29tLnR3Cnx8cGxleHZwbi5wcm8KfHxtLnBs +aXhpLmNvbQpwbG0ub3JnLmhrCnBsdW5kZXIuY29tCi5wbHVyay5jb20KfHxwbHVy +ay5jb20KLnBsdXMyOC5jb20KLnBsdXNiYi5jb20KLnBtYXRlaHVudGVyLmNvbQp8 +fHBtYXRlaHVudGVyLmNvbQoucG1hdGVzLmNvbQp8fHBvMmIuY29tCnBvYmllcmFt +eS50b3AKIS0tfHxwb2Nvby5vcmcKfHxwb2RiZWFuLmNvbQp8fHBvZGljdGlvbmFy +eS5jb20KLnBva2Vyc3RhcnMuY29tCnx8cG9rZXJzdGFycy5jb20KLnBva2Vyc3Rh +cnMubmV0CnpoLnBva2Vyc3RyYXRlZ3kuY29tCnBvbGl0aWNhbGNoaW5hLm9yZwpw +b2xpdGljYWxjb25zdWx0YXRpb24ub3JnCi5wb2xpdGlzY2FsZXMubmV0Cnx8cG9s +b25pZXguY29tCi5wb2x5bWVyaGsuY29tCnxodHRwOi8vcG9seW1lcmhrLmNvbQou +cG9wby50dwohLS18fHBvcHVsYXJwYWdlcy5uZXQKfHxwb3B2b3RlLmhrCnx8cG9w +eGkuY2xpY2sKLnBvcHlhcmQuY29tCnx8cG9weWFyZC5vcmcKLnBvcm4uY29tCi5w +b3JuMi5jb20KLnBvcm41LmNvbQoucG9ybmJhc2Uub3JnCi5wb3JuZXJicm9zLmNv +bQp8fHBvcm5oZC5jb20KLnBvcm5ob3N0LmNvbQoucG9ybmh1Yi5jb20KfHxwb3Ju +aHViLmNvbQoucG9ybmh1YmRldXRzY2gubmV0CnxodHRwOi8vcG9ybmh1YmRldXRz +Y2gubmV0Cnx8cG9ybm1tLm5ldAoucG9ybm94by5jb20KLnBvcm5yYXBpZHNoYXJl +LmNvbQp8fHBvcm5yYXBpZHNoYXJlLmNvbQoucG9ybnNoYXJpbmcuY29tCnxodHRw +Oi8vcG9ybnNoYXJpbmcuY29tCi5wb3Juc29ja2V0LmNvbQoucG9ybnN0YXJjbHVi +LmNvbQp8fHBvcm5zdGFyY2x1Yi5jb20KLnBvcm50dWJlLmNvbQoucG9ybnR1YmVu +ZXdzLmNvbQoucG9ybnR2YmxvZy5jb20KfHxwb3JudHZibG9nLmNvbQoucG9ybnZp +c2l0LmNvbQoucG9ydGFibGV2cG4ubmwKfHxwb3Nrb3RhbmV3cy5jb20KLnBvc3Qw +MS5jb20KLnBvc3Q3Ni5jb20KfHxwb3N0NzYuY29tCi5wb3N0ODUyLmNvbQp8fHBv +c3Q4NTIuY29tCnBvc3RhZHVsdC5jb20KLnBvc3RpbWcub3JnCnx8cG90dnBuLmNv +bQp8fHBvd2VyY3guY29tCi5wb3dlcnBob3RvLm9yZwp8fHd3dy5wb3dlcnBvaW50 +bmluamEuY29tCnx8cHJlc2lkZW50bGVlLnR3Cnx8Y2RuLnByaW50ZnJpZW5kbHku +Y29tCi5wcml0dW5sLmNvbQpwcm92cG5hY2NvdW50cy5jb20KfHxwcm92cG5hY2Nv +dW50cy5jb20KLnByb3hmcmVlLmNvbQp8fHByb3hmcmVlLmNvbQpwcm94eWFub25p +bW8uZXMKLnByb3h5bmV0d29yay5vcmcudWsKfHxwcm94eW5ldHdvcmsub3JnLnVr +Cnx8cHRzLm9yZy50dwoucHR0dmFuLm9yZwpwdWJ1LmNvbS50dwpwdWZmaW5icm93 +c2VyLmNvbQpwdXJlaW5zaWdodC5vcmcKLnB1c2hjaGluYXdhbGwuY29tCi5wdXR0 +eS5vcmcKfHxwdXR0eS5vcmcKCiEtLS0tLS0tLS0tLS0tUG9zdGVyb3VzLS0tLS0K +fHxjYWxlYmVsc3Rvbi5jb20KfHxibG9nLmZpenppay5jb20KfHxuZi5pZC5hdQp8 +fHNvZ3JhZHkubWUKfHx2YXRuLm9yZwp8fHZlbnR1cmVzd2VsbC5jb20KfHx3aGVy +ZWlzd2VybmVyLmNvbQoKLnBvd2VyLmNvbQp8fHBvd2VyLmNvbQpwb3dlcmFwcGxl +LmNvbQp8fHBvd2VyYXBwbGUuY29tCnx8YWJjLnBwLnJ1CmhlaXgucHAucnUKfHxw +cmF5Zm9yY2hpbmEubmV0Cnx8cHJlbWVmb3J3aW5kb3dzNy5jb20KfHxwcmVzZW50 +YXRpb256ZW4uY29tCnx8cHJlc3RpZ2UtYXYuY29tCnByaXNvbmVyLXN0YXRlLXNl +Y3JldC1qb3VybmFsLXByZW1pZXIKLnByaXNvbmVyYWxlcnQuY29tCnx8cHJpdHVu +bC5jb20KfHxwcml2YWN5Ym94LmRlCi5wcml2YXRlLmNvbS9ob21lCnx8cHJpdmF0 +ZWludGVybmV0YWNjZXNzLmNvbQpwcml2YXRlcGFzdGUuY29tCnx8cHJpdmF0ZXBh +c3RlLmNvbQpwcml2YXRldHVubmVsLmNvbQp8fHByaXZhdGV0dW5uZWwuY29tCnx8 +cHJpdmF0ZXZwbi5jb20KfHxwcml2b3h5Lm9yZwp8fHByb2NvcHl0aXBzLmNvbQp8 +fHByb2plY3Qtc3luZGljYXRlLm9yZwp8fHByb3Rvbi5tZQpwcm92aWRlb2NvYWxp +dGlvbi5jb20KfHxwcm9zaWJlbi5kZQpwcm94aWZpZXIuY29tCmFwaS5wcm94bGV0 +LmNvbQp8fHByb3hvbWl0cm9uLmluZm8KLnByb3hwbi5jb20KfHxwcm94cG4uY29t +Ci5wcm94eWxpc3Qub3JnLnVrCnx8cHJveHlsaXN0Lm9yZy51awoucHJveHlweS5u +ZXQKfHxwcm94eXB5Lm5ldApwcm94eXJvYWQuY29tCi5wcm94eXR1bm5lbC5uZXQK +IS0tNDAzIG1heWJlCnx8cHJveWVjdG9jbHViZXMuY29tCnByb3p6Lm5ldApwc2Js +b2cubmFtZQp8fHBzYmxvZy5uYW1lCnx8cHNodnBuLmNvbQp8fHBzaXBob24uY2EK +LnBzaXBob24zLmNvbQp8fHBzaXBob24zLmNvbQoucHNpcGhvbnRvZGF5LmNvbQp8 +fHB0LmltCi5wdHQuY2MKfHxwdHQuY2MKfHxwdHRnYW1lLmNvbQoucHVmZnN0b3Jl +LmNvbQoucHV1a28uY29tCnx8cHVsbGZvbGlvLmNvbQoucHVueXUuY29tL3B1bnkK +fHxwdXJlY29uY2VwdHMubmV0Cnx8cHVyZWluc2lnaHQub3JnCnx8cHVyZXBkZi5j +b20KfHxwdXJldnBuLmNvbQoucHVycGxlbG90dXMub3JnCi5wdXJzdWVzdGFyLmNv +bQp8fHB1cnN1ZXN0YXIuY29tCnx8bml0dGVyLnB1c3N0aGVjYXQub3JnCi5wdXNz +eXNwYWNlLmNvbQoucHV0aWhvbWUub3JnCi5wdXRsb2NrZXIuY29tL2ZpbGUKcHdu +ZWQuY29tCnx8cHhpbWcubmV0CnB5dGhvbi5jb20KLnB5dGhvbi5jb20udHcKfGh0 +dHA6Ly9weXRob24uY29tLnR3CnB5dGhvbmhhY2tlcnMuY29tL3AKc3MucHl0aG9u +aWMubGlmZS8KCiEtLS0tLS0tLS0tLS0tLS0tLS0tLVFRLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLQoucWFub3RlLmNvbQp8fHFhbm90ZS5jb20KLnFnaXJsLmNvbS50 +dwp8fHFpYW5iYWkudHcKfHxxaWFuZGFvLnRvZGF5Cnx8cWlhbmd3YWlrYW4uY29t +Ci5xaS1nb25nLm1lCnx8cWktZ29uZy5tZQohLS0jOTIxCnx8cWlhbmd5b3Uub3Jn +Ci5xaWRpYW4uY2EKLnFpZW5rdWVuLm9yZwp8fHFpZW5rdWVuLm9yZwp8fHFpd2Vu +Lmx1CnFpeGlhbmdsdS5jbgpiYnMucW16ZGQuY29tCi5xa3NoYXJlLmNvbQpxb29z +LmNvbQp8fHFvb3MuY29tCmJsb2cucW9vemEuaGsvZGFmZW5ncWl4aQp8fGVma3Nv +ZnQuY29tCnx8cXN0YXR1cy5jb20KfHxxdHdlZXRlci5jb20KfHxxdHJhYy5ldQou +cXVhbm5lbmdzaGVuLm9yZwp8aHR0cDovL3F1YW5uZW5nc2hlbi5vcmcKcXVhbnR1 +bWJvb3Rlci5uZXQKfHxxdWl0Y2NwLm5ldAoucXVpdGNjcC5uZXQKfHxxdWl0Y2Nw +Lm9yZwoucXVpdGNjcC5vcmcKLnF1b3JhLmNvbS9DaGluYXMtRnV0dXJlCi5xdXJh +bi5jb20KfGh0dHA6Ly9xdXJhbi5jb20KLnF1cmFuZXhwbG9yZXIuY29tCnF1c2k4 +Lm5ldAoucXZvZHp5Lm9yZwpuZW1lc2lzMi5xeC5uZXQvcGFnZXMvTXlFblR1bm5l +bApxeGJicy5vcmcKCiEtLS0tLS0tLS0tLS0tLS0tLS0tLVJSLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLQp8fHIwLnJ1Ci5yYS5nZwp8aHR0cDovL3JhLmdnLwoucmFk +aWNhbHBhcnR5Lm9yZwp8fHJhZWwub3JnCnJhZGljYWxwYXJ0eS5vcmcKfHxyYWRp +by5nYXJkZW4KcmFkaW9hdXN0cmFsaWEubmV0LmF1Ci5yYWRpb2hpbGlnaHQubmV0 +Cnx8cmFkaW9oaWxpZ2h0Lm5ldAp8fHJhZGlvbGluZS5jbwpvcG1sLnJhZGlvdGlt +ZS5jb20KfHxyYWRpb3ZhdGljYW5hLm9yZwp8fHJhZGlvdm5jci5jb20KfHxyYWdn +ZWRiYW5uZXIuY29tCnx8cmFpZGNhbGwuY29tLnR3Ci5yYWlkdGFsay5jb20udHcK +LnJhaW5ib3dwbGFuLm9yZy9iYnMKfGh0dHBzOi8vcmFpbmRyb3AuaW8vCi5yYWl6 +b2ppLm9yLmpwCnxodHRwOi8vcmFpem9qaS5vci5qcApyYW5nd2FuZy5iaXoKcmFu +Z3plbi5jb20KcmFuZ3plbi5uZXQKcmFuZ3plbi5vcmcKfGh0dHA6Ly9ibG9nLnJh +bnhpYW5nLmNvbS8KcmFueXVuZmVpLmNvbQp8fHJhbnl1bmZlaS5jb20KLnJhcGJ1 +bGwubmV0CnxodHRwOi8vcmFwaWRnYXRvci5uZXQvCnx8cmFwaWRtb3ZpZXouY29t +CnJhcGlkdnBuLmNvbQp8fHJhcGlkdnBuLmNvbQp8fHJhcmJncHJ4Lm9yZwoucmFy +ZW1vdmllLmNjCnxodHRwOi8vcmFyZW1vdmllLmNjCi5yYXJlbW92aWUubmV0Cnxo +dHRwOi8vcmFyZW1vdmllLm5ldAp8fHJhdGlvbmFsd2lraS5vcmcKfHxyYXdnaXQu +Y29tCnx8cmF3Z2l0aHViLmNvbQohLS0ucmF5Zm1lLmNvbS9iYnMKfHxyYXp5Ym9h +cmQuY29tCnJjaW5ldC5jYQoucmVhZDEwMC5jb20KLnJlYWRpbmd0aW1lcy5jb20u +dHcKfHxyZWFkaW5ndGltZXMuY29tLnR3Cnx8cmVhZG1vby5jb20KLnJlYWR5ZG93 +bi5jb20KfGh0dHA6Ly9yZWFkeWRvd24uY29tCi5yZWFsY291cmFnZS5vcmcKLnJl +YWxpdHlraW5ncy5jb20KfHxyZWFsaXR5a2luZ3MuY29tCi5yZWFscmFwdGFsay5j +b20KLnJlYWxzZXhwYXNzLmNvbQp8fHJlYXNvbi5jb20KLnJlY29yZGhpc3Rvcnku +b3JnCi5yZWNvdmVyeS5vcmcudHcKfGh0dHA6Ly9vbmxpbmUucmVjb3Zlcnl2ZXJz +aW9uLm9yZwp8fHJlY292ZXJ5dmVyc2lvbi5jb20udHcKfHxyZWQtbGFuZy5vcmcK +cmVkYmFsbG9vbnNvbGlkYXJpdHkub3JnCnx8cmVkYnViYmxlLmNvbQoucmVkY2hp +bmFjbi5uZXQKfGh0dHA6Ly9yZWRjaGluYWNuLm5ldApyZWRjaGluYWNuLm9yZwpy +ZWR0dWJlLmNvbQpyZWZlcmVyLnVzCnx8cmVmZXJlci51cwp8fHJlZmxlY3RpdmVj +b2RlLmNvbQpyZWxheGJicy5jb20KLnJlbGF5LmNvbS50dwoucmVsZWFzZWludGVy +bmF0aW9uYWwub3JnCnx8cmVsaWdpb25uZXdzLmNvbQpyZWxpZ2lvdXN0b2xlcmFu +Y2Uub3JnCnJlbm1pbmJhby5jb20KfHxyZW5taW5iYW8uY29tCi5yZW55dXJlbnF1 +YW4ub3JnCnx8cmVueXVyZW5xdWFuLm9yZwp8aHR0cDovL2NlcnRpZmljYXRlLnJl +dm9jYXRpb25jaGVjay5jb20Kc3ViYWNtZS5yZXJvdXRlZC5vcmcKfHxyZXNpbGlv +LmNvbQoucmV1dGVycy5jb20KfHxyZXV0ZXJzLmNvbQp8fHJldXRlcnNtZWRpYS5u +ZXQKLnJldmxlZnQuY29tCnx8cmVzaXN0Y2hpbmEub3JnCnJldHdlZXRpc3QuY29t +Cnx8cmV0d2VldHJhbmsuY29tCiEtLWNvbm5lY3RlZGNoaW5hLnJldXRlcnMuY29t +CiEtLXxodHRwOi8vd3d3LnJldXRlcnMuY29tL25ld3MvdmlkZW8KcmV2dmVyLmNv +bQoucmZhLm9yZwp8fHJmYS5vcmcKLnJmYWNoaW5hLmNvbQoucmZhbW9iaWxlLm9y +ZwpyZmF3ZWIub3JnCnx8cmZlcmwub3JnCi5yZmkuZnIKfHxyZmkuZnIKfHxyZmku +bXkKIS0tLnJoY2xvdWQuY29tCiEtLUVkZ2VjYXN0CnxodHRwOi8vdmRzLnJpZ2h0 +c3Rlci5jb20vCi5yaWdwYS5vcmcKLnJpbGV5Z3VpZGUuY29tCnx8cmlrdS5tZQou +cml0b3VraS5qcAp8fHJpdHRlci52Zwoucmx3bHcuY29tCnx8cmx3bHcuY29tCnx8 +cm1ibC53cwoucm1qZHcuY29tCi5ybWpkdzEzMi5pbmZvCi5yb2Fkc2hvdy5oawou +cm9ib2ZvcmV4LmNvbQp8fHJvYnVzdG5lc3Npc2tleS5jb20KIS0tfHxyb2MtdGFp +d2FuLm9yZwp8fHJvY2tldC1pbmMubmV0CnxodHRwOi8vd3d3Mi5yb2NrZXRiYnMu +Y29tLzExL2Jicy5jZ2k/aWQ9NW11cwp8aHR0cDovL3d3dzIucm9ja2V0YmJzLmNv +bS8xMS9iYnMuY2dpP2lkPWZyZWVtZ2wKIS0tfHxyb2NtcC5vcmcKfHxyb2pvLmNv +bQp8fHJvbmpvbmVzd3JpdGVyLmNvbQp8fHJvbGZvdW5kYXRpb24ub3JnCnx8cm9s +aWEubmV0Cnx8cm9sc29jaWV0eS5vcmcKLnJvb2RvLmNvbQoucm9zZWNoaW5hLm5l +dAoucm90dGVuLmNvbQoucnNmLm9yZwp8fHJzZi5vcmcKLnJzZi1jaGluZXNlLm9y +Zwp8fHJzZi1jaGluZXNlLm9yZwoucnNnYW1lbi5vcmcKfHxyc3NodWIuYXBwCnx8 +cGhvc3BoYXRpb24xMy5yc3NpbmcuY29tCi5yc3NtZW1lLmNvbQp8fHJzc21lbWUu +Y29tCnx8cnRhbGFiZWwub3JnCi5ydGhrLmhrCnx8cnRoay5oawoucnRoay5vcmcu +aGsKfHxydGhrLm9yZy5oawoucnRpLm9yZy50dwp8fHJ0aS5vcmcudHcKfHxydGku +dHcKLnJ0eWNtaW5uZXNvdGEub3JnCi5ydWFueWlmZW5nLmNvbS9ibG9nKnNvbWVf +d2F5c190b19icmVha190aGVfZ3JlYXRfZmlyZXdhbGwKcnVrb3Iub3JnCnx8cnVs +ZTM0Lnh4eAp8fHJ1bWJsZS5jb20KLnJ1bmJ0eC5jb20KLnJ1c2hiZWUuY29tCnx8 +cnVzdnBuLmNvbQoucnV0ZW4uY29tLnR3Cnx8cnV0ZW4uY29tLnR3Cnx8cnV0cmFj +a2VyLm5ldApydXR1YmUucnUKLnJ1eWlzZWVrLmNvbQoucnhoai5uZXQKfGh0dHA6 +Ly9yeGhqLm5ldAoKIS0tLS0tLS0tLS0tLS0tLS0tLS0tU1MtLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tCi5zMXMxczEuY29tCnx8cy1jdXRlLmNvbQoucy1kcmFnb24u +b3JnCnx8czFoZW5nLmNvbQp8aHR0cDovL3d3dy5zNG1pbmlhcmNoaXZlLmNvbQp8 +fHM4Zm9ydW0uY29tCmNkbjEubHAuc2Fib29tLmNvbQp8fHNhY2tzLmNvbQpzYWNv +bS5oawp8fHNhY29tLmhrCnx8c2FkcGFuZGEudXMKfHxzYWZlY2hhdC5jb20KfHxz +YWZlZ3VhcmRkZWZlbmRlcnMuY29tCi5zYWZlcnZwbi5jb20KfHxzYWZlcnZwbi5j +b20KLnNhaW50eWN1bHR1cmUuY29tCnxodHRwOi8vc2FpbnR5Y3VsdHVyZS5jb20K +LnNhaXEubWUKfHxzYWlxLm1lCnx8c2FrdXJhbGl2ZS5jb20KLnNha3lhLm9yZwou +c2FsdmF0aW9uLm9yZy5oawp8fHNhbHZhdGlvbi5vcmcuaGsKLnNhbWFpci5ydS9w +cm94eS90eXBlLTAxCi5zYW1iaG90YS5vcmcKLmNuLnNhbmRzY290YWljZW50cmFs +LmNvbQp8aHR0cDovL2NuLnNhbmRzY290YWljZW50cmFsLmNvbQp8fHNhbmtlaS5j +b20KLnNhbm1pbi5jb20udHcKc2FwaWthY2h1Lm5ldApzYXZlbWVkaWEuY29tCnx8 +c2F2ZXRoZXNvdW5kcy5pbmZvCi5zYXZldGliZXQuZGUKfHxzYXZldGliZXQuZGUK +c2F2ZXRpYmV0LmZyCnNhdmV0aWJldC5ubAouc2F2ZXRpYmV0Lm9yZwp8fHNhdmV0 +aWJldC5vcmcKc2F2ZXRpYmV0LnJ1Ci5zYXZldGliZXRzdG9yZS5vcmcKfHxzYXZl +dGliZXRzdG9yZS5vcmcKfHxzYXZldWlnaHVyLm9yZwpzYXZldmlkLmNvbQp8fHNh +eTIuaW5mbwouc2JtZS5tZQp8aHR0cDovL3NibWUubWUKLnNicy5jb20uYXUveW91 +cmxhbmd1YWdlCi5zY2FzaW5vLmNvbQp8aHR0cDovL3d3dy5zY2llbmNlbWFnLm9y +Zy9jb250ZW50LzM0NC82MTg3Lzk1Mwouc2NpZW5jZW5ldHMuY29tCi5zY21wLmNv +bQp8fHNjbXAuY29tCi5zY21wY2hpbmVzZS5jb20KfHxzY3JhbWJsZS5pbwouc2Ny +aWJkLmNvbQp8fHNjcmliZC5jb20KfHxzY3JpcHRzcG90LmNvbQp8fHNlYXJjaC5j +b20KLnNlYXJjaHRydXRoLmNvbQp8fHNlYXJ4Lm1lCnx8c2VhdHRsZWZkYy5jb20K +LnNlY3JldGNoaW5hLmNvbQp8fHNlY3JldGNoaW5hLmNvbQp8fHNlY3JldGdhcmRl +bi5ubwouc2VjcmV0c2xpbmUuYml6Cnx8c2VjcmV0c2xpbmUuYml6Cnx8c2VjdXJl +c2VydmVyY2RuLm5ldAp8fHNlY3VyZXR1bm5lbC5jb20Kc2VjdXJpdHlpbmFib3gu +b3JnCnxodHRwczovL3NlY3VyaXR5aW5hYm94Lm9yZwouc2VjdXJpdHlraXNzLmNv +bQp8fHNlY3VyaXR5a2lzcy5jb20KfHxzZWVkNC5tZQpuZXdzLnNlZWh1YS5jb20K +c2Vlc21pYy5jb20KfHxzZWV2cG4uY29tCnx8c2Vlem9uZS5uZXQKc2VqaWUuY29t +Ci5zZW5kc3BhY2UuY29tCnx8c2Vuc29ydG93ZXIuY29tCnxodHRwOi8vdHdlZXRz +LnNlcmFwaC5tZS8Kc2VzYXdlLm5ldAp8fHNlc2F3ZS5uZXQKLnNlc2F3ZS5vcmcK +fHxzZXRod2tsZWluLm5ldAouc2V0bi5jb20KLnNldHR2LmNvbS50dwpmb3J1bS5z +ZXR0eS5jb20udHcKLnNldmVubG9hZC5jb20KfHxzZXZlbmxvYWQuY29tCi5zZXgu +Y29tCnx8c2V4LmNvbQouc2V4LTExLmNvbQp8fHNleDMuY29tCnx8c2V4OC5jYwou +c2V4YW5kc3VibWlzc2lvbi5jb20KLnNleGJvdC5jb20KLnNleGh1LmNvbQouc2V4 +aHVhbmcuY29tCnNleGluc2V4Lm5ldAp8fHNleGluc2V4Lm5ldAouc2V4dHZ4LmNv +bQoKIS0tSVAgb2YgU2V4SW5TZXgKNjcuMjIwLjkxLjE1CjY3LjIyMC45MS4xOAo2 +Ny4yMjAuOTEuMjMKCnxodHRwOi8vKi5zZi5uZXQKLnNmaWxleWR5LmNvbQp8fHNm +c2hpYmFvLmNvbQouc2Z0aW5kaWEub3JnCi5zZnR1ay5vcmcKfHxzZnR1ay5vcmcK +fHxzaGFkZXlvdXZwbi5jb20Kc2hhZG93Lm1hCi5zaGFkb3dza3kueHl6Ci5zaGFk +b3dzb2Nrcy5hc2lhCnx8d3d3LnNoYWRvd3NvY2tzLmNvbQouc2hhZG93c29ja3Mu +Y29tCnx8c2hhZG93c29ja3MuY29tLmhrCi5zaGFkb3dzb2Nrcy5vcmcKfHxzaGFk +b3dzb2Nrcy5vcmcKfHxzaGFkb3dzb2Nrcy1yLmNvbQp8aHR0cDovL2NuLnNoYWZh +cW5hLmNvbQp8fHNoYWhpdC5iaXoKLnNoYW1iYWxhcG9zdC5jb20KLnNoYW1iaGFs +YXN1bi5jb20KLnNoYW5nZmFuZy5vcmcKfHxzaGFuZ2Zhbmcub3JnCnNoYXBlc2Vy +dmljZXMuY29tCi5zaGFyZWJlZS5jb20KfHxzaGFyZWNvb2wub3JnCiEtLXx8c2hh +cmtkb2xwaGluLmNvbQpzaGFycGRhaWx5LmNvbS5oawp8fHNoYXJwZGFpbHkuY29t +LmhrCi5zaGFycGRhaWx5LmhrCi5zaGFycGRhaWx5LnR3Ci5zaGF0LXRpYmV0LmNv +bQpzaGVpa3llcm1hbWkuY29tCi5zaGVsbGZpcmUuZGUKfHxzaGVsbGZpcmUuZGUK +LnNoZW5zaG91Lm9yZwpzaGVueXVuLmNvbQpzaGVueXVucGVyZm9ybWluZ2FydHMu +b3JnCnx8c2hlbnl1bnBlcmZvcm1pbmdhcnRzLm9yZwp8fHNoZW55dW5zaG9wLmNv +bQpzaGVuemhvdWZpbG0uY29tCnx8c2hlbnpob3VmaWxtLmNvbQp8fHNoZW56aG91 +emhlbmdkYW8ub3JnCnx8c2hlcmFiZ3lhbHRzZW4uY29tCi5zaGlhdHYubmV0Ci5z +aGljaGVuZy5vcmcKc2hpbnljaGFuLmNvbQpzaGlwY2Ftb3VmbGFnZS5jb20KLnNo +aXJleWlzaHVuamlhbi5jb20KLnNoaXRhb3R2Lm9yZwp8fHNoaXhpYW8ub3JnCnx8 +c2hpemhhby5vcmcKc2hpemhhby5vcmcKc2hrc3ByLm1vYmkvZGFicgp8fHNob2Rh +bmhxLmNvbQp8fHNob29zaHRpbWUuY29tCi5zaG9wMjAwMC5jb20udHcKfHxzaG9w +ZWUudHcKLnNob3BwaW5nLmNvbQouc2hvd2hhb3R1LmNvbQouc2hvd3RpbWUuanAK +fHxzaG93d2UudHcKLnNodXR0ZXJzdG9jay5jb20KfHxzaHV0dGVyc3RvY2suY29t +CmNoLnNodm9vbmcuY29tCi5zaHdjaHVyY2gub3JnCnx8c2h3Y2h1cmNoLm9yZwou +c2h3Y2h1cmNoMy5jb20KfGh0dHA6Ly9zaHdjaHVyY2gzLmNvbQouc2lkZGhhcnRo +YXNpbnRlbnQub3JnCnx8c2lkZWxpbmVzbmV3cy5jb20KLnNpZGVsaW5lc3Nwb3J0 +c2VhdGVyeS5jb20KfHxzaWduYWwub3JnCi5zaWppaHVpc3VvLmNsdWIKLnNpamlo +dWlzdW8uY29tCi5zaWxrYm9vay5jb20KfHxzaW1ib2xvc3R3aXR0ZXIuY29tCnNp +bXBsZWNkLm9yZwp8fHNpbXBsZWNkLm9yZwpAQHx8c2ltcGxlY2QubWUKc2ltcGxl +cHJvZHVjdGl2aXR5YmxvZy5jb20KYmJzLnNpbmEuY29tLwpiYnMuc2luYS5jb20l +MkYKYmxvZy5zaW5hLmNvbS50dwpkYWlseW5ld3Muc2luYS5jb20vCmRhaWx5bmV3 +cy5zaW5hLmNvbSUyRgpmb3J1bS5zaW5hLmNvbS5oawpob21lLnNpbmEuY29tCnx8 +bWFnYXppbmVzLnNpbmEuY29tLnR3Cm5ld3Muc2luYS5jb20uaGsKbmV3cy5zaW5h +LmNvbS50dwpuZXdzLnNpbmNoZXcuY29tLm15Ci5zaW5jaGV3LmNvbS5teS9ub2Rl +Lwouc2luY2hldy5jb20ubXkvdGF4b25vbXkvdGVybQouc2luZ2Fwb3JlcG9vbHMu +Y29tLnNnCnx8c2luZ2Fwb3JlcG9vbHMuY29tLnNnCi5zaW5nZm9ydGliZXQuY29t +Ci5zaW5ncGFvLmNvbS5oawpzaW5ndGFvLmNvbQp8fHNpbmd0YW8uY29tCm5ld3Mu +c2luZ3Rhby5jYQouc2luZ3Rhb3VzYS5jb20KfHxzaW5ndGFvdXNhLmNvbQohLS18 +fGNkcC5zaW5pY2EuZWR1LnR3CnNpbm8tbW9udGhseS5jb20KfHxzaW5vY2EuY29t +Cnx8c2lub2Nhc3QuY29tCnNpbm9jaXNtLmNvbQpzaW5vbW9udHJlYWwuY2EKLnNp +bm9uZXQuY2EKLnNpbm9waXR0LmluZm8KLnNpbm9hbnRzLmNvbQp8fHNpbm9hbnRz +LmNvbQp8fHNpbm9pbnNpZGVyLmNvbQouc2lub3F1ZWJlYy5jb20KLnNpZXJyYWZy +aWVuZHNvZnRpYmV0Lm9yZwpzaXMueHh4Cnx8c2lzMDAxLmNvbQpzaXMwMDEudXMK +LnNpdGUydW5ibG9jay5jb20KfHxzaXRlOTAubmV0Ci5zaXRlYnJvLnR3Cnx8c2l0 +ZWtyZWF0b3IuY29tCnx8c2l0ZWtzLnVrLnRvCnx8c2l0ZW1hcHMub3JnCi5zanJ0 +Lm9yZwp8aHR0cDovL3NqcnQub3JnCnx8c2p1bS5jbgp8fHNrZXRjaGFwcHNvdXJj +ZXMuY29tCnx8c2tpbXR1YmUuY29tCnx8bGFiLnNray5tb2UKfHxza3liZXQuY29t +CnxodHRwOi8vdXNlcnMuc2t5bmV0LmJlL3JldmVzL3RpYmV0aG9tZS5odG1sCi5z +a3lraW5nLmNvbS50dwpiYnMuc2t5a2l3aS5jb20KfGh0dHA6Ly93d3cuc2t5cGUu +Y29tL2ludGwvCnxodHRwOi8vd3d3LnNreXBlLmNvbS96aC1IYW50Cnx8c2t5dmVn +YXMuY29tCi54c2t5d2Fsa2VyLmNvbQp8fHhza3l3YWxrZXIuY29tCnx8c2t5eHZw +bi5jb20KbS5zbGFuZHIubmV0Ci5zbGF5dGl6bGUuY29tCi5zbGVhenlkcmVhbS5j +b20KfHxzbGhlbmcuY29tCnx8c2xpZGVzaGFyZS5uZXQKZm9ydW0uc2xpbWUuY29t +LnR3Ci5zbGlua3NldC5jb20KfHxzbGlja3Zwbi5jb20KLnNsdXRsb2FkLmNvbQp8 +fHNtYXJ0ZG5zcHJveHkuY29tCi5zbWFydGhpZGUuY29tCnx8YXBwLnNtYXJ0bWFp +bGNsb3VkLmNvbQpzbWNoYm9va3MuY29tCi5zbWguY29tLmF1L3dvcmxkL2RlYXRo +LW9mLWNoaW5lc2UtcGxheWJveS1sZWF2ZXMtZnJlc2gtc2NyYXRjaGVzLWluLXBh +cnR5LXBhaW50d29yay0yMDEyMDkwMy0yNWE4dgpzbWhyaWMub3JnCi5zbWl0aC5l +ZHUvZGFsYWlsYW1hCi5zbXl4eS5vcmcKIS0tVE9ETy1uby1ob21lcGFnZQp8fHNu +YXBjaGF0LmNvbQouc25hcHR1LmNvbQp8fHNuYXB0dS5jb20KfHxzbmRjZG4uY29t +CnNuZWFrbWUubmV0CnNub3dsaW9ucHViLmNvbQpob21lLnNvLW5ldC5uZXQudHcv +eWlzYV90c2FpCnx8c29jLm1pbAp8fHNvY2lhbGJsYWRlLmNvbQouc29ja3MtcHJv +eHkubmV0Cnx8c29ja3MtcHJveHkubmV0Ci5zb2Nrc2NhcDY0LmNvbQp8fHNvY2tz +bGlzdC5uZXQKLnNvY3JlYy5vcmcKfGh0dHA6Ly9zb2NyZWMub3JnCi5zb2QuY28u +anAKLnNvZnRldGhlci5vcmcKfHxzb2Z0ZXRoZXIub3JnCi5zb2Z0ZXRoZXItZG93 +bmxvYWQuY29tCnx8c29mdGV0aGVyLWRvd25sb2FkLmNvbQp8fGNkbi5zb2Z0bGF5 +ZXIubmV0Cnx8c29nY2x1Yi5jb20Kc29oY3JhZGlvLmNvbQp8fHNvaGNyYWRpby5j +b20KLnNva21pbC5jb20KfHxzb3J0aW5nLWFsZ29yaXRobXMuY29tCi5zb3N0aWJl +dC5vcmcKLnNvdW1vLmluZm8KfHxzb3VwLmlvCkBAfHxzdGF0aWMuc291cC5pbwou +c29iZWVzLmNvbQp8fHNvYmVlcy5jb20Kc29jaWFsd2hhbGUuY29tCi5zb2Z0ZXRo +ZXIuY28uanAKfHxzb2Z0d2FyZWJ5Y2h1Y2suY29tCmJsb2cuc29nb28ub3JnCnNv +aC50dwp8fHNvaC50dwpzb2hmcmFuY2Uub3JnCnx8c29oZnJhbmNlLm9yZwpjaGlu +ZXNlLnNvaWZpbmQuY29tCnNva2Ftb25saW5lLmNvbQp8fHNvbGFuYS5jb20KLnNv +bGlkYXJpdGV0aWJldC5vcmcKLnNvbGlkZmlsZXMuY29tCnx8c29tZWUuY29tCi5z +b25namlhbmp1bi5jb20KfHxzb25namlhbmp1bi5jb20KLnNvbmljYmJzLmNjCi5z +b25pZG9kZWxhZXNwZXJhbnphLm9yZwouc29wY2FzdC5jb20KLnNvcGNhc3Qub3Jn +Cnx8bmFrZWRzZWN1cml0eS5zb3Bob3MuY29tCi5zb3Jhem9uZS5uZXQKfHxzb3Mu +b3JnCmJicy5zb3UtdG9uZy5vcmcKLnNvdWJvcnkuY29tCnxodHRwOi8vc291Ym9y +eS5jb20KLnNvdWwtcGx1cy5uZXQKLnNvdWxjYWxpYnVyaGVudGFpLm5ldAp8fHNv +dWxjYWxpYnVyaGVudGFpLm5ldAp8fHNvdW5kY2xvdWQuY29tCiEtLXxodHRwczov +L3NvdW5kY2xvdWQuY29tL3B1bmtnb2QKLnNvdW5kb2Zob3BlLmtyCnNvdW5kb2Zo +b3BlLm9yZwp8fHNvdW5kb2Zob3BlLm9yZwp8fHNvdXBvZm1lZGlhLmNvbQohLS0u +c291cmNlZm9yZ2UubmV0CiEtfGh0dHA6Ly9zb3VyY2Vmb3JnZS5uZXQKfGh0dHA6 +Ly9zb3VyY2Vmb3JnZS5uZXQvcCovc2hhZG93c29ja3NndWkvCi5zb3VyY2V3YWRp +by5jb20KfHxzb3V0aC1wbHVzLm9yZwpzb3V0aG5ld3MuY29tLnR3CnNvd2Vycy5v +cmcuaGsKfHx3bHguc293aWtpLm5ldAp8fHNwYW5rYmFuZy5jb20KLnNwYW5raW5n +dHViZS5jb20KLnNwYW5rd2lyZS5jb20KfHxzcGIuY29tCnx8c3BlYWtlcmRlY2su +Y29tCnx8c3BlZWRpZnkuY29tCnNwZW0uYXQKfHxzcGVuY2VydGlwcGluZy5jb20K +fHxzcGVuZGVlLmNvbQp8fHNwaWNldnBuLmNvbQouc3BpZGVyb2FrLmNvbQp8fHNw +aWRlcm9hay5jb20KLnNwaWtlLmNvbQouc3BvdGZsdXguY29tCnx8c3BvdGZsdXgu +Y29tCi5zcHJpbmc0dS5pbmZvCnxodHRwOi8vc3ByaW5nNHUuaW5mbwp8fHNwcm91 +dGNvcmUuY29tCnx8c3Byb3h5LmluZm8KfHxzcXVpcnJlbHZwbi5jb20KfHxzcm9j +a2V0LnVzCi5zcy1saW5rLmNvbQp8fHNzLWxpbmsuY29tCi5zc2dsb2JhbC5jby93 +cAp8aHR0cDovL3NzZ2xvYmFsLmNvCi5zc2dsb2JhbC5tZQp8fHNzaDkxLmNvbQou +c3Nwcm8ubWwKfGh0dHA6Ly9zc3Byby5tbAouc3Nyc2hhcmUuY29tCnx8c3Nyc2hh +cmUuY29tCnx8c3NzLmNhbXAKIS0tfGh0dHA6Ly9jZG4uc3N0YXRpYy5uZXQvCnx8 +c3N0bS5tb2UKfHxzc3RtbHQubW9lCnNzdG1sdC5uZXQKfHxzc3RtbHQubmV0Cnxo +dHRwOi8vc3RhY2tvdmVyZmxvdy5jb20vdXNlcnMvODk1MjQ1Ci5zdGFnZTY0Lmhr +Cnx8c3RhZ2U2NC5oawp8fHN0YW5kdXBmb3J0aWJldC5vcmcKfHxzdGFuZHdpdGho +ay5vcmcKc3RhbmZvcmQuZWR1L2dyb3VwL2ZhbHVuCnVzaW5mby5zdGF0ZS5nb3YK +fHxzdGF0dWVvZmRlbW9jcmFjeS5vcmcKLnN0YXJmaXNoZnguY29tCi5zdGFycDJw +LmNvbQp8fHN0YXJwMnAuY29tCi5zdGFydHBhZ2UuY29tCnx8c3RhcnRwYWdlLmNv +bQouc3RhcnR1cGxpdmluZ2NoaW5hLmNvbQp8aHR0cDovL3N0YXJ0dXBsaXZpbmdj +aGluYS5jb20KfHxzdGF0aWMtZWNvbm9taXN0LmNvbQp8fHN0Ym95Lm5ldAp8fHN0 +Yy5jb20uc2EKfHxzdGVlbC1zdG9ybS5jb20KLnN0ZWdhbm9zLmNvbQp8fHN0ZWdh +bm9zLmNvbQouc3RlZ2Fub3MubmV0Ci5zdGVwY2hpbmEuY29tCiEtLXx8c3RlcG1h +bmlhLmNvbQpueS5zdGdsb2JhbGxpbmsuY29tCmhkLnN0aGVhZGxpbmUuY29tL25l +d3MvcmVhbHRpbWUKc3Rob28uY29tCnx8c3Rob28uY29tCi5zdGlja2FtLmNvbQpz +dGlja2VyYWN0aW9uLmNvbS9zZXNhd2UKLnN0aWxlcHJvamVjdC5jb20KLnN0by5j +Ywouc3RvcG9yZ2FuaGFydmVzdGluZy5vcmcKfHxzdG9yYWdlbmV3c2xldHRlci5j +b20KLnN0b3JtLm1nCnx8c3Rvcm0ubWcKLnN0b3B0aWJldGNyaXNpcy5uZXQKfHxz +dG9wdGliZXRjcmlzaXMubmV0Cnx8c3RvcmlmeS5jb20KLnN0b3JtbWVkaWFncm91 +cC5jb20KfHxzdG93ZWJveWQuY29tCnx8c3RyYWl0c3RpbWVzLmNvbQpzdHJhbmFi +Zy5jb20KfHxzdHJhcGxlc3NkaWxkby5jb20KfHxzdHJlYW1hYmxlLmNvbQp8fHN0 +cmVhbWF0ZS5jb20KfHxzdHJlYW1pbmd0aGUubmV0CnN0cmVlbWEuY29tL3R2L05U +RFRWX0NoaW5lc2UKY24uc3RyZWV0dm9pY2UuY29tL2FydGljbGUKY24uc3RyZWV0 +dm9pY2UuY29tL2RpYXJ5CmNuMi5zdHJlZXR2b2ljZS5jb20KdHcuc3RyZWV0dm9p +Y2UuY29tCi5zdHJpa2luZ2x5LmNvbQp8fHN0cm9uZ3Zwbi5jb20KLnN0cm9uZ3dp +bmRwcmVzcy5jb20KLnN0dWRlbnQudHcvZGIKfHxzdHVkZW50c2ZvcmFmcmVldGli +ZXQub3JnCnx8c3R1bWJsZXVwb24uY29tCnN0dXBpZHZpZGVvcy5jb20KfHxzdWJz +dGFjay5jb20KLnN1Y2Nlc3Nmbi5jb20KcGFuYW1hcGFwZXJzLnN1ZWRkZXV0c2No +ZS5kZQouc3VnYXJzeW5jLmNvbQp8fHN1Z2Fyc3luYy5jb20KLnN1Z29iYnMuY29t +Cnx8c3VndW1pcnUxOC5jb20KfHxzdWlzc2wuY29tCnN1bW1pZnkuY29tCi5zdW1y +YW5kby5jb20KfHxzdW1yYW5kby5jb20Kc3VuMTkxMS5jb20KfHxzdW5kYXlndWFy +ZGlhbmxpdmUuY29tCi5zdW5wb3Juby5jb20KfHxzdW5tZWRpYS5jYQp8fHN1bnBv +cm5vLmNvbQouc3Vuc2t5Zm9ydW0uY29tCi5zdW50YS5jb20udHcKLnN1bnZwbi5u +ZXQKLnN1b2x1by5vcmcKLnN1cGVyZnJlZXZwbi5jb20KLnN1cGVydnBuLm5ldAp8 +fHN1cGVydnBuLm5ldAouc3VwZXJ6b29pLmNvbQp8aHR0cDovL3N1cGVyem9vaS5j +b20KLnN1cHBpZy5uZXQKLnN1cHJlbWVtYXN0ZXJ0di5jb20KfGh0dHA6Ly9zdXBy +ZW1lbWFzdGVydHYuY29tCi5zdXJmZWFzeS5jb20KfHxzdXJmZWFzeS5jb20KLnN1 +cmZlYXN5LmNvbS5hdQp8aHR0cDovL3N1cmZlYXN5LmNvbS5hdQp8fHN1cmZzaGFy +ay5jb20KfHxzdXJyZW5kZXJhdDIwLm5ldAouc3ZzZnguY29tCi5zd2lzc2luZm8u +Y2gKfHxzd2lzc2luZm8uY2gKLnN3aXNzdnBuLm5ldAp8fHN3aXNzdnBuLm5ldApz +d2l0Y2h2cG4ubmV0Cnx8c3dpdGNodnBuLm5ldAouc3lkbmV5dG9kYXkuY29tCnx8 +c3lkbmV5dG9kYXkuY29tCi5zeWxmb3VuZGF0aW9uLm9yZwp8fHN5bGZvdW5kYXRp +b24ub3JnCnx8c3luY2JhY2suY29tCnN5c3Jlc2NjZC5vcmcKLnN5dGVzLm5ldApi +bG9nLnN5eDg2LmNvbS8yMDA5LzA5L3B1ZmYKYmxvZy5zeXg4Ni5jbi8yMDA5LzA5 +L3B1ZmYKLnN6YmJzLm5ldAouc3pldG93YWgub3JnLmhrCgohLS0tLS0tLS0tLS0t +LS0tLS0tLS1UVC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KfHx0LWcuY29tCi50 +MzUuY29tCi50NjZ5LmNvbQp8fHQ2NnkuY29tCnx8ZXNnLnQ5MXkuY29tCi50YWEt +dXNhLm9yZwp8aHR0cDovL3RhYS11c2Eub3JnCi50YWF6ZS50dwp8fHRhYXplLnR3 +CnxodHRwOi8vd3d3LnRhYmxlc2dlbmVyYXRvci5jb20vCnRhYnR0ZXIuanAKLnRh +Y2VtLm9yZwoudGFjb25ldC5jb20udHcKfHx0YWVkcC5vcmcudHcKLnRhZm0ub3Jn +Ci50YWd3YS5vcmcuYXUKdGFnd2Fsay5jb20KfHx0YWd3YWxrLmNvbQp0YWhyLm9y +Zy50dwoudGFpcGVpc29jaWV0eS5vcmcKfHx0YWlwZWlzb2NpZXR5Lm9yZwp8fHRh +aXBlaXRpbWVzLmNvbQoudGFpd2FuYmlibGUuY29tCi50YWl3YW5jb24uY29tCi50 +YWl3YW5kYWlseS5uZXQKfHx0YWl3YW5kYWlseS5uZXQKLnRhaXdhbmRjLm9yZwoh +LS18fHRhaXdhbmVtYmFzc3kub3JnCnx8dGFpd2FuaG90Lm5ldAoudGFpd2FuanVz +dGljZS5jb20KdGFpd2Fua2lzcy5jb20KdGFpd2FubmF0aW9uLmNvbQp0YWl3YW5u +YXRpb24uY29tLnR3Cnx8dGFpd2FubmNmLm9yZy50dwp8fHRhaXdhbm5ld3MuY29t +LnR3CnxodHRwOi8vd3d3LnRhaXdhbm9ubGluZS5jYy8KIS0tfHx0YWl3YW50b2Rh +eS50dwp0YWl3YW50cC5uZXQKfHx0YWl3YW50dC5vcmcudHcKdGFpd2FudXMubmV0 +CnRhaXdhbnllcy5jb20KdGFpd2FuLXNleC5jb20KLnRhbGs4NTMuY29tCi50YWxr +Ym94YXBwLmNvbQp8fHRhbGtib3hhcHAuY29tCi50YWxrY2MuY29tCnx8dGFsa2Nj +LmNvbQoudGFsa29ubHkubmV0Cnx8dGFsa29ubHkubmV0Cnx8dGFtaWFvZGUudGsK +fHx0YW5jLm9yZwp0YW5nYmVuLmNvbQoudGFuZ3Jlbi51cwoudGFvaXNtLm5ldAp8 +aHR0cDovL3Rhb2lzbS5uZXQKLnRhb2x1bi5pbmZvCnx8dGFvbHVuLmluZm8KLnRh +cGF0YWxrLmNvbQp8fHRhcGF0YWxrLmNvbQpibG9nLnRhcmFnYW5hLmNvbQoudGFz +Y24uY29tLmF1Cnx8dGF1cC5uZXQKfGh0dHA6Ly93d3cudGF1cC5vcmcudHcKLnRh +d2VldC5jb20KfHx0YXdlZXQuY29tCi50YmNvbGxlZ2Uub3JnCnx8dGJjb2xsZWdl +Lm9yZwoudGJpLm9yZy5oawoudGJpY24ub3JnCi50Ymp5dC5vcmcKfHx0YnBpYy5p +bmZvCi50YnJjLm9yZwp0YnMtcmFpbmJvdy5vcmcKLnRic2VjLm9yZwp8fHRic2Vj +Lm9yZwp0YnNra2luYWJhbHUucGFnZS50bAoudGJzbWFsYXlzaWEub3JnCi50YnNu +Lm9yZwp8fHRic24ub3JnCi50YnNzZWF0dGxlLm9yZwoudGJzc3FoLm9yZwp8aHR0 +cDovL3Ric3NxaC5vcmcKdGJzd2Qub3JnCi50YnRlbXBsZS5vcmcudWsKLnRidGhv +dXN0b24ub3JnCi50Y2N3b25saW5lLm9yZwoudGNld2Yub3JnCnRjaHJkLm9yZwp0 +Y255bmoub3JnCnx8dGNwc3BlZWQuY28KLnRjcHNwZWVkLmNvbQp8fHRjcHNwZWVk +LmNvbQoudGNzb2ZiYy5vcmcKLnRjc292aS5vcmcKLnRkbS5jb20ubW8KdGVhbWFt +ZXJpY2FueS5jb20KfHx0ZWNoc3BvdC5jb20KIS0tT1ZICnx8dGVjaHZpei5uZXQK +fHx0ZWNrLmluCi50ZWVuaWVmdWNrLm5ldAp0ZWVuc2luYXNpYS5jb20KfHx0ZWhy +YW50aW1lcy5jb20KLnRlbGVjb21zcGFjZS5jb20KfHx0ZWxlZ3JhcGguY28udWsK +fHx0ZWxlZ3JhLnBoCi50ZW5hY3kuY29tCnx8dGVuemlucGFsbW8uY29tCi50ZXcu +b3JnCnx8dGV3Lm9yZwp8fHRmaWZsdmUuY29tCi50aGFpY24uY29tCnx8dGhlYXRs +YW50aWMuY29tCnx8dGhlYXRydW0tYmVsbGkuY29tCnx8Y24udGhlYXVzdHJhbGlh +bi5jb20uYXUKdGhlYmxlbWlzaC5jb20KfHx0aGViY29tcGxleC5jb20KfHx0aGVi +bGF6ZS5jb20KLnRoZWJvYnMuY29tCnx8dGhlYm9icy5jb20KLnRoZWNoaW5hYmVh +dC5vcmcKfHx0aGVjaGluYWNvbGxlY3Rpb24ub3JnCnxodHRwOi8vd3d3LnRoZWNo +aW5hc3Rvcnkub3JnL3llYXJib29rcy95ZWFyYm9vay0yMDEyLwp8fHRoZWNvbnZl +cnNhdGlvbi5jb20KLnRoZWRhbGFpbGFtYW1vdmllLmNvbQp8aHR0cDovL3RoZWRh +bGFpbGFtYW1vdmllLmNvbQp8fHRoZWRpcGxvbWF0LmNvbQp8fHRoZWR3LnVzCnx8 +dGhlZXBvY2h0aW1lcy5jb20KIS0tfHx0aGVmcmVlbGFuZC5jbHViCnRoZWZyb250 +aWVyLmhrL3RmCnx8dGhlZ3VhcmRpYW4uY29tCnx8dGhlZ2F5LmNvbQp8aHR0cDov +L3RoZWdpb2l0aW5ob2Mudm4vCi50aGVnbHkuY29tCi50aGVob3RzLmluZm8KdGhl +aG91c2VuZXdzLmNvbQp8fHRoZWh1bi5uZXQKLnRoZWluaXRpdW0uY29tCnx8dGhl +aW5pdGl1bS5jb20KfHx0aGVtb3ZpZWRiLm9yZwoudGhlbmV3c2xlbnMuY29tCnx8 +dGhlbmV3c2xlbnMuY29tCi50aGVwaXJhdGViYXkub3JnCnx8dGhlcGlyYXRlYmF5 +Lm9yZwohLS18fHRoZXBpcmF0ZWJheS5zZQoudGhlcG9ybmR1ZGUuY29tCnx8dGhl +cG9ybmR1ZGUuY29tCnx8dGhlcG9ydGFsd2lraS5jb20KfHx0aGVwcmludC5pbgp0 +aGVyZWFsbG92ZS5rcgp0aGVyb2NrLm5ldC5uegp8fHRoZXNhdHVyZGF5cGFwZXIu +Y29tLmF1Cnx8dGhlc3RhbmRuZXdzLmNvbQp0aGV0aWJldGNlbnRlci5vcmcKdGhl +dGliZXRjb25uZWN0aW9uLm9yZwoudGhldGliZXRtdXNldW0ub3JnCi50aGV0aWJl +dHBvc3QuY29tCnx8dGhldGliZXRwb3N0LmNvbQohLS1Ub3IKfHx0aGV0aW5oYXQu +Y29tCnRoZXRyb3Rza3ltb3ZpZS5jb20KfHx0aGV0dmRiLmNvbQp0aGV2aXZla3Nw +b3QuY29tCnx8dGhld2dvLm9yZwoudGhleW5jLmNvbQp8aHR0cDovL3RoZXluYy5j +b20KLnRoaW5raW5ndGFpd2FuLmNvbQp8fHRoaW5raW5ndGFpd2FuLmNvbQoudGhp +c2F2LmNvbQp8aHR0cDovL3RoaXNhdi5jb20KLnRobGliLm9yZwp8fHRob21hc2Jl +cm5oYXJkLm9yZwoudGhvbmdkcmVhbXMuY29tCnRocmVhdGNoYW9zLmNvbQp8fHRo +cm91Z2huaWdodHNmaXJlLmNvbQoudGh1bWJ6aWxsYS5jb20KfHx0aHl3b3Jkcy5j +b20KLnRoeXdvcmRzLmNvbS50dwp0aWFuYW5tZW5tb3RoZXIub3JnCi50aWFuYW5t +ZW5kdWl6aGkuY29tCnx8dGlhbmFubWVuZHVpemhpLmNvbQp8fHRpYW5hbm1lbnVu +aXYuY29tCnx8dGlhbmFubWVudW5pdi5uZXQKfHx0aWFuZGl4aW5nLm9yZwoudGlh +bmh1YXl1YW4uY29tCi50aWFubGF3b2ZmaWNlLmNvbQp8fHRpYW50aS5pbwp0aWFu +dGlib29rcy5vcmcKfHx0aWFudGlib29rcy5vcmcKdGlhbnlhbnRvbmcub3JnLmNu +Ci50aWFuemh1Lm9yZwoudGliZXQuYXQKdGliZXQuY2EKLnRpYmV0LmNvbQp8fHRp +YmV0LmNvbQp0aWJldC5mcgoudGliZXQubmV0Cnx8dGliZXQubmV0CnRpYmV0Lm51 +Ci50aWJldC5vcmcKfHx0aWJldC5vcmcKLnRpYmV0LnNrCnRpYmV0Lm9yZy50dwou +dGliZXQudG8KLnRpYmV0LWVudm95LmV1Cnx8dGliZXQtZW52b3kuZXUKLnRpYmV0 +LWZvdW5kYXRpb24ub3JnCi50aWJldC1ob3VzZS10cnVzdC5jby51awp8fHRpYmV0 +LWluaXRpYXRpdmUuZGUKLnRpYmV0LW11bmljaC5kZQoudGliZXQzcmRwb2xlLm9y +Zwp8aHR0cDovL3RpYmV0M3JkcG9sZS5vcmcKdGliZXRhY3Rpb24ubmV0Cnx8dGli +ZXRhY3Rpb24ubmV0Ci50aWJldGFpZC5vcmcKdGliZXRhbGsuY29tCi50aWJldGFu +LmZyCnRpYmV0YW4tYWxsaWFuY2Uub3JnCi50aWJldGFuYXJ0cy5vcmcKLnRpYmV0 +YW5idWRkaGlzdGluc3RpdHV0ZS5vcmcKfHx0aWJldGFuYnVkZGhpc3RpbnN0aXR1 +dGUub3JnCnx8dGliZXRhbmNvbW11bml0eS5vcmcKfHx0aWJldGFuZW50cmVwcmVu +ZXVycy5vcmcKfHx0aWJldGFuaGVhbHRoLm9yZwoudGliZXRhbmpvdXJuYWwuY29t +Ci50aWJldGFubGFuZ3VhZ2Uub3JnCi50aWJldGFubGliZXJhdGlvbi5vcmcKfHx0 +aWJldGFubGliZXJhdGlvbi5vcmcKLnRpYmV0Y29sbGVjdGlvbi5jb20KLnRpYmV0 +YW5haWRwcm9qZWN0Lm9yZwoudGliZXRhbmNvbW11bml0eXVrLm5ldAp8aHR0cDov +L3RpYmV0YW5jb21tdW5pdHl1ay5uZXQKdGliZXRhbmN1bHR1cmUub3JnCnRpYmV0 +YW5mZW1pbmlzdGNvbGxlY3RpdmUub3JnCi50aWJldGFucGFpbnRpbmdzLmNvbQou +dGliZXRhbnBob3RvcHJvamVjdC5jb20KLnRpYmV0YW5wb2xpdGljYWxyZXZpZXcu +b3JnCi50aWJldGFucmV2aWV3Lm5ldAp8aHR0cDovL3RpYmV0YW5zcG9ydHMub3Jn +Ci50aWJldGFud29tZW4ub3JnCnxodHRwOi8vdGliZXRhbndvbWVuLm9yZwoudGli +ZXRhbnlvdXRoLm9yZwoudGliZXRhbnlvdXRoY29uZ3Jlc3Mub3JnCnx8dGliZXRh +bnlvdXRoY29uZ3Jlc3Mub3JnCi50aWJldGNoYXJpdHkuZGsKdGliZXRjaGFyaXR5 +LmluCi50aWJldGNoaWxkLm9yZwoudGliZXRjaXR5LmNvbQp8fHRpYmV0Y29ycHMu +b3JnCnx8dGliZXRleHByZXNzLm5ldAp8fHRpYmV0Zm9jdXMuY29tCnx8dGliZXRm +dW5kLm9yZwoudGliZXRnZXJtYW55LmNvbQp8fHRpYmV0Z2VybWFueS5kZQoudGli +ZXRoYXVzLmNvbQoudGliZXRoZXJpdGFnZWZ1bmQub3JnCnx8dGliZXRob3VzZS5q +cAp8fHRpYmV0aG91c2Uub3JnCnx8dGliZXRob3VzZS51cwoudGliZXRpbmZvbmV0 +Lm5ldAoudGliZXRqdXN0aWNlLm9yZwoudGliZXRrb21pdGUuZGsKfHx0aWJldG11 +c2V1bS5vcmcKfHx0aWJldG5ldHdvcmsub3JnCi50aWJldG9mZmljZS5jaAp8aHR0 +cDovL3RpYmV0b2ZmaWNlLmNoCnRpYmV0b2ZmaWNlLmV1Cnx8dGliZXRvZmZpY2Uu +b3JnCnRpYmV0b25saW5lLmNvbQp8fHRpYmV0b25saW5lLmNvbQoudGliZXRvZmZp +Y2UuY29tLmF1CnxodHRwOi8vdGliZXRvZmZpY2UuY29tLmF1Cnx8dGliZXRvbmxp +bmUudHYKLnRpYmV0b25saW5lLnR2Ci50aWJldG9yYWxoaXN0b3J5Lm9yZwp8aHR0 +cDovL3RpYmV0b3JhbGhpc3Rvcnkub3JnCi50aWJldHBvbGljeS5ldQoudGliZXRy +ZWxpZWZmdW5kLmNvLnVrCnRpYmV0c2l0ZXMuY29tCi50aWJldHNvY2lldHkuY29t +Cnx8dGliZXRzb2NpZXR5LmNvbQoudGliZXRzdW4uY29tCi50aWJldHN1cHBvcnRn +cm91cC5vcmcKfGh0dHA6Ly90aWJldHN1cHBvcnRncm91cC5vcmcKLnRpYmV0c3dp +c3MuY2gKLnRpYmV0dGVsZWdyYXBoLmNvbQp0aWJldHRpbWVzLm5ldAp8fHRpYmV0 +d3JpdGVzLm9yZwoudGlja2V0LmNvbS50dwoudGlnZXJ2cG4uY29tCnx8dGlnZXJ2 +cG4uY29tCi50aW1kaXIuY29tCnxodHRwOi8vdGltZGlyLmNvbQoudGltZS5jb20K +fGh0dHA6Ly90aW1lLmNvbQohLS0udGltZS5jb20vdGltZS90aW1lMTAwL2xlYWRl +cnMvcHJvZmlsZS9yZWJlbAohLS0udGltZS5jb20vdGltZS9zcGVjaWFscy9wYWNr +YWdlcy9hcnRpY2xlLzAsMjg4MDQKIS0tLnRpbWUuY29tL3RpbWUvbWFnYXppbmUK +fHx0aW1lc25vd25ld3MuY29tCi50aW1zYWguY29tCnx8dGltdGFsZXMuY29tCnx8 +YmxvZy50aW5leS5jb20KdGludHVjMTAxLmNvbQoudGlueS5jYwp8aHR0cDovL3Rp +bnkuY2MKdGlueWNoYXQuY29tCnx8dGlueXBhc3RlLmNvbQp8fHRpcGFzLm5ldAou +dGlzdG9yeS5jb20KfHx0a2NzLWNvbGxpbnMuY29tCi50bWFnYXppbmUuY29tCnx8 +dG1hZ2F6aW5lLmNvbQoudG1kZmlzaC5jb20KfGh0dHA6Ly90bWkubWUKLnRtcHAu +b3JnCnxodHRwOi8vdG1wcC5vcmcKLnRuYWZsaXguY29tCnx8dG5hZmxpeC5jb20K +LnRuZ3Jub3cuY29tCi50bmdybm93Lm5ldAoudG5wLm9yZwp8aHR0cDovL3RucC5v +cmcKLnRvLXBvcm5vLmNvbQp8fHRvLXBvcm5vLmNvbQp0b2dldHRlci5jb20KLnRv +a3lvLTI0Ny5jb20KLnRva3lvLWhvdC5jb20KfHx0b2t5by1wb3JuLXR1YmUuY29t +Cnx8dG9reW9jbi5jb20KdHcudG9tb25ld3MubmV0Ci50b25naWwub3Iua3IKLnRv +bm8tb2thLmpwCnRvbnl5YW4ubmV0Ci50b29kb2MuY29tCnRvb25lbC5uZXQKdG9w +ODEud3MKLnRvcG5ld3MuaW4KLnRvcHBvcm5zaXRlcy5jb20KfGh0dHA6Ly90b3Bw +b3Juc2l0ZXMuY29tCi50b3JndWFyZC5uZXQKfHx0b3JndWFyZC5uZXQKfHx0b3Au +dHYKLnRvcHNoYXJld2FyZS5jb20KLnRvcHN5LmNvbQp8fHRvcHN5LmNvbQp8fHRv +cHRpcC5jYQp0b3JhLnRvCi50b3Jjbi5jb20KfHx0b3Jsb2NrLmNvbQoudG9ycHJv +amVjdC5vcmcKfHx0b3Jwcm9qZWN0Lm9yZwp8fHRvcnJlbnRraXR0eS50dgp0b3Jy +ZW50cHJpdmFjeS5jb20KfHx0b3JyZW50cHJpdmFjeS5jb20KfGh0dHA6Ly90b3Jy +ZW50cHJvamVjdC5zZQp8fHRvcnJlbnR5Lm9yZwp8fHRvcnJlbnR6LmV1Cnx8dG9y +dnBuLmNvbQp8fHRvdGFsdnBuLmNvbQoudG91dGlhb2FiYy5jb20KdG93bmdhaW4u +Y29tCnRveXBhcmsuaW4KdG95dHJhY3RvcnNob3cuY29tCi50cGFyZW50cy5vcmcK +LnRwaS5vcmcudHcKfHx0cGkub3JnLnR3Cnx8dHJhZGluZ3ZpZXcuY29tCnx8dHJh +bnNwYXJlbmN5Lm9yZwp8fHRyZWVtYWxsLmNvbS50dwp0cmVuZHNtYXAuY29tCnx8 +dHJlbmRzbWFwLmNvbQoudHJpYWxvZmNjcC5vcmcKfHx0cmlhbG9mY2NwLm9yZwou +dHJpbW9uZGkuZGUvU0RMRQoudHJvdXcubmwKfHx0cm91dy5ubAoudHJ0Lm5ldC50 +cgp8fHRydC5uZXQudHIKdHJ0Yy5jb20udHcKLnRydWVidWRkaGEtbWQub3JnCnx8 +dHJ1ZWJ1ZGRoYS1tZC5vcmcKdHJ1bHllcmdvbm9taWMuY29tCi50cnV0aDEwMS5j +by50dgp8fHRydXRoMTAxLmNvLnR2Ci50cnV0aG9udG91ci5vcmcKfHx0cnV0aG9u +dG91ci5vcmcKfHx0cnV0aHNvY2lhbC5jb20KLnRydXZlby5jb20KLnRzY3R2Lm5l +dAoudHNlbXR1bGt1LmNvbQp0c3F1YXJlLnR2Ci50c3Uub3JnLnR3CnRzdW5hZ2Fy +dW1vbi5jb20KIS0tfGh0dHA6Ly93d3cudHN1cnUtYmlyZC5uZXQvCi50c2N0di5u +ZXQKfHx0dDEwNjkuY29tCi50dHRhbi5jb20KfHx0dHRhbi5jb20KYmIudHR2LmNv +bS50dy9iYgp0dTg5NjQuY29tCi50dWJhaG9saWMuY29tCi50dWJlLmNvbQp0dWJl +OC5jb20KfHx0dWJlOC5jb20KLnR1YmU5MTEuY29tCnx8dHViZTkxMS5jb20KLnR1 +YmVjdXAuY29tCi50dWJlZ2Fscy5jb20KLnR1YmVpc2xhbS5jb20KfGh0dHA6Ly90 +dWJlaXNsYW0uY29tCi50dWJlc3RhY2suY29tCnx8dHViZXdvbGYuY29tCi50dWli +ZWl0dS5uZXQKdHVpZGFuZy5uZXQKLnR1aWRhbmcub3JnCnx8dHVpZGFuZy5vcmcK +LnR1aWRhbmcuc2UKYmJzLnR1aXR1aS5pbmZvCi50dW11dGFuemkuY29tCnxodHRw +Oi8vdHVtdXRhbnppLmNvbQp8fHR1bXZpZXcuY29tCi50dW5laW4uY29tCnxodHRw +Oi8vdHVuZWluLmNvbQp8fHR1bm5lbGJlYXIuY29tCnx8dHVubmVsYmxpY2submV0 +Ci50dW5uZWxyLmNvbQp8fHR1bm5lbHIuY29tCnx8dHVuc2FmZS5jb20KdHVpdHdp +dC5jb20KLnR1cmFuc2FtLm9yZwoudHVyYm9iaXQubmV0Cnx8dHVyYm9iaXQubmV0 +Ci50dXJib2hpZGUuY29tCnx8dHVyYm9oaWRlLmNvbQp8fHR1cmtpc3RhbnRpbWVz +LmNvbQoudHVzaHljYXNoLmNvbQp8aHR0cDovL3R1c2h5Y2FzaC5jb20KfHxhcHAu +dHV0YW5vdGEuY29tCi50dXZwbi5jb20KfHx0dXZwbi5jb20KfGh0dHA6Ly90dXph +aWppZGkuY29tCnxodHRwOi8vKi50dXphaWppZGkuY29tCi50dzAxLm9yZwp8aHR0 +cDovL3R3MDEub3JnCgohLS0tVHVtYmxyLS0tCi50dW1ibHIuY29tCnx8dHVtYmxy +LmNvbQohLS1AQHx8YXNzZXRzLnR1bWJsci5jb20KIS0tQEB8fGRhdGEudHVtYmxy +LmNvbQohLS1AQHx8bWVkaWEudHVtYmxyLmNvbQohLS1AQHx8c3RhdGljLnR1bWJs +ci5jb20KIS0tQEB8fHd3dy50dW1ibHIuY29tCnx8bGVjbG91ZC5uZXQKfGh0dHA6 +Ly9jb3NtaWMubW9uYXIuY2gKfHxzbHV0bW9vbmJlYW0uY29tCnxodHRwOi8vYmxv +Zy5zb3lsZW50LmNvbQoKLnR2LmNvbQp8aHR0cDovL3R2LmNvbQp0dmFudHMuY29t +CmZvcnVtLnR2Yi5jb20KbmV3cy50dmIuY29tL2xpc3Qvd29ybGQKbmV3cy50dmIu +Y29tL2xvY2FsCm5ld3MudHZicy5jb20udHcKLnR2Ym94bm93LmNvbQp8aHR0cDov +L3R2Ym94bm93LmNvbS8KdHZpZGVyLmNvbQoudHZtb3N0LmNvbS5oawoudHZwbGF5 +dmlkZW9zLmNvbQp8fHR2dW5ldHdvcmtzLmNvbQoudHctYmxvZy5jb20KfGh0dHBz +Oi8vdHctYmxvZy5jb20KLnR3LW5wby5vcmcKLnR3YWl0dGVyLmNvbQp0d2FwcGVy +a2VlcGVyLmNvbQp8fHR3YXBwZXJrZWVwZXIuY29tCnx8dHdhdWQuaW8KLnR3YXVk +LmlvCi50d2F2aS5jb20KLnR3YmJzLm5ldC50dwp0d2Jicy5vcmcKdHdiYnMudHcK +fHx0d2Jsb2dnZXIuY29tCnR3ZWVwbWFnLmNvbQoudHdlZXBtbC5vcmcKfHx0d2Vl +cG1sLm9yZwoudHdlZXRiYWNrdXAuY29tCnx8dHdlZXRiYWNrdXAuY29tCnR3ZWV0 +Ym9hcmQuY29tCnx8dHdlZXRib2FyZC5jb20KLnR3ZWV0Ym9uZXIuYml6Cnx8dHdl +ZXRib25lci5iaXoKLnR3ZWV0Y3MuY29tCnxodHRwOi8vdHdlZXRjcy5jb20KfGh0 +dHA6Ly9kZWNrLmx5CiEtLSBPcGVyYXRpb24gZGlzY29udGludWVkCiEtLXx8dHdl +ZXRlLm5ldAohLS1tLnR3ZWV0ZS5uZXQKfHxtdHcudGwKfHx0d2VldGVkdGltZXMu +Y29tCiEtLSBPcGVyYXRpb24gZGlzY29udGludWVkCiEtLXR3ZWV0bWVtZS5jb20K +fHx0d2VldG15bGFzdC5mbQp0d2VldHBob3RvLmNvbQp8fHR3ZWV0cGhvdG8uY29t +Cnx8dHdlZXRyYW5zLmNvbQp0d2VldHJlZS5jb20KfHx0d2VldHJlZS5jb20KLnR3 +ZWV0dHVubmVsLmNvbQp8fHR3ZWV0dHVubmVsLmNvbQp8fHR3ZWV0d2FsbHkuY29t +CnR3ZWV0eW1haWwuY29tCnx8dHdlbHZlLnRvZGF5Ci50d2Vlei5uZXQKfGh0dHA6 +Ly90d2Vlei5uZXQKfHx0d2Z0cC5vcmcKfHx0d2dyZWF0ZGFpbHkuY29tCnR3aWJh +c2UuY29tCi50d2liYmxlLmRlCnx8dHdpYmJsZS5kZQp0d2liYm9uLmNvbQp8fHR3 +aWJzLmNvbQoudHdpY291bnRyeS5vcmcKfGh0dHA6Ly90d2ljb3VudHJ5Lm9yZwp0 +d2ljc3kuY29tCi50d2llbmRzLmNvbQp8aHR0cDovL3R3aWVuZHMuY29tCi50d2lm +YW4uY29tCnxodHRwOi8vdHdpZmFuLmNvbQp0d2lmZm8uY29tCnx8dHdpZmZvLmNv +bQoudHdpbGlnaHRzZXguY29tCnR3aWxvZy5vcmcKdHdpbWJvdy5jb20KfHx0d2lu +ZGV4eC5jb20KdHdpcHBsZS5qcAp8fHR3aXBwbGUuanAKfHx0d2lwLm1lCnR3aXNo +b3J0LmNvbQp8fHR3aXNob3J0LmNvbQp0d2lzdGFyLmNjCnx8dHdpc3Rlci5uZXQu +Y28KfHx0d2lzdGVyaW8uY29tCnR3aXN0ZXJub3cuY29tCnR3aXN0b3J5Lm5ldAp0 +d2l0YnJvd3Nlci5uZXQKfHx0d2l0Y2F1c2UuY29tCnx8dHdpdGdldGhlci5jb20K +fHx0d2lnZ2l0Lm9yZwp0d2l0Z29vLmNvbQp0d2l0aXEuY29tCnx8dHdpdGlxLmNv +bQoudHdpdGxvbmdlci5jb20KfHx0d2l0bG9uZ2VyLmNvbQp8aHR0cDovL3RsLmdk +Lwp0d2l0bWFuaWEuY29tCnR3aXRvYXN0ZXIuY29tCnx8dHdpdG9hc3Rlci5jb20K +fHx0d2l0b25tc24uY29tCiEtLVNhbWUgSVAKLnR3aXQyZC5jb20KfHx0d2l0MmQu +Y29tCi50d2l0c3RhdC5jb20KfHx0d2l0c3RhdC5jb20KfHxmaXJzdGZpdmVmb2xs +b3dlcnMuY29tCnx8cmV0d2VldGVmZmVjdC5jb20KfHx0d2VlcGxpa2UubWUKfHx0 +d2VlcGd1aWRlLmNvbQp8fHR1cmJvdHdpdHRlci5jb20KLnR3aXR2aWQuY29tCnx8 +dHdpdHZpZC5jb20KfGh0dHA6Ly90d3QudGwKdHdpdHRib3QubmV0Cnx8YWRzLXR3 +aXR0ZXIuY29tCnx8dHd0dHIuY29tCnx8dHdpdHRlcjRqLm9yZwoudHdpdHRlcmNv +dW50ZXIuY29tCnx8dHdpdHRlcmNvdW50ZXIuY29tCnR3aXR0ZXJmZWVkLmNvbQou +dHdpdHRlcmdhZGdldC5jb20KfHx0d2l0dGVyZ2FkZ2V0LmNvbQoudHdpdHRlcmty +LmNvbQp8fHR3aXR0ZXJrci5jb20KfHx0d2l0dGVybWFpbC5jb20KfHx0d2l0dGVy +cmlmaWMuY29tCnR3aXR0ZXJ0aW0uZXMKfHx0d2l0dGVydGltLmVzCnR3aXR0aGF0 +LmNvbQp8fHR3aXR0dXJrLmNvbQoudHdpdHR1cmx5LmNvbQp8fHR3aXR0dXJseS5j +b20KLnR3aXR6YXAuY29tCnR3aXlpYS5jb20KfHx0d3N0YXIubmV0Ci50d3Rrci5j +b20KfGh0dHA6Ly90d3Rrci5jb20KLnR3bm9ydGgub3JnLnR3Cnx8dHdyZXBvcnRl +ci5vcmcKdHdza3lwZS5jb20KdHd0cmxhbmQuY29tCnR3dXJsLm5sCi50d3lhYy5v +cmcKfHx0d3lhYy5vcmcKLnR4eHguY29tCi50eWNvb2wuY29tCnx8dHljb29sLmNv +bQoKIS0tdHlwZXBhZAp8fHR5cGVwYWQuY29tCkBAfHx3d3cudHlwZXBhZC5jb20K +QEB8fHN0YXRpYy50eXBlcGFkLmNvbQp8fGJsb2cuZXhwb2Z1dHVyZXMuY29tCnx8 +bGVnYWx0ZWNoLmxhdy5jb20KfHxibG9ncy50YW1wYWJheS5jb20KfHxjb250ZXN0 +cy50d2lsaW8uY29tCiEtbGF3cHJvZmVzc29ycy50eXBlcGFkLmNvbS9jaGluYV9s +YXdfcHJvZgp8fHR5cG9yYS5pbwoKIS0tLS0tLS0tLS0tLS0tLS0tLS0tVVUtLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tCi51OXVuLmNvbQp8fHU5dW4uY29tCi51YmRk +bnMub3JnCnxodHRwOi8vdWJkZG5zLm9yZwp8fHViZXJwcm94eS5uZXQKLnVjLWph +cGFuLm9yZwp8fHVjLWphcGFuLm9yZwouc3JjZi51Y2FtLm9yZy9zYWxvbi8KfGh0 +dHA6Ly9jaGluYS51Y2FuZXdzLmNvbS8KfHx1Y2RjMTk5OC5vcmcKfGh0dHA6Ly9o +dW0qLnVjaGljYWdvLmVkdS9mYWN1bHR5L3l3YW5nL2hpc3RvcnkKfHx1ZGVyem8u +aXQKLnVkbi5jb20KfHx1ZG4uY29tCnx8dWRuLmNvbS50dwp1ZG5ia2suY29tL2Ji +cwp8fHVmb3JhZGlvLmNvbS50dwp1ZnJlZXZwbi5jb20KLnVnby5jb20KIS0tZ2hz +Cnx8dWhkd2FsbHBhcGVycy5vcmcKfHx1aHJwLm9yZwoudWlnaHVyLm5sCnx8dWln +aHVyLm5sCnVpZ2h1cmJpei5uZXQKLnVsaWtlLm5ldAp1a2NkcC5jby51awp1a2xp +ZmVyYWRpby5jby51awp8fHVrbGlmZXJhZGlvLmNvLnVrCnVsdHJhdnBuLmZyCnx8 +dWx0cmF2cG4uZnIKdWx0cmF4cy5jb20KdW1pY2guZWR1L35mYWx1bgp8fHVuYmxv +Y2suY24uY29tCi51bmJsb2NrZXIueXQKdW5ibG9jay11cy5jb20KfHx1bmJsb2Nr +LXVzLmNvbQoudW5ibG9ja2RtbS5jb20KfGh0dHA6Ly91bmJsb2NrZG1tLmNvbQp8 +fHVuYmxvY2tzaXQuZXMKdW5jeWNsb21lZGlhLm9yZwoudW5jeWNsb3BlZGlhLmhr +L3dpa2kKfGh0dHA6Ly91bmN5Y2xvcGVkaWEuaGsKIS0tdW5jeWNsb3BlZGlhLmlu +Zm8KfGh0dHA6Ly91bmN5Y2xvcGVkaWEudHcKdW5kZXJ3b29kYW1tby5jb20KfHx1 +bmRlcndvb2RhbW1vLmNvbQp8fHVuaG9seWtuaWdodC5jb20KLnVuaS5jYwp8fGNs +ZHIudW5pY29kZS5vcmcKLnVuaWZpY2F0aW9uLm5ldAoudW5pZmljYXRpb24ub3Jn +LnR3Cnx8dW5pcnVsZS5jbG91ZAoudW5pdGVkc29jaWFscHJlc3MuY29tCi51bml4 +MTAwLmNvbQp8fHVua25vd25zcGFjZS5vcmcKLnVub2RlZG9zLmNvbQp1bnBvLm9y +Zwp8fHVuc3RhYmxlLmljdQoudW50cmFjZWFibGUudXMKfGh0dHA6Ly91bnRyYWNl +YWJsZS51cwp8fHVvY24ub3JnCnRvci51cGRhdGVzdGFyLmNvbQp8fHVwZ2hzYmMu +Y29tCi51cGhvbGRqdXN0aWNlLm9yZwoudXBsb2FkNHUuaW5mbwp1cGxvYWRlZC5u +ZXQvZmlsZQp8aHR0cDovL3VwbG9hZGVkLm5ldC9maWxlCnxodHRwOi8vdXBsb2Fk +ZWQudG8vZmlsZQoudXBsb2Fkc3RhdGlvbi5jb20vZmlsZQoudXBtZWRpYS5tZwp8 +fHVwbWVkaWEubWcKLnVwb3JuaWEuY29tCnxodHRwOi8vdXBvcm5pYS5jb20KfHx1 +cHJveHkub3JnCnx8dXB0b2Rvd24uY29tCi51cHdpbGwub3JnCnVyN3MuY29tCnx8 +dXJiYW5kaWN0aW9uYXJ5LmNvbQp8fHVyYmFuc3Vydml2YWwuY29tCm15c2hhcmUu +dXJsLmNvbS50dy8KfHx1cmxib3JnLmNvbQp8fHVybHBhcnNlci5jb20KdXMudG8K +fHx1c2Fjbi5jb20KLnVzYWlwLmV1Cnx8dXNhaXAuZXUKZGFsYWlsYW1hLnVzYy5l +ZHUKfHx1c2NucG0ub3JnCnx8dXNtYS5lZHUKLnVzb2NjdG4uY29tCnx8dXN0aWJl +dGNvbW1pdHRlZS5vcmcKLnVzdHJlYW0udHYKfHx1c3RyZWFtLnR2CnVzdXMuY2MK +LnV0b3BpYW5wYWwuY29tCnx8dXRvcGlhbnBhbC5jb20KLnV1LWdnLmNvbQoudXZ3 +eHl6Lnh5egp8fHV2d3h5ei54eXoKLnV3YW50cy5jb20KfHx1d2FudHMuY29tCi51 +d2FudHMubmV0CnV5Z2h1ci5jby51awp8aHR0cDovL3V5Z2h1ci1qLm9yZwp8fHV5 +Z2h1cmFhLm9yZwp8fHV5Z2h1cmFtZXJpY2FuLm9yZwp8fHV5Z2h1cmJpei5vcmcK +fHx1eWdodXJjYW5hZGlhbi5jYQp8fHV5Z2h1cmNvbmdyZXNzLm9yZwp8fHV5Z2h1 +cnBlbi5vcmcKfHx1eWdodXJwcmVzcy5jb20KfHx1eWdodXJzdHVkaWVzLm9yZwp8 +fHV5Z2h1cnRyaWJ1bmFsLmNvbQp1eWd1ci5vcmcKfGh0dHA6Ly91eW1hYXJpcC5j +b20vCgohLS0tLS0tLS0tLS0tLS0tLS0tLS1WVi0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0tLS0KfHx2MmZseS5vcmcKLnYycmF5LmNvbQp8fHYycmF5LmNvbQp8fHYycmF5 +Y24uY29tCnx8djJyYXl0ZWNoLmNvbQp8fHZhbGV1cnNhY3R1ZWxsZXMuY29tCi52 +YW4wMDEuY29tCi52YW42OTguY29tCi52YW5lbXUuY24KLnZhbmlsbGEtanAuY29t +Ci52YW5wZW9wbGUuY29tCnZhbnNreS5jb20KfHx2YXRpY2FubmV3cy52YQp8fHZj +Zi1vbmxpbmUub3JnCnx8dmNmYnVpbGRlci5vcmcKLnZlZ2FzcmVkLmNvbQoudmVs +a2FlcG9jaGEuc2sKLnZlbmJicy5jb20KLnZlbmNoaW5hLmNvbQoudmVuZXRpYW5t +YWNhby5jb20KfHx2ZW5ldGlhbm1hY2FvLmNvbQp2ZW9oLmNvbQp8fHZlcmNlbC5h +cHAKbXlzaXRlLnZlcml6b24ubmV0CnZlcm1vbnR0aWJldC5vcmcKLnZlcnNhdnBu +LmNvbQp8fHZlcnNhdnBuLmNvbQp8fHZlcnlicy5jb20KLnZmdC5jb20udHcKLnZp +YmVyLmNvbQp8fHZpYmVyLmNvbQoudmljYS5pbmZvCi52aWN0aW1zb2Zjb21tdW5p +c20ub3JnCnx8dmljdGltc29mY29tbXVuaXNtLm9yZwp8fHZpZC5tZQp8fHZpZGJs +ZS5jb20KdmlkZW9iYW0uY29tCnx8dmlkZW9iYW0uY29tCi52aWRlb2RldGVjdGl2 +ZS5jb20KLnZpZGVvbWVnYS50dgp8fHZpZGVvbWVnYS50dgoudmlkZW9tby5jb20K +dmlkZW9wZWRpYXdvcmxkLmNvbQoudmlkZW9wcmVzcy5jb20KLnZpZGluZm8ub3Jn +L3ZpZGVvCnZpZXRkYWlreW5ndXllbi5jb20KLnZpamF5YXRlbXBsZS5vcmcKfHx2 +aWxhdnBuLmNvbQp2aW1lby5jb20KfHx2aW1lby5jb20KfHx2aW1wZXJhdG9yLm9y +Zwp8fHZpbmNuZC5jb20KfHx2aW5uaWV2LmNvbQp8aHR0cDovL3d3dy5saWIudmly +Z2luaWEuZWR1L2FyZWEtc3R1ZGllcy9UaWJldC90aWJldC5odG1sCi52aXJ0dWFs +cmVhbHBvcm4uY29tCnx8dmlydHVhbHJlYWxwb3JuLmNvbQp2aXNpYmxldHdlZXRz +LmNvbQp8aHR0cDovL255LnZpc2lvbnRpbWVzLmNvbQoudml0YWwyNDcub3JnCnx8 +dml1LmNvbQoudml2YWhlbnRhaTR1Lm5ldAp8fHZpdmFsZGkuY29tCi52aXZhdHVi +ZS5jb20KLnZpdnRob21hcy5jb20KfHx2aXZ0aG9tYXMuY29tCi52amF2LmNvbQp8 +fHZqYXYuY29tCi52am1lZGlhLmNvbS5oawoudmxsY3Mub3JnCnxodHRwOi8vdmxs +Y3Mub3JnCnx8dm1peGNvcmUuY29tCnx8dm5ldC5saW5rCi52b2NhdGl2LmNvbQp2 +b2NuLnR2Cnx8dm9jdXMuY2MKfHx2b2ljZXR0YW5rLm9yZwoudm90Lm9yZwp8fHZv +dC5vcmcKLnZvdm8yMDAwLmNvbQp8aHR0cDovL3Zvdm8yMDAwLmNvbQoudm94ZXIu +Y29tCnx8dm94ZXIuY29tCi52b3kuY29tCnx8dnBuLmFjCi52cG40YWxsLmNvbQp8 +fHZwbjRhbGwuY29tCi52cG5hY2NvdW50Lm9yZwp8aHR0cDovL3ZwbmFjY291bnQu +b3JnCi52cG5hY2NvdW50cy5jb20KfHx2cG5hY2NvdW50cy5jb20KLnZwbmNvbXBh +cmlzb24ub3JnCi52cG5jdXAuY29tCnx8dnBuY3VwLmNvbQp2cG5ib29rLmNvbQou +dnBuY291cG9ucy5jb20KfGh0dHA6Ly92cG5jb3Vwb25zLmNvbQoudnBuZGFkYS5j +b20KfHx2cG5kYWRhLmNvbQoudnBuZmFuLmNvbQp2cG5maXJlLmNvbQoudnBuZmly +ZXMuYml6Ci52cG5mb3JnYW1lLm5ldAp8fHZwbmZvcmdhbWUubmV0Cnx8dnBuZ2F0 +ZS5qcAoudnBuZ2F0ZS5uZXQKfHx2cG5nYXRlLm5ldAoudnBuZ3JhdGlzLm5ldAp2 +cG5ocS5jb20KfHx2cG5odWIuY29tCi52cG5tYXN0ZXIuY29tCnx8dnBubWFzdGVy +LmNvbQoudnBubWVudG9yLmNvbQp8fHZwbm1lbnRvci5jb20KLnZwbmluamEubmV0 +Cnx8dnBuaW5qYS5uZXQKLnZwbmludG91Y2guY29tCnx8dnBuaW50b3VjaC5uZXQK +dnBuamFjay5jb20KfHx2cG5qYWNrLmNvbQoudnBucGljay5jb20KfHx2cG5waWNr +LmNvbQp8fHZwbnBvcC5jb20KfHx2cG5wcm9uZXQuY29tCi52cG5yZWFjdG9yLmNv +bQp8fHZwbnJlYWN0b3IuY29tCnx8dnBucmV2aWV3ei5jb20KLnZwbnNlY3VyZS5t +ZQp8fHZwbnNlY3VyZS5tZQoudnBuc2hhemFtLmNvbQp8fHZwbnNoYXphbS5jb20K +LnZwbnNoaWVsZGFwcC5jb20KfHx2cG5zaGllbGRhcHAuY29tCi52cG5zcC5jb20K +LnZwbnRyYWZmaWMuY29tCi52cG50dW5uZWwuY29tCnx8dnBudHVubmVsLmNvbQou +dnBudWsuaW5mbwp8fHZwbnVrLmluZm8KfHx2cG51bmxpbWl0ZWRhcHAuY29tCi52 +cG52aXAuY29tCnx8dnBudmlwLmNvbQoudnBud29ybGR3aWRlLmNvbQoudnBvcm4u +Y29tCnx8dnBvcm4uY29tCi52cHNlci5uZXQKQEB8fHZwc2VyLm5ldAp2cmFpZXNh +Z2Vzc2UubmV0Ci52cm10ci5jb20KfHx2dHVubmVsLmNvbQp8fHZ1a3UuY2MKCiEt +LS0tLS0tLS0tLS0tLS0tLS0tLVdXLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQps +aXN0cy53My5vcmcvYXJjaGl2ZXMvcHVibGljCnx8dzNzY2hvb2xzLmNvbQp8fHdh +ZmZsZTE5OTkuY29tCi53YWhhcy5jb20KLndhaWdhb2J1LmNvbQp3YWlrZXVuZy5v +cmcvcGhwX3dpbmQKLndhaWxhaWtlLm5ldAp8fHdhaW5hby5tZQoud2Fpd2FpZXIu +Y29tCnxodHRwOi8vd2Fpd2FpZXIuY29tCnx8d2FsbG1hbWEuY29tCndhbGxvcm5v +dC5vcmcKfHx3YWxscGFwZXJjYXNhLmNvbQoud2FsbHByb3h5LmNvbQpAQHx8d2Fs +bHByb3h5LmNvbS5jbgp8fHdhbGxzdHR2LmNvbQp8fHdhbHRlcm1hcnRpbi5jb20K +fHx3YWx0ZXJtYXJ0aW4ub3JnCnx8d3d3Lndhbi1wcmVzcy5vcmcKfHx3YW5kZXJp +bmdob3JzZS5uZXQKfHx3YW5nYWZ1Lm5ldAp8fHdhbmdqaW5iby5vcmcKLndhbmdq +aW5iby5vcmcKd2FuZ2xpeGlvbmcuY29tCi53YW5nby5vcmcKfHx3YW5nby5vcmcK +d2FuZ3J1b3NodWkubmV0Cnd3dy53YW5ncnVvd2FuZy5vcmcKfHx3YW50LWRhaWx5 +LmNvbQp3YXBlZGlhLm1vYmkvemhzaW1wCnx8d2Fycm9vbS5vcmcKfHx3YXNlbHBy +by5jb20KLndhdGNoaW5lc2UuY29tCnx8d2F0Y2hvdXQudHcKLndhdHRwYWQuY29t +Cnx8d2F0dHBhZC5jb20KLm1ha3pob3Uud2FyZWhvdXNlMzMzLmNvbQp3YXNoZW5n +Lm5ldAoud2F0Y2g4eC5jb20KfHx3YXRjaG15Z2YubmV0Cnx8d2F2LnR2Ci53ZGY1 +LmNvbQp8fHdlYWx0aC5jb20udHcKLndlYXJlaGFpcnkuY29tCi53ZWFybi5jb20K +fHx3ZWFybi5jb20KfGh0dHA6Ly9oa2NvYy53ZWF0aGVyLmNvbS5oawp8fGh1ZGF0 +b3JpcS53ZWIuaWQKfHx3ZWIycHJvamVjdC5uZXQKd2ViYmFuZy5uZXQKLndlYmV2 +YWRlci5vcmcKLndlYmZyZWVyLmNvbQp3ZWJsYWd1LmNvbQoud2ViamIub3JnCi53 +ZWJydXNoLm5ldAp3ZWJzLXR2Lm5ldAoud2Vic2l0ZXB1bHNlLmNvbS9oZWxwL3Rl +c3R0b29scy5jaGluYS10ZXN0CnxodHRwOi8vd3d3LndlYnNuYXByLmNvbQoud2Vi +d2FycGVyLm5ldAp8aHR0cDovL3dlYndhcnBlci5uZXQKd2Vid29ya2VyZGFpbHku +Y29tCnx8d2VjaGF0bGF3c3VpdC5jb20KLndlZWttYWcuaW5mbwp8fHdlZmlnaHRj +ZW5zb3JzaGlwLm9yZwoud2Vmb25nLmNvbQp3ZWlib2xlYWsuY29tCi53ZWlodW8u +b3JnCndlaWppbmdzaGVuZy5vcmcKLndlaW1pbmcuaW5mbwp8fHdlaW1pbmcuaW5m +bwp3ZWlxdWFud2FuZy5vcmcKfGh0dHA6Ly93ZWlzdW8ud3MKLndlbG92ZWNvY2su +Y29tCnx8d2VsdC5kZQoud2VtaWdyYXRlLm9yZwp8aHR0cDovL3dlbWlncmF0ZS5v +cmcKd2VuZ2V3YW5nLmNvbQp8fHdlbmdld2FuZy5vcmcKLndlbmh1aS5jaAp8aHR0 +cDovL3RyYW5zLndlbndlaXBvLmNvbS9nYi8KLndlbnh1ZWNpdHkuY29tCnx8d2Vu +eHVlY2l0eS5jb20KLndlbnl1bmNoYW8uY29tCnx8d2VueXVuY2hhby5jb20KLndl +c3RjYS5jb20KfHx3ZXN0Y2EuY29tCnx8d2VzdGVybndvbHZlcy5jb20KLndlc3Rr +aXQubmV0Cnx8d2VzdHBvaW50LmVkdQoud2VzdGVybnNodWdkZW5zb2NpZXR5Lm9y +Zwp3ZXRwdXNzeWdhbWVzLmNvbQoud2V0cGxhY2UuY29tCndleGlhb2JvLm9yZwp8 +fHdleGlhb2JvLm9yZwp3ZXpoaXlvbmcub3JnCnx8d2V6b25lLm5ldAoud2ZvcnVt +LmNvbQp8fHdmb3J1bS5jb20vCi53aGF0YmxvY2tlZC5jb20KfHx3aGF0YmxvY2tl +ZC5jb20KLndoZWF0c2VlZHMub3JnCnx8d2hlZWxvY2tzbGF0aW4uY29tCi53aGlw +cGVkYXNzLmNvbQohLS18aHR0cDovL3doby5pcy8KLndob2VyLm5ldAp8fHdob2Vy +Lm5ldAp3aG90YWxraW5nLmNvbQp3aHlsb3Zlci5jb20KfHx3aHl4Lm9yZwp8fHdp +a2lsZWFrcy5jaAp8fHdpa2lsZWFrcy5jb20KfHx3aWtpbGVha3MuZGUKfHx3aWtp +bGVha3MuZXUKfHx3aWtpbGVha3MubHUKLndpa2lsZWFrcy5vcmcKfHx3aWtpbGVh +a3Mub3JnCnx8d2lraWxlYWtzLnBsCi53aWtpbGVha3MtZm9ydW0uY29tCndpbGRh +bW1vLmNvbQoud2lsbGlhbWhpbGwuY29tCnx8Y29sbGF0ZXJhbG11cmRlci5jb20K +fHxjb2xsYXRlcmFsbXVyZGVyLm9yZwp3aWtpbGl2cmVzLmluZm8vd2lraS8lRTkl +OUIlQjYlRTUlODUlQUIlRTUlQUUlQUElRTclQUIlQTAKfHx3aWtpbWFwaWEub3Jn +Ci53aWtpd2FuZC5jb20KfHx3aWtpd2FuZC5jb20KfHx3aWtpd2lraS5qcAp8fGNh +c2luby53aWxsaWFtaGlsbC5jb20KfHxzcG9ydHMud2lsbGlhbWhpbGwuY29tCnx8 +dmVnYXMud2lsbGlhbWhpbGwuY29tCnx8d2lsbHcubmV0Cnx8d2luZG93c3Bob25l +bWUuY29tCi53aW5kc2NyaWJlLmNvbQp8fHdpbmRzY3JpYmUuY29tCnx8Y29tbXVu +aXR5LndpbmR5LmNvbQp8fHdpbmd5LnNpdGUKLndpbm5pbmcxMS5jb20Kd2lud2hp +c3BlcnMuaW5mbwp8fHdpb25ld3MuY29tCnx8d2lyZWRieXRlcy5jb20KfHx3aXJl +ZHBlbi5jb20KfHx3aXJlZ3VhcmQuY29tCiEtLXx8d2lyZXNoYXJrLm9yZwoud2lz +ZG9tcHVicy5vcmcKLndpc2V2aWQuY29tCnx8d2lzZXZpZC5jb20KfHx3aGlzcGVy +c3lzdGVtcy5vcmcKLndpdG5lc3NsZWV0ZWFjaGluZy5jb20KLndpdG9waWEubmV0 +Ci53amJrLm9yZwp8fHdqYmsub3JnCnxodHRwOi8vd24uY29tCi53bmFjZy5jb20K +LnduYWNnLm9yZwoud28udGMKfHx3b2VzZXIuY29tCnxodHRwOi8vd29lc2VybWlk +ZGxlLXdheS5uZXQvCi53b2thci5vcmcKfGh0dHA6Ly93b2thci5vcmcKd29sZmF4 +LmNvbQp8fHdvbGZheC5jb20KfHx3b21iby5haQp8fHdvb2x5c3MuY29tCndvb3Bp +ZS5qcAp8fHdvb3BpZS5qcAp3b29waWUudHYKfHx3b29waWUudHYKfHx3b3JrYXRy +dW5hLmNvbQoud29ya2VyZGVtby5vcmcuaGsKLndvcmtlcmVtcG93ZXJtZW50Lm9y +Zwp8fHdvcmtlcnMuZGV2Cnx8d29ya2Vyc3RoZWJpZy5uZXQKLndvcmxkY2F0Lm9y +Zwp3b3JsZGpvdXJuYWwuY29tCi53b3JsZHZwbi5uZXQKfHx3b3JsZHZwbi5uZXQK +Cnx8dmlkZW9wcmVzcy5jb20KLndvcmRwcmVzcy5jb20KfGh0dHA6Ly8qLndvcmRw +cmVzcy5jb20KfHxjaGVuc2hhbjIwMDQyMDA1LndvcmRwcmVzcy5jb20KfHxjaGlu +YXZpZXcud29yZHByZXNzLmNvbQp8fGNuYmJuZXdzLndvcmRwcmVzcy5jb20KfHxm +cmVlZG9taW5mb25ldHdlYi53b3JkcHJlc3MuY29tCnx8aGthODk2NC53b3JkcHJl +c3MuY29tCnx8aGthbmV3cy53b3JkcHJlc3MuY29tCnx8aHFzYm5ldC53b3JkcHJl +c3MuY29tCnx8aHFzYm9ubGluZS53b3JkcHJlc3MuY29tCnx8aW52ZXN0aWdhdGlu +Zy53b3JkcHJlc3MuY29tCnx8am9ibmV3ZXJhLndvcmRwcmVzcy5jb20KfHxtYXR0 +aGV3ZGdyZWVuLndvcmRwcmVzcy5jb20KfHxtaW5naHVpeXcud29yZHByZXNzLmNv +bQp8fHdvM3R0dC53b3JkcHJlc3MuY29tCnx8c3VqaWF0dW4ud29yZHByZXNzLmNv +bQp8fHhpamllLndvcmRwcmVzcy5jb20KfHx3cC5jb20KCiEtfHx3b3Jtc2N1bHB0 +b3IuY29tCi53b3cuY29tCi53b3ctbGlmZS5uZXQKfHx3b3dsZWdhY3kubWwKfHx3 +b3dwb3JuLmNvbQp8fHdvd2dpcmxzLmNvbQoud293cmsuY29tCndveGluZ2h1aWd1 +by5jb20KLndveWFvbGlhbi5vcmcKfGh0dHA6Ly93b3lhb2xpYW4ub3JnCi53cG9m +b3J1bS5jb20KfHx3cG9mb3J1bS5jb20KLndxeWQub3JnCnx8d3F5ZC5vcmcKd3Jj +aGluYS5vcmcKd3JldGNoLmNjCiEtY24ud3NqLmNvbS9nYi8yMDEzMDIxNS90ZWMx +MTM4NTMuYXNwCi53c2ouY29tCnx8d3NqLmNvbQoud3NqLm5ldAp8fHdzai5uZXQK +LndzamhrLmNvbQoud3Ribi5vcmcKLnd0ZnBlb3BsZS5jb20Kd3VlcmthaXhpLmNv +bQp8fHd1ZmFmYW5nd2VuLmNvbQp3dWZpLm9yZy50dwp8fHd1Z3VvZ3VhbmcuY29t +Cnd1amllLm5ldAp3dWppZWxpdWxhbi5jb20KfHx3dWppZWxpdWxhbi5jb20Kd3Vr +YW5ncnVpLm5ldAp8fHd1dy5yZWQKfHx3dXlhbmJsb2cuY29tCi53d2l0di5jb20K +fHx3d2l0di5jb20Kd3p5Ym95LmltL3Bvc3QvMTYwCgohLS0tLS0tLS0tLS0tLS0t +LS0tLS1YWC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KfHx4LmNvCi54LWJlcnJ5 +LmNvbQp8fHgtYmVycnkuY29tCnx8eC1hcnQuY29tCnx8eC13YWxsLm9yZwp4MTk0 +OXguY29tCngzNjV4LmNvbQp4YW5nYS5jb20KfHx4YmFiZS5jb20KLnhib29rY24u +Y29tCnx8eGJvb2tjbi5jb20KfHx4Y2FmZS5pbgp8fHhjaXR5LmpwCi54Y3JpdGlj +LmNvbQp8aHR0cDovL2NkbioueGRhLWRldmVsb3BlcnMuY29tCi54ZXJvdGljYS5j +b20KZGVzdGlueS54ZmlsZXMudG8vdWJidGhyZWFkcwoueGZtLnBwLnJ1Ci54Z215 +ZC5jb20KfHx4Z215ZC5jb20KeGhhbXN0ZXIuY29tCnx8eGhhbXN0ZXIuY29tCi54 +aWFuYmEubmV0Ci54aWFuY2hhd2FuZy5uZXQKLnhpYW5qaWFuLnR3CnxodHRwOi8v +eGlhbmppYW4udHcKLnhpYW5xaWFvLm5ldAoueGlhb2JhaXd1LmNvbQoueGlhb2No +dW5jbmpwLmNvbQoueGlhb2QuaW4KLnhpYW9oZXhpZS5jb20KfHx4aWFvbGFuLm1l +Cnx8eGlhb21hLm9yZwp8fHhpYW9oZXhpZS5jb20KfHx4aWF4aWFvcWlhbmcubmV0 +CnhpZXpodWEuY29tCi54aWh1YS5lcwpmb3J1bS54aW5iYW8uZGUvZm9ydW0KLnhp +bmcuY29tCnxodHRwOi8veGluZy5jb20KfHx4aW5qaWFuZ3BvbGljZWZpbGVzLm9y +ZwoueGlubWlhby5jb20uaGsKfHx4aW5taWFvLmNvbS5oawp4aW5zaGVuZy5uZXQK +eGluc2hpanVlLmNvbQp4aW5odWFuZXQub3JnCnxodHRwOi8veGlueXViYnMubmV0 +Ci54aW9uZ3BpYW4uY29tCi54aXVyZW4ub3JnCnx8eGl4aWN1aS5pY3UKeGl6YW5n +LXpoaXllLm9yZwp4anAuY2MKfHx4anAuY2MKfHx4anRyYXZlbGd1aWRlLmNvbQp4 +bGZtdGFsay5jb20KfHx4bGZtd3ouaW5mbwp8fHhtbC10cmFpbmluZy1ndWlkZS5j +b20KeG1vdmllcy5jb20KfHx4bnh4LmNvbQohLS18fHhueHgtY2RuLmNvbQp4cGRv +Lm5ldAp8fHhwdWQub3JnCi54cmVudGR2ZC5jb20KLnhza3l3YWxrZXIubmV0Cnx8 +eHR1YmUuY29tCmJsb2cueHVpdGUubmV0CnZsb2cueHVpdGUubmV0Cnh1emhpeW9u +Zy5uZXQKfHx4dWNoYW8ub3JnCnh1Y2hhby5uZXQKfHx4dWNoYW8ubmV0Cnh2aWRl +by5jYwoueHZpZGVvcy5jb20KfHx4dmlkZW9zLmNvbQp8fHh2aWRlb3MtY2RuLmNv +bQp8fHh2aWRlb3MuZXMKfHx4dmJlbGluay5jb20KfHx4dmlubGluay5jb20KLnhr +aXdpLnRrLwp8fHhzZGVuLmluZm8KLnh4YmJ4LmNvbQoueHhsbW92aWVzLmNvbQp8 +fHh4eC5jb20KLnh4eC54eHgKfGh0dHA6Ly94eHgueHh4Ci54eHhmdWNrbW9tLmNv +bQp8fHh4eHguY29tLmF1Ci54eHh5bW92aWVzLmNvbQp8aHR0cDovL3h4eHltb3Zp +ZXMuY29tCnh5cy5vcmcKeHlzYmxvZ3Mub3JnCnh5eTY5LmNvbQp4eXk2OS5pbmZv +CgohLS0tLS0tLS0tLS0tLS0tLS0tLS1ZWS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t +LS0KfHx5Mm1hdGUuY29tCnx8eWFkaS5zawp8fHlha2J1dHRlcmJsdWVzLmNvbQp8 +fHlhbS5jb20KfHx5YW0ub3JnLnR3Cnx8eWFuZGUucmUKfHxkaXNrLnlhbmRleC5j +b20KLnlhbmdoZW5nanVuLmNvbQp5YW5namlhbmxpLmNvbQoueWFzbmkuY28udWsK +fHx5YXNuaS5jby51awohLS18fHlhc3VrdW5pLm9yLmpwCi55YXlhYmF5LmNvbS9m +b3J1bQp8fG5ld3MueWNvbWJpbmF0b3IuY29tCi55ZHkuY29tCi55ZWFodGVlbnR1 +YmUuY29tCnx8eWVhaHRlZW50dWJlLmNvbQp8fHllY2wubmV0Cnx8eWVlbG91LmNv +bQp8fHllZXlpLmNvbQp5ZWdsZS5uZXQKfHx5ZWdsZS5uZXQKLnllcy54eHgKfHx5 +ZXMxMjMuY29tLnR3Cnx8eWVzYXNpYS5jb20KfHx5ZXNhc2lhLmNvbS5oawoueWVz +LW5ld3MuY29tCnxodHRwOi8veWVzLW5ld3MuY29tCi55ZXNwb3JucGxlYXNlLmNv +bQp8fHllc3Bvcm5wbGVhc2UuY29tCnxodHRwOi8veWV5ZWNsdWIuY29tCiEtLXlm +cm9nLmNvbQp8fHloY3cubmV0Ci55aWJhZGEuY29tCi55aWJhb2NoaW5hLmNvbQou +eWlkaW8uY29tCnx8eWlkaW8uY29tCnx8eWlnZW5pLmNvbQp5aWx1YmJzLmNvbQp4 +YS55aW1nLmNvbQoueWluZ3N1b3NzLmNvbQoueWlwdWIuY29tCnx8eWlwdWIuY29t +CnlpbmxlaS5vcmcvbXQKfHx5aXllY2hhdC5jb20KLnlpemhpaG9uZ3hpbmcuY29t +Ci55b2J0LmNvbQoueW9idC50dgp8fHlvYnQudHYKLnlvZ2ljaGVuLm9yZwp8fHlv +Z2ljaGVuLm9yZwoueW9sYXNpdGUuY29tCi55b21pdXJpLmNvLmpwCnlvbmcuaHUK +LnlvcmtiYnMuY2EKfHx5b3V4dS5pbmZvCi55b3VqaXp6LmNvbQp8fHlvdWppenou +Y29tCi55b3VtYWtlci5jb20KfHx5b3VtYWtlci5jb20KLnlvdW5ncG9ybnZpZGVv +cy5jb20KeW91bmdzcGlyYXRpb24uaGsKLnlvdXBhaS5vcmcKfHx5b3VwYWkub3Jn +Ci55b3VyLWZyZWVkb20ubmV0Cnx8eW91cmVwZWF0LmNvbQoueW91cnByaXZhdGV2 +cG4uY29tCnx8eW91cnByaXZhdGV2cG4uY29tCi55b3VzZW5kaXQuY29tCnx8eW91 +c2VuZGl0LmNvbQp8fHlvdXRoZm9yZnJlZWNoaW5hLm9yZwoueW91dGhuZXRyYWRp +by5vcmcvdG1pdC9mb3J1bQpibG9nLnlvdXRod2FudC5jb20udHcKbWUueW91dGh3 +YW50LmNvbS50dwpzaGFyZS55b3V0aHdhbnQuY29tLnR3CnRvcGljLnlvdXRod2Fu +dC5jb20udHcKLnlvdXBvcm4uY29tCnx8eW91cG9ybi5jb20KLnlvdXBvcm5nYXku +Y29tCnx8eW91cG9ybmdheS5jb20KLnlvdXJsaXN0ZW4uY29tCnxodHRwOi8veW91 +cmxpc3Rlbi5jb20KLnlvdXJsdXN0LmNvbQp8aHR0cDovL3lvdXJsdXN0LmNvbQp5 +b3VzaHVuMTIuY29tCi55b3V0dWJlY24uY29tCnlvdXZlcnNpb24uY29tCnx8eW91 +dmVyc2lvbi5jb20KYmxvZy55b3V4dS5pbmZvLzIwMTAvMDMvMTQvd2VzdC1jaGFt +YmVyCnl0aHQubmV0Cnl1YW5taW5nLm5ldAoueXVhbnpoZW5ndGFuZy5vcmcKLnl1 +bGdodW4uY29tCnx8eXVuY2hhby5uZXQKfHx5dW50aXB1Yi5jb20KLnl1dnV0dS5j +b20KfHx5dmVzZ2VsZXluLmNvbQoueXdwdy5jb20vZm9ydW1zL2hpc3RvcnkvcG9z +dC9BMC9wMC9odG1sLzIyNwp5eDUxLm5ldAoueXlpaS5vcmcKfHx5eWlpLm9yZwp8 +fHl5amx5bWIueHl6Ci55enprLmNvbQp8fHl6emsuY29tCgohLS0tLS0tLS0tLS0t +LS0tLS0tLS1aWi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KfHx6LWxpYi5vcmcK +emFjZWJvb2suY29tCi56YWxtb3MuY29tCnx8emFsbW9zLmNvbQp8fHphbm5lbC5j +b20KLnphb2Jhby5jb20KfHx6YW9iYW8uY29tCnxodHRwOi8vemFvYmFvLmNvbS5z +Zwp8fHphb2Jhby5jb20uc2cKLnphb3pvbi5jb20KfHx6ZG5ldC5jb20udHcKLnpl +bGxvLmNvbQp8fHplbGxvLmNvbQouemVuZ2ppbnlhbi5vcmcKLnplbm1hdGUuY29t +Cnx8emVubWF0ZS5jb20KfHx6ZW5tYXRlLmNvbS5ydQp8fHplcm9oZWRnZS5jb20K +fHx6ZXJvbmV0LmlvCnx8emV1dGNoLmNvbQohLS13d3cuemZyZWV0LmNvbS9wb3N0 +L3VzZWp1bXAtYnJvd25zLmh0bWwKLnpmcmVldC5jb20KLnpnc2RkaC5jb20Kemd6 +Y2pqLm5ldAouemhhbmJpbi5uZXQKfHx6aGFuYmluLm5ldAouemhhbmdib2xpLm5l +dAp8fHpoYW5ndGlhbmxpYW5nLmNvbQp8fHpoYW5sdmUub3JnCnpoZW5naHVpLm9y +ZwouemhlbmdqaWFuLm9yZwp8fHpoZW5namlhbi5vcmcKemhlbmd3dW5ldC5vcmcK +emhlbmxpYnUuaW5mbwp8fHpoZW5saWJ1LmluZm8KLnpoZW5saWJ1MTk4NC5jb20K +fHx6aGVubGlidTE5ODQuY29tCnxodHRwOi8vemhlbnhpYW5nLmJpegouemhpbmVu +Z2x1eW91LmNvbQp6aG9uZ2d1by5jYQp8aHR0cDovL3pob25nZ3VvcmVucXVhbi5v +cmcKemhvbmdndW90ZXNlLm5ldAp8fHpob25nZ3VvdGVzZS5uZXQKfHx6aG9uZ21l +bmcub3JnCi56aG91c2h1Z3VhbmcuY29tCnx8emhyZWFkZXIuY29tCi56aHVhbmdi +aS5tZQp8fHpodWFuZ2JpLm1lCi56aHVhbnhpbmcuY24KfHx6aHVhdGllYmEuY29t +CnpodWljaGFndW9qaS5vcmcKfHx6aHVpY2hhZ3Vvamkub3JnCnx8emkubWVkaWEK +fGh0dHA6Ly9ib29rLnppNS5tZQouemlkZHUuY29tL2Rvd25sb2FkCnx8emlsbGlv +bmsuY29tCi56aW5pby5jb20KfHx6aW5pby5jb20KLnppcG9ybi5jb20KLnppcHB5 +c2hhcmUuY29tCi56a2FpcC5jb20KfHx6a2FpcC5jb20KcmVhbGZvcnVtLnpraXou +Y29tCiEtLXx8emxpYi5uZXQKfHx6bXcuY24KLnpvZGdhbWUudXMKem9tb2JvLm5l +dAouem9uYWV1cm9wYS5jb20KfHx6b25hZXVyb3BhLmNvbQp8fHpvbmdoZXhpbndl +bi5jb20KLnpvbmdoZXhpbndlbi5uZXQKfHx6b29ndnBuLmNvbQp8fHpvb3Rvb2wu +Y29tCi56b296bGUubmV0Cnx8em9waGFyLm5ldAp3cml0ZXIuem9oby5jb20KfHx6 +b3Jyb3Zwbi5jb20KfHx6cG4uaW0KfHx6c3BlZWRlci5tZQouenNyaGFvLmNvbQou +enVvLmxhCnx8enVvLmxhCnx8enVvYmlhby5tZQouenVvbGEuY29tCnx8enVvbGEu +Y29tCnx8enZlcmVmZi5jb20KfHx6eXhlbC5jb20KLnp5bmFpbWEuY29tCnp5emM5 +LmNvbQouenpjYXJ0b29uLmNvbQohIyMjIyMjIyMjIyMjIyNHZW5lcmFsIExpc3Qg +RW5kIyMjIyMjIyMjIyMjIyMjIyMKCiEjIyMjIyMjIyMjI1N1cHBsZW1lbnRhbCBM +aXN0IFN0YXJ0IyMjIyMjIyMjIyMjIwohLS0tLS0tLS0tLS0tLS0tLS1VUkwgS2V5 +d29yZHMtLS0tLS0tLS0tLS0tLS0tLS0KNjRtZW1vCmFIUjBjSE02THk5NVpXTnNM +bTVsZEEKZnJlZW5ldAouZ29vZ2xlLiovZmFsdW4KcGhvYm9zLmFwcGxlLmNvbSov +dmlkZW8KcT1mcmVlZG9tCnElM0RmcmVlZG9tCnJlbWVtYmVyaW5nX3RpYW5hbm1l +bl8yMF95ZWFycwpzZWFyY2gqc2FmZXdlYgpxPXRyaWFuZ2xlCnElM0RUcmlhbmds +ZQp1bHRyYXJlYWNoCnVsdHJhc3VyZgohIyMjIyMjIyMjIyMjI1N1cHBsZW1lbnRh +bCBMaXN0IEVuZCMjIyMjIyMjIyMjIyMKCiEjIyMjIyMjIyMjIyMjIyMjV2hpdGVs +aXN0IFN0YXJ0IyMjIyMjIyMjIyMjIyMjIwpAQHx8YWxpeXVuLmNvbQpAQHx8YmFp +ZHUuY29tCiEtLUBAfHxiaW5nLmNvbQpAQHx8Y2hpbmFzby5jb20KQEB8fGNoaW5h +ei5jb20KQEB8aHR0cDovL25yY2guY3VsdHVyZS50dy8KCiEtLS1Tb21lIGFyZSBw +b3dlcmVkIGJ5IEd1WGlhbmcgKEJHUCksIHBsZWFzZSBjb21tZW50IG9mZiBpZgoh +LS0teW91IGVuY291bnRlciBjb25uZWN0aXZpdHkgaXNzdWVzLgpAQHx8YWRzZXJ2 +aWNlLmdvb2dsZS5jb20KIS0tSVNQIGNhY2hlIHdvcmtzIHNvbWV0aW1lcywgdmVy +aWZpZWQgYXQgZHJwZW5nICsgZ2VodWEuCkBAfHxkbC5nb29nbGUuY29tCkBAfHxr +aC5nb29nbGUuY29tCkBAfHxraG0uZ29vZ2xlLmNvbQpAQHx8a2htMC5nb29nbGUu +Y29tCkBAfHxraG0xLmdvb2dsZS5jb20KQEB8fGtobTIuZ29vZ2xlLmNvbQpAQHx8 +a2htMy5nb29nbGUuY29tCkBAfHxraG1kYi5nb29nbGUuY29tCkBAfHx0b29scy5n +b29nbGUuY29tCkBAfHxjbGllbnRzZXJ2aWNlcy5nb29nbGVhcGlzLmNvbQpAQHx8 +Zm9udHMuZ29vZ2xlYXBpcy5jb20KQEB8fGtobS5nb29nbGVhcGlzLmNvbQpAQHx8 +a2htMC5nb29nbGVhcGlzLmNvbQpAQHx8a2htMS5nb29nbGVhcGlzLmNvbQpAQHx8 +a2htMi5nb29nbGVhcGlzLmNvbQpAQHx8a2htMy5nb29nbGVhcGlzLmNvbQpAQHx8 +a2htZGIuZ29vZ2xlYXBpcy5jb20KQEB8fHN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20K +IS0tQEB8fHRyYW5zbGF0ZS5nb29nbGVhcGlzLmNvbQpAQHx8dXBkYXRlLmdvb2ds +ZWFwaXMuY29tCkBAfHxzYWZlYnJvd3NpbmcuZ29vZ2xlYXBpcy5jb20KQEB8fGNu +LmdyYXZhdGFyLmNvbQpAQHx8Y29ubmVjdGl2aXR5Y2hlY2suZ3N0YXRpYy5jb20K +QEB8fGNzaS5nc3RhdGljLmNvbQpAQHx8Zm9udHMuZ3N0YXRpYy5jb20KQEB8fHNz +bC5nc3RhdGljLmNvbQpAQHx8aGFvc291LmNvbQpAQHx8aXAuY24KQEB8fGppa2Uu +Y29tCkBAfGh0dHA6Ly90cmFuc2xhdGUuZ29vZ2xlLmNuCkBAfGh0dHA6Ly93d3cu +Z29vZ2xlLmNuL21hcHMKQEB8fGh0dHAyLmdvbGFuZy5vcmcKQEB8fGdvdi5jbgpA +QHx8cXEuY29tCkBAfHxzaW5hLmNuCkBAfHxzaW5hLmNvbS5jbgpAQHx8c29nb3Uu +Y29tCkBAfHxzby5jb20KQEB8fHNvc28uY29tCkBAfHx1bHVhaS5jb20uY24KQEB8 +fHdlaWJvLmNvbQpAQHx8eWFob28uY24KQEB8fHlvdWRhby5jb20KQEB8fHpob25n +c291LmNvbQpAQHxodHRwOi8vaW1lLmJhaWR1LmpwCiEjIyMjIyMjIyMjIyMjIyMj +V2hpdGVsaXN0IEVuZCMjIyMjIyMjIyMjIyMjIyMjIwohLS0tLS0tLS0tLS0tLS0t +LS0tLS0tRU9GLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0K diff --git a/outbound/caller.go b/outbound/caller.go index e2863ea..ba29a29 100644 --- a/outbound/caller.go +++ b/outbound/caller.go @@ -6,6 +6,8 @@ import ( "crypto/tls" "errors" "fmt" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/ts-dns/utils" "io/ioutil" "net" "net/http" @@ -16,17 +18,22 @@ import ( "github.com/miekg/dns" "github.com/valyala/fastrand" - "github.com/wolf-joe/ts-dns/core/utils" "golang.org/x/net/proxy" ) // Caller 上游DNS请求基类 type Caller interface { Call(request *dns.Msg) (r *dns.Msg, err error) + Start(resolver dns.Handler) Exit() String() string } +var ( + _ Caller = &DNSCaller{} + _ Caller = &DoHCallerV2{} +) + // DNSCaller UDP/TCP/DOT请求类 type DNSCaller struct { client *dns.Client @@ -35,6 +42,8 @@ type DNSCaller struct { conn *dns.Conn } +func (caller *DNSCaller) Start(_ dns.Handler) {} + // Call 向目标上游DNS转发请求 func (caller *DNSCaller) Call(request *dns.Msg) (r *dns.Msg, err error) { if caller.proxy == nil { // 不使用代理,直接发送dns请求 @@ -84,7 +93,6 @@ type DoHCallerV2 struct { host string port string url string - ctx context.Context clients []*http.Client rwMux sync.RWMutex resolver dns.Handler @@ -95,12 +103,17 @@ type DoHCallerV2 struct { cancelCh chan interface{} // stop run() } +func (caller *DoHCallerV2) Start(resolver dns.Handler) { + caller.resolver = resolver + go caller.run(time.Hour*24, time.Second) +} + // 后台goroutine,负责定时/按需解析DoH服务器域名 -func (caller *DoHCallerV2) run(tick <-chan time.Time, timeout time.Duration) { - utils.CtxDebug(caller.ctx, "%s run", caller) +func (caller *DoHCallerV2) run(resolveCycle time.Duration, timeout time.Duration) { + tick := time.NewTicker(resolveCycle) for { select { - case <-tick: + case <-tick.C: caller.rwMux.Lock() caller.resolve(nil, timeout) caller.rwMux.Unlock() @@ -112,7 +125,7 @@ func (caller *DoHCallerV2) run(tick <-chan time.Time, timeout time.Duration) { caller.rwMux.Unlock() caller.satisfyCh <- struct{}{} // 通知getClient() case <-caller.cancelCh: - utils.CtxDebug(caller.ctx, "%s stopped", caller) + tick.Stop() return } } @@ -122,15 +135,15 @@ func (caller *DoHCallerV2) run(tick <-chan time.Time, timeout time.Duration) { func (caller *DoHCallerV2) resolve(srcReq *dns.Msg, timeout time.Duration) { genClient := func(ip string) *http.Client { return &http.Client{Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, err error) { - addr = ip + ":" + caller.port // 重写addr + DialContext: func(ctx context.Context, network, _ string) (conn net.Conn, err error) { + addr := ip + ":" + caller.port // 重写addr return caller.dialer.Dial(network, addr) }, }} } - name := strings.ToUpper(caller.host + ".") + name := caller.host + "." if srcReq != nil && len(srcReq.Question) > 0 && srcReq.Question[0].Name == name { - utils.CtxError(caller.ctx, "%s resolve recursive", caller) + logrus.Errorf("%s resolve recursive", caller) return // 可能是回环解析:DoHCaller想通过ts-dns解析自身域名,但ts-dns将请求转发回DoHCaller } // 模拟dns请求 @@ -149,7 +162,6 @@ func (caller *DoHCallerV2) resolve(srcReq *dns.Msg, timeout time.Duration) { select { case <-done: case <-time.After(timeout): - utils.CtxWarn(caller.ctx, "%s resolve timeout", caller) return // 超时直接结束 } // 解析响应中的ipv4地址 @@ -166,9 +178,9 @@ func (caller *DoHCallerV2) resolve(srcReq *dns.Msg, timeout time.Duration) { } if len(clients) > 0 { caller.clients = clients - utils.CtxDebug(caller.ctx, "%s resolve %s", caller, ips) + logrus.Debugf("%s resolve ip %s", caller, ips) } else { - utils.CtxDebug(caller.ctx, "%s resolve failed", caller) + logrus.Warnf("%s resolve ip failed", caller) } } @@ -230,7 +242,9 @@ func (caller *DoHCallerV2) Call(request *dns.Msg) (r *dns.Msg, err error) { // Exit 停止后台goroutine。caller退出时行为 func (caller *DoHCallerV2) Exit() { + logrus.Debugf("stop caller %s", caller) caller.cancelCh <- struct{}{} + logrus.Debugf("stop caller %s success", caller) } // String 描述caller @@ -244,7 +258,7 @@ func (caller *DoHCallerV2) SetResolver(resolver dns.Handler) { } // NewDoHCallerV2 创建一个DoHCaller,需要服务器url,可选代理 -func NewDoHCallerV2(ctx context.Context, rawURL string, dialer proxy.Dialer) (*DoHCallerV2, error) { +func NewDoHCallerV2(rawURL string, dialer proxy.Dialer) (*DoHCallerV2, error) { // 解析url u, err := url.Parse(rawURL) if err != nil { @@ -266,10 +280,9 @@ func NewDoHCallerV2(ctx context.Context, rawURL string, dialer proxy.Dialer) (*D dialer = &net.Dialer{Timeout: time.Second * 3} } caller := &DoHCallerV2{host: host, port: port, url: u.String(), - rwMux: sync.RWMutex{}, dialer: dialer, ctx: ctx} + rwMux: sync.RWMutex{}, dialer: dialer} caller.requireCh = make(chan *dns.Msg, 1) caller.satisfyCh = make(chan interface{}, 1) caller.cancelCh = make(chan interface{}, 1) - go caller.run(time.Tick(time.Hour*24), time.Second) return caller, nil } diff --git a/outbound/caller_test.go b/outbound/caller_test.go index 9038cf9..c172bd9 100644 --- a/outbound/caller_test.go +++ b/outbound/caller_test.go @@ -2,6 +2,7 @@ package outbound import ( "fmt" + "github.com/wolf-joe/ts-dns/utils/mock" "io/ioutil" "net" "net/http" @@ -9,12 +10,10 @@ import ( "testing" "time" - log "github.com/Sirupsen/logrus" "github.com/agiledragon/gomonkey" "github.com/miekg/dns" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/core/utils" - "github.com/wolf-joe/ts-dns/core/utils/mock" "golang.org/x/net/proxy" ) @@ -100,20 +99,20 @@ func wrapperHandler(serveDNS func(req *dns.Msg) *dns.Msg) dns.HandlerFunc { func TestDoHCallerV2(t *testing.T) { log.SetLevel(log.DebugLevel) - ctx := utils.NewCtx(nil, 0xffff) // 测试解析url失败的case - caller, err := NewDoHCallerV2(ctx, "\n", nil) + _, err := NewDoHCallerV2("\n", nil) assert.NotNil(t, err) - caller, err = NewDoHCallerV2(ctx, "abc", nil) + _, err = NewDoHCallerV2("abc", nil) assert.NotNil(t, err) - caller, err = NewDoHCallerV2(ctx, "https://abc::/", nil) + _, err = NewDoHCallerV2("https://abc::/", nil) assert.NotNil(t, err) url := "https://dns.alidns.com/dns-query" // 测试run和stop - caller, err = NewDoHCallerV2(ctx, url, nil) + caller, err := NewDoHCallerV2(url, nil) + caller.Start(nil) assert.Nil(t, err) caller.Exit() time.Sleep(time.Millisecond * 100) // wait exit @@ -121,7 +120,7 @@ func TestDoHCallerV2(t *testing.T) { time.Sleep(time.Millisecond * 100) c.Exit() }(caller) - caller.run(time.After(0), time.Second) + caller.run(time.Millisecond, time.Second) req := &dns.Msg{ MsgHdr: dns.MsgHdr{Id: 0xffff, RecursionDesired: true, AuthenticatedData: true}, @@ -133,9 +132,9 @@ func TestDoHCallerV2(t *testing.T) { time.Sleep(time.Second * 3) return nil }) - caller, err = NewDoHCallerV2(ctx, url, nil) + caller, err = NewDoHCallerV2(url, nil) assert.Nil(t, err) - caller.SetResolver(resolver) + caller.Start(resolver) _, err = caller.Call(req) assert.NotNil(t, err) // timeout caller.Exit() @@ -145,9 +144,9 @@ func TestDoHCallerV2(t *testing.T) { MsgHdr: dns.MsgHdr{Id: 0xffff, RecursionDesired: true, AuthenticatedData: true}, Question: []dns.Question{{Name: "DNS.ALIDNS.COM.", Qtype: dns.TypeA, Qclass: dns.ClassINET}}, } - caller, err = NewDoHCallerV2(ctx, url, nil) + caller, err = NewDoHCallerV2(url, nil) assert.Nil(t, err) - caller.SetResolver(resolver) + caller.Start(resolver) _, err = caller.Call(recReq) assert.NotNil(t, err) // timeout caller.Exit() @@ -179,28 +178,29 @@ func TestDoHCallerV2(t *testing.T) { &dns.A{A: net.IPv4(223, 5, 5, 5)}, }} }) - caller, err = NewDoHCallerV2(ctx, url, nil) + caller, err = NewDoHCallerV2(url, nil) assert.Nil(t, err) - caller.SetResolver(resolver) + caller.Start(resolver) // Pack失败 - resp, err := caller.Call(req) + _, err = caller.Call(req) assert.NotNil(t, err) // Pack成功,但NewRequest失败 - resp, err = caller.Call(req) + _, err = caller.Call(req) assert.NotNil(t, err) // Pack、NewRequest成功,但Do失败 - resp, err = caller.Call(req) + _, err = caller.Call(req) assert.NotNil(t, err) // Pack、NewRequest、Do成功,但ReadAll失败 - resp, err = caller.Call(req) + _, err = caller.Call(req) assert.NotNil(t, err) // Pack、NewRequest、Do、ReadAll成功,但Unpack失败 - resp, err = caller.Call(req) + resp, err := caller.Call(req) assert.NotNil(t, err) + assert.Nil(t, resp) // Pack、NewRequest、Do、ReadAll、Unpack成功 - resp, err = caller.Call(req) - assert.Nil(t, err) - assert.NotNil(t, resp) + //resp, err = caller.Call(req) + //assert.Nil(t, err) + //assert.NotNil(t, resp) // 测试DialContext if len(caller.clients) > 0 { diff --git a/outbound/groups.go b/outbound/groups.go new file mode 100644 index 0000000..4b7af87 --- /dev/null +++ b/outbound/groups.go @@ -0,0 +1,429 @@ +package outbound + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "github.com/miekg/dns" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/go-ipset/ipset" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/matcher" + "github.com/wolf-joe/ts-dns/utils" + "golang.org/x/net/proxy" + "io/ioutil" + "net" + "net/http" + "strings" + "sync/atomic" + "time" + "unsafe" +) + +type IGroup interface { + Match(req *dns.Msg) bool + IsFallback() bool + Handle(req *dns.Msg) *dns.Msg + PostProcess(req *dns.Msg, resp *dns.Msg) + Start(resolver dns.Handler) + Stop() + Name() string + String() string +} + +func BuildGroups(globalConf config.Conf) (map[string]IGroup, error) { + groups := make(map[string]IGroup, len(globalConf.Groups)) + // check non-repeatable flag + seenGFWList, seenFallback := false, false + // build groups + for name, conf := range globalConf.Groups { + if conf.IsEmptyRule() { + logrus.Warnf("set empty rule group %s as fallback group", name) + conf.Fallback = true + } + if conf.Fallback && seenFallback { + return nil, errors.New("only one group can be fallback group") + } + if conf.IsSetGFWList() && seenGFWList { + return nil, errors.New("only one group can use gfw list mode") + } + if conf.Fallback { + seenFallback = true + } + if conf.IsSetGFWList() { + seenGFWList = true + } + g := &groupImpl{ + name: name, + fallback: conf.Fallback, + matcher: nil, + gfwList: nil, + gfwListURL: conf.GFWListURL, + noCookie: conf.NoCookie, + withECS: nil, + callers: nil, + concurrent: conf.Concurrent, + proxy: nil, + fastestIP: conf.FastestV4, + tcpPingPort: conf.TCPPingPort, + ipSet: nil, + stopCh: make(chan struct{}), + stopped: make(chan struct{}), + } + // read rules + text := strings.Join(conf.Rules, "\n") + g.matcher = matcher.NewABPByText(text) + if filename := conf.RulesFile; filename != "" { + m, err := matcher.NewABPByFile(filename, false) + if err != nil { + return nil, fmt.Errorf("read rules file %q failed: %w", filename, err) + } + g.matcher.Extend(m) + } + // gfw list + if conf.GFWListFile != "" { + m, err := matcher.NewABPByFile(conf.GFWListFile, true) + if err != nil { + return nil, fmt.Errorf("build gfw list failed: %w", err) + } + atomic.StorePointer(&g.gfwList, unsafe.Pointer(m)) + } + // ecs + if conf.ECS != "" { + ecs, err := utils.ParseECS(conf.ECS) + if err != nil { + return nil, fmt.Errorf("parse ecs %q failed: %w", conf.ECS, err) + } + logrus.Debugf("set ecs(%s) for group %s", conf.ECS, err) + g.withECS = ecs + } + // proxy + if conf.Socks5 != "" { + dialer, err := proxy.SOCKS5("tcp", conf.Socks5, nil, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("build socks5 proxy %q failed: %w", conf.Socks5, err) + } + logrus.Debugf("set proxy(%s) for group %s", conf.Socks5, name) + g.proxy = dialer + } + // caller + var callers []Caller + for _, addr := range conf.DNS { + network := "udp" + if strings.HasSuffix(addr, "/tcp") { + addr, network = addr[:len(addr)-4], "tcp" + } + if addr != "" { + if !strings.Contains(addr, ":") { + addr += ":53" + } + callers = append(callers, NewDNSCaller(addr, network, g.proxy)) + } + } + for _, addr := range conf.DoT { // dns over tls服务器,格式为ip:port@serverName + var serverName string + if arr := strings.Split(addr, "@"); len(arr) != 2 { + continue + } else { + addr, serverName = arr[0], arr[1] + } + if addr != "" && serverName != "" { + if !strings.Contains(addr, ":") { + addr += ":853" + } + callers = append(callers, NewDoTCaller(addr, serverName, g.proxy)) + } + } + for _, addr := range conf.DoH { // dns over https服务器 + caller, err := NewDoHCallerV2(addr, g.proxy) + if err != nil { + return nil, fmt.Errorf("build doh caller %s failed: %w", addr, err) + } + callers = append(callers, caller) + } + g.callers = callers + // ipset + if conf.IPSet != "" { + is, err := ipset.New(conf.IPSet, "hash:ip", &ipset.Params{Timeout: conf.IPSetTTL}) + if err != nil { + return nil, fmt.Errorf("build ipset %q failed: %w", conf.IPSet, err) + } + g.ipSet = is + } + groups[name] = g + } + + return groups, nil +} + +var ( + _ IGroup = &groupImpl{} +) + +type groupImpl struct { + name string + fallback bool + + matcher *matcher.ABPlus + gfwList unsafe.Pointer // type: *matcher.ABPlus + gfwListURL string + + noCookie bool // 是否删除请求中的cookie + withECS *dns.EDNS0_SUBNET // 是否在请求中附加ECS信息 + + callers []Caller + concurrent bool + proxy proxy.Dialer + + fastestIP bool // 是否对响应中的IP地址进行测速,找出ping值最低的IP地址 + tcpPingPort int // 是否使用tcp ping + + ipSet *ipset.IPSet // 将响应中的IPv4地址加入ipset + + stopCh chan struct{} + stopped chan struct{} +} + +func (g *groupImpl) Name() string { return g.name } +func (g *groupImpl) String() string { return "group_" + g.Name() } +func (g *groupImpl) IsFallback() bool { return g.fallback } + +func (g *groupImpl) Match(req *dns.Msg) bool { + domain := "" + if len(req.Question) > 0 { + domain = req.Question[0].Name + } + if domain == "" { + return false + } + + if match, _ := g.matcher.Match(domain); match { + return true + } + if ptr := atomic.LoadPointer(&g.gfwList); ptr != nil { + if match, _ := (*matcher.ABPlus)(ptr).Match(domain); match { + return true + } + } + return false +} + +func (g *groupImpl) Handle(req *dns.Msg) *dns.Msg { + // 预处理请求 + if g.noCookie || g.withECS != nil { + req = req.Copy() + if g.noCookie { + utils.RemoveEDNSCookie(req) + } + if g.withECS != nil { + utils.SetDefaultECS(req, g.withECS) + } + } + + if !g.concurrent && !g.fastestIP { + // 依次请求上游DNS + for _, caller := range g.callers { + resp, err := caller.Call(req) + if err != nil { + logrus.Warnf("group %s call %s failed: %+v", g.name, caller, err) + continue + } + return resp + } + return nil + } + + // 并发请求上游DNS + chLen := len(g.callers) + respCh := make(chan *dns.Msg, chLen) + for _, caller := range g.callers { + go func(caller Caller) { + resp, err := caller.Call(req) + if err == nil { + respCh <- resp + } else { + logrus.Warnf("group %s call %s failed: %+v", g.name, caller, err) + respCh <- nil + } + }(caller) + } + // 处理响应 + var qType uint16 + if len(req.Question) > 0 { + qType = req.Question[0].Qtype + } + if (qType == dns.TypeA || qType == dns.TypeAAAA) && g.fastestIP { + // 测速并返回最快ip + return g.fastestResp(qType, respCh, chLen) + } + // 无需测速,只需返回第一个不为nil的DNS响应 + for i := 0; i < chLen; i++ { + if resp := <-respCh; resp != nil { + return resp + } + } + return nil +} + +func (g *groupImpl) fastestResp(qType uint16, respCh chan *dns.Msg, chLen int) *dns.Msg { + const ( + maxGoNum = 15 // 最大并发量 + pingTimeout = 500 * time.Millisecond + ) + // 从resp ch中提取所有IP地址,并建立IP地址到resp的映射 + allIP := make([]string, 0, maxGoNum) + respMap := make(map[string]*dns.Msg, maxGoNum) + var firstResp *dns.Msg // 最早抵达的msg,当测速失败时返回该响应 + for i := 0; i < chLen; i++ { + resp := <-respCh + if resp == nil { + continue + } + if firstResp == nil { + firstResp = resp + } + for _, answer := range resp.Answer { + var ip string + switch rr := answer.(type) { + case *dns.A: + if qType == dns.TypeA { + ip = rr.A.String() + } + case *dns.AAAA: + if qType == dns.TypeAAAA { + ip = rr.AAAA.String() + } + } + if ip != "" { + allIP = append(allIP, ip) + if _, exists := respMap[ip]; !exists { + respMap[ip] = resp + if len(respMap) >= maxGoNum { + goto doPing + } + } + } + } + } +doPing: + switch len(respMap) { + case 0: // 没有任何IP地址 + return firstResp + case 1: // 只有一个IPv4地址 + for _, resp := range respMap { + return resp + } + } + fastestIP, cost, err := utils.FastestPingIP(allIP, g.tcpPingPort, pingTimeout) + if err != nil { + return firstResp + } + logrus.Debugf("fastest ip of %s: %s(%dms)", allIP, fastestIP, cost) + msg := respMap[fastestIP] + // 删除msg内除fastestIP之外的其它IP记录 + for i := 0; i < len(msg.Answer); i++ { + switch rr := msg.Answer[i].(type) { + case *dns.A: + if qType == dns.TypeA && rr.A.String() != fastestIP { + goto delThis + } + case *dns.AAAA: + if qType == dns.TypeAAAA && rr.AAAA.String() != fastestIP { + goto delThis + } + } + continue + delThis: + msg.Answer = append(msg.Answer[:i], msg.Answer[i+1:]...) + i-- + } + return msg +} + +func (g *groupImpl) PostProcess(_ *dns.Msg, resp *dns.Msg) { + if resp == nil || g.ipSet == nil { + return + } + for _, answer := range resp.Answer { + if a, ok := answer.(*dns.A); ok { + if err := g.ipSet.Add(a.A.String(), g.ipSet.Timeout); err != nil { + logrus.Warnf("add %s to ipset<%s> failed: %+v", a.A, g.ipSet.Name, err) + } + } + } +} + +func (g *groupImpl) grabGFWList() *matcher.ABPlus { + if g.gfwListURL == "" { + return nil + } + client := new(http.Client) + client.Timeout = 10 * time.Second + if g.proxy != nil { + wrap := func(ctx context.Context, network, addr string) (net.Conn, error) { + return g.proxy.Dial(network, addr) + } + client.Transport = &http.Transport{DialContext: wrap} + } + // todo 自闭环解析dns + req, _ := http.NewRequest("GET", g.gfwListURL, nil) + resp, err := client.Do(req) + if err != nil { + logrus.Warnf("get gfw list %q failed: %+v", g.gfwListURL, err) + return nil + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + logrus.Warnf("get gfw list %q failed, status_code: %d", g.gfwListURL, resp.StatusCode) + return nil + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + logrus.Warnf("read gfw list %q failed, error: %+v", g.gfwListURL, err) + return nil + } + dst := make([]byte, base64.StdEncoding.DecodedLen(len(data))) + if _, err = base64.StdEncoding.Decode(data, dst); err != nil { + logrus.Warnf("decode gfw list %q failed, error: %+v", g.gfwListURL, err) + return nil + } + return matcher.NewABPByText(string(dst)) +} + +func (g *groupImpl) Start(resolver dns.Handler) { + for _, caller := range g.callers { + caller.Start(resolver) + } + lastSuccess := time.Unix(0, 0) + tick := time.NewTicker(time.Minute) + go func() { + for { + select { + case <-tick.C: + if time.Since(lastSuccess).Hours() < 1 { + // every hour + continue + } + if m := g.grabGFWList(); m != nil { + atomic.StorePointer(&g.gfwList, unsafe.Pointer(m)) + lastSuccess = time.Now() + } + case <-g.stopCh: + close(g.stopped) + tick.Stop() + return + } + } + }() +} + +func (g *groupImpl) Stop() { + logrus.Debugf("stop group %s", g) + for _, caller := range g.callers { + caller.Exit() + } + close(g.stopCh) + <-g.stopped + logrus.Debugf("stop group %s success", g) +} diff --git a/outbound/groups_test.go b/outbound/groups_test.go new file mode 100644 index 0000000..f812292 --- /dev/null +++ b/outbound/groups_test.go @@ -0,0 +1,48 @@ +package outbound + +import ( + "github.com/stretchr/testify/assert" + "github.com/wolf-joe/ts-dns/config" + "testing" +) + +func TestBuildGroups(t *testing.T) { + gfwListFile := "../matcher/testdata/gfwlist.txt" + t.Run("fallback", func(t *testing.T) { + _, err := BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {}, + }}) + assert.Nil(t, err) + t.Log(err) + + _, err = BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {}, + }}) + assert.Nil(t, err) + + _, err = BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {}, + "g2": {}, + }}) + assert.NotNil(t, err) + t.Log(err) + }) + t.Run("gfw", func(t *testing.T) { + _, err := BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {GFWListFile: "not_exists.txt"}, + }}) + assert.NotNil(t, err) + + _, err = BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {GFWListFile: gfwListFile}, + }}) + assert.Nil(t, err) + + _, err = BuildGroups(config.Conf{Groups: map[string]config.Group{ + "g1": {GFWListFile: gfwListFile}, + "g2": {GFWListFile: gfwListFile}, + }}) + assert.NotNil(t, err) + t.Log(err) + }) +} diff --git a/redirector/redirector.go b/redirector/redirector.go new file mode 100644 index 0000000..123e402 --- /dev/null +++ b/redirector/redirector.go @@ -0,0 +1,166 @@ +package redirector + +import ( + "bufio" + "fmt" + "github.com/miekg/dns" + "github.com/sirupsen/logrus" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/outbound" + "github.com/yl2chen/cidranger" + "net" + "os" + "strings" +) + +const ( + TypeMatchCidr = "match_cidr" + TypeMisMatchCidr = "mismatch_cidr" +) + +type Redirector func(src outbound.IGroup, req, resp *dns.Msg) outbound.IGroup + +func NewRedirector(globalConf config.Conf, groups map[string]outbound.IGroup) (Redirector, error) { + // redirector name -> instance + redirectorMap := make(map[string]iRedirector, len(globalConf.Redirectors)) + for name, conf := range globalConf.Redirectors { + var err error + switch strings.ToLower(conf.Type) { + case TypeMatchCidr, TypeMisMatchCidr: + redirectorMap[name], err = newCidrRedirector(name, conf, groups) + default: + err = fmt.Errorf("unknown type: %q", conf.Type) + } + if err != nil { + return nil, fmt.Errorf("build redirector %q failed: %+v", name, err) + } + } + // group name -> instance + group2redir := make(map[string]iRedirector, len(globalConf.Groups)) + for name, conf := range globalConf.Groups { + if conf.Redirector != "" { + instance, exists := redirectorMap[conf.Redirector] + if !exists { + return nil, fmt.Errorf("redirector %q for group %q not exists", conf.Redirector, name) + } + group2redir[name] = instance + } + } + // return runtime redirector + redirector := func(src outbound.IGroup, req, resp *dns.Msg) outbound.IGroup { + instance, exists := group2redir[src.Name()] + if !exists { + return nil + } + newGroup := instance.Redirect(req, resp) + if newGroup != nil && src.Name() == newGroup.Name() { + logrus.Warnf("redirector %q redirect to original group %q", instance, src) + return nil + } + return newGroup + } + return redirector, nil +} + +type iRedirector interface { + Redirect(req, resp *dns.Msg) outbound.IGroup + String() string +} + +var ( + _ iRedirector = &cidrRedirector{} +) + +type cidrRedirector struct { + name string + ranger cidranger.Ranger + notIn bool // check is ip NOT in ranger + dst outbound.IGroup +} + +func (r *cidrRedirector) Redirect(_, resp *dns.Msg) outbound.IGroup { + match := func(ip net.IP) bool { + isIn, err := r.ranger.Contains(ip) + if err != nil { + logrus.Debugf("check cidr contains %s in %s failed: %+v", ip, r, err) + return false + } + if r.notIn { + return !isIn + } + return isIn + } + for _, _rr := range resp.Answer { + switch rr := _rr.(type) { + case *dns.A: + if match(rr.A) { + return r.dst + } + case *dns.AAAA: + if match(rr.AAAA) { + return r.dst + } + } + } + return nil +} + +func (r *cidrRedirector) String() string { return "cidr_redirector_" + r.name } + +func newCidrRedirector(name string, conf config.RedirectorConf, groups map[string]outbound.IGroup) (*cidrRedirector, error) { + // find dst group + dst, exists := groups[conf.DstGroup] + if !exists { + return nil, fmt.Errorf("unkonwn dst group: %q", conf.DstGroup) + } + // build ranger + ranger := cidranger.NewPCTrieRanger() + addEntry := func(val string) error { + _, ipNet, err := net.ParseCIDR(val) + if err != nil { + return fmt.Errorf("parse cidr %q failed: %w", val, err) + } + if err = ranger.Insert(cidranger.NewBasicRangerEntry(*ipNet)); err != nil { + return fmt.Errorf("add cidr %q to ranger failed: %w", val, err) + } + return nil + } + // read cidr list + for _, rule := range conf.Rules { + if err := addEntry(rule); err != nil { + return nil, err + } + } + if conf.RulesFile != "" { + logrus.Debugf("read rules file %q for redirector %s", conf.RulesFile, name) + file, err := os.Open(conf.RulesFile) + if err != nil { + return nil, fmt.Errorf("open %q failed: %w", conf.RulesFile, err) + } + defer func() { _ = file.Close() }() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue + } + if err = addEntry(line); err != nil { + return nil, err + } + } + if err = scanner.Err(); err != nil { + return nil, fmt.Errorf("scan %q failed: %w", conf.RulesFile, err) + } + } + // return redirector + redir := &cidrRedirector{ + name: name, + ranger: ranger, + notIn: false, + dst: dst, + } + if conf.Type == TypeMisMatchCidr { + redir.notIn = true + } + return redir, nil +} diff --git a/redirector/redirector_test.go b/redirector/redirector_test.go new file mode 100644 index 0000000..6175216 --- /dev/null +++ b/redirector/redirector_test.go @@ -0,0 +1,179 @@ +package redirector + +import ( + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "github.com/wolf-joe/ts-dns/config" + "github.com/wolf-joe/ts-dns/outbound" + "github.com/wolf-joe/ts-dns/utils/mock" + "testing" +) + +func newResp(ip string) *dns.Msg { + resp := new(dns.Msg) + rr, err := dns.NewRR("z.cn. 0 IN A " + ip) + if err != nil { + panic(err) + } + resp.Answer = append(resp.Answer, rr) + return resp +} + +func Test_newCidrRedirector(t *testing.T) { + t.Run("unknown_dst", func(t *testing.T) { + conf := config.RedirectorConf{ + Type: TypeMatchCidr, + Rules: []string{"1.1.1.0/24"}, + RulesFile: "", + DstGroup: "group1", + } + _, err := newCidrRedirector("redir1", conf, nil) + assert.NotNil(t, err) + t.Log(err) + }) + t.Run("strange_file", func(t *testing.T) { + conf := config.RedirectorConf{ + Type: TypeMatchCidr, + Rules: []string{"1.1.1.0/24"}, + RulesFile: "testdata/not_exists.txt", + DstGroup: "group1", + } + groups := map[string]outbound.IGroup{"group1": mock.Group{}} + _, err := newCidrRedirector("redir1", conf, groups) + assert.NotNil(t, err) + t.Log(err) + + conf.RulesFile = "testdata/strange_cidr.txt" + _, err = newCidrRedirector("redir1", conf, groups) + assert.NotNil(t, err) + t.Log(err) + }) + t.Run("match", func(t *testing.T) { + conf := config.RedirectorConf{ + Type: TypeMatchCidr, + Rules: []string{"1.1.1.0/24"}, + RulesFile: "testdata/normal_cidr.txt", + DstGroup: "group1", + } + groups := map[string]outbound.IGroup{"group1": mock.Group{}} + redir, err := newCidrRedirector("redir1", conf, groups) + t.Logf("%s", redir) + assert.Nil(t, err) + + assert.NotNil(t, redir.Redirect(nil, newResp("1.1.1.1"))) + assert.NotNil(t, redir.Redirect(nil, newResp("1.1.2.1"))) + assert.Nil(t, redir.Redirect(nil, newResp("1.1.3.1"))) + }) + t.Run("mismatch", func(t *testing.T) { + conf := config.RedirectorConf{ + Type: TypeMisMatchCidr, + Rules: []string{"1.1.1.0/24"}, + RulesFile: "testdata/normal_cidr.txt", + DstGroup: "group1", + } + groups := map[string]outbound.IGroup{"group1": mock.Group{}} + redir, err := newCidrRedirector("redir1", conf, groups) + assert.Nil(t, err) + + assert.Nil(t, redir.Redirect(nil, newResp("1.1.1.1"))) + assert.Nil(t, redir.Redirect(nil, newResp("1.1.2.1"))) + assert.NotNil(t, redir.Redirect(nil, newResp("1.1.3.1"))) + }) +} + +func TestNewRedirector(t *testing.T) { + t.Run("unknown_type", func(t *testing.T) { + conf := config.Conf{ + Redirectors: map[string]config.RedirectorConf{ + "redir1": {Type: ""}, + }, + } + group1 := mock.Group{} + groups := map[string]outbound.IGroup{"group1": group1} + _, err := NewRedirector(conf, groups) + assert.NotNil(t, err) + t.Logf("%s", err) + }) + t.Run("unknown_redir", func(t *testing.T) { + conf := config.Conf{ + Groups: map[string]config.Group{ + "g1": {Redirector: "redir1"}, + }, + Redirectors: map[string]config.RedirectorConf{ + "redir2": {Type: TypeMatchCidr, Rules: []string{"1.1.1.0/24"}, DstGroup: "g1"}, + }, + } + groups := map[string]outbound.IGroup{"g1": mock.Group{}} + _, err := NewRedirector(conf, groups) + assert.NotNil(t, err) + t.Logf("%s", err) + }) + t.Run("redirect_cycle", func(t *testing.T) { + conf := config.Conf{ + Groups: map[string]config.Group{ + "g1": {Redirector: "redir1"}, + }, + Redirectors: map[string]config.RedirectorConf{ + "redir1": {Type: TypeMatchCidr, Rules: []string{"1.1.1.0/24"}, DstGroup: "g1"}, + }, + } + g1 := mock.Group{ + MockName: func() string { return "g1" }, + MockString: func() string { return "group_g1" }, + } + groups := map[string]outbound.IGroup{"g1": g1} + redir, err := NewRedirector(conf, groups) + assert.Nil(t, err) + newGroup := redir(g1, nil, newResp("1.1.1.1")) + assert.Nil(t, newGroup) + }) + t.Run("redirect_success", func(t *testing.T) { + conf := config.Conf{ + Groups: map[string]config.Group{ + "g1": {Redirector: "redir1"}, + }, + Redirectors: map[string]config.RedirectorConf{ + "redir1": {Type: TypeMatchCidr, Rules: []string{"1.1.1.0/24"}, DstGroup: "g2"}, + }, + } + g1 := mock.Group{ + MockName: func() string { return "g1" }, + MockString: func() string { return "group_g1" }, + } + g2 := mock.Group{ + MockName: func() string { return "g2" }, + MockString: func() string { return "group_g2" }, + } + groups := map[string]outbound.IGroup{"g1": g1, "g2": g2} + redir, err := NewRedirector(conf, groups) + assert.Nil(t, err) + assert.NotNil(t, redir) + newGroup := redir(g1, nil, newResp("1.1.1.1")) + assert.NotNil(t, newGroup) + assert.Equal(t, "g2", newGroup.Name()) + }) + t.Run("redirect_empty", func(t *testing.T) { + conf := config.Conf{ + Groups: map[string]config.Group{ + "g1": {}, + }, + Redirectors: map[string]config.RedirectorConf{ + "redir1": {Type: TypeMatchCidr, Rules: []string{"1.1.1.0/24"}, DstGroup: "g1"}, + }, + } + g1 := mock.Group{ + MockName: func() string { return "g1" }, + MockString: func() string { return "group_g1" }, + } + g2 := mock.Group{ + MockName: func() string { return "g2" }, + MockString: func() string { return "group_g2" }, + } + groups := map[string]outbound.IGroup{"g1": g1, "g2": g2} + redir, err := NewRedirector(conf, groups) + assert.Nil(t, err) + assert.NotNil(t, redir) + newGroup := redir(g1, nil, newResp("1.1.1.1")) + assert.Nil(t, newGroup) + }) +} diff --git a/redirector/testdata/normal_cidr.txt b/redirector/testdata/normal_cidr.txt new file mode 100644 index 0000000..2a0c654 --- /dev/null +++ b/redirector/testdata/normal_cidr.txt @@ -0,0 +1,3 @@ + 1.1.2.0/24 + # 1.1.3.0/24 +// 1.1.4.0/24 \ No newline at end of file diff --git a/redirector/testdata/strange_cidr.txt b/redirector/testdata/strange_cidr.txt new file mode 100644 index 0000000..b6fc4c6 --- /dev/null +++ b/redirector/testdata/strange_cidr.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/ts-dns-full.toml b/ts-dns-full.toml index e153f69..1123b32 100644 --- a/ts-dns-full.toml +++ b/ts-dns-full.toml @@ -1,10 +1,7 @@ # Telescope DNS Configure File # https://github.com/wolf-joe/ts-dns -listen = ":53/udp" # 监听端口,支持指定tcp/udp,不指定时默认同时监听tcp&udp -gfwlist = "gfwlist.txt" # gfwlist文件路径,release包中已预下载。官方地址:https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt -gfwlist_b64 = true # 是否使用base64解码gfwlist文件,默认为true -cnip = "cnip.txt" # 中国ip网段列表,用于辅助域名分组 +listen = ":53/udp" # 监听地址,支持tcp/udp后缀,无后缀则同时监听tcp&udp。推荐使用命令行参数代替 disable_qtypes = ["AAAA", "HTTPS"] # 屏蔽IPv6/HTTPS查询 hosts_files = ["/etc/hosts"] # hosts文件路径,支持多hosts @@ -13,30 +10,30 @@ hosts_files = ["/etc/hosts"] # hosts文件路径,支持多hosts "*.example.com" = "8.8.8.8" # 通配符Hosts "cloudflare-dns.com" = "1.0.0.1" # 防止下文提到的DoH回环解析 -[query_log] -file = "/dev/null" # dns请求日志文件,值为/dev/null时不记录,值为空时记录到stdout -ignore_qtypes = ["DNSKEY", "NS"] # 不记录指定类型的dns请求,默认为空 -ignore_cache = true # 不记录命中缓存的dns请求,默认为false -ignore_hosts = true # 不记录命中hosts的dns请求,默认为false - [cache] # dns缓存配置 -size = 4096 # 缓存大小,为负数时禁用缓存 +size = 4096 # 缓存大小,为非正数时禁用缓存 min_ttl = 60 # 最小ttl,单位为秒 max_ttl = 86400 # 最大ttl,单位为秒 [groups] # 对域名进行分组 - [groups.clean] # 必选分组,默认域名所在分组 + [groups.clean] + rules = ["qq.com", ".baidu.com", "*.taobao.com"] # "qq.com"规则可匹配"test.qq.com"、"qq.com"两种域名,".qq.com"和"*.qq.com"规则无法匹配"qq.com" + rules_file = "rules.txt" # 规则文件,每行一个规则 + fallback = true # 设置为兜底域名组 + ecs = "1.2.4.0/24" # edns-client-subnet信息,配置后转发DNS请求时默认附带(已有ecs时不覆盖),暂不支持doh no_cookie = false # 禁用edns cookie,默认false,dnspod(119.29.29.29)等特殊服务器需要设置为true dns = ["223.5.5.5:53", "114.114.114.114/tcp"] # DNS服务器列表,默认使用53端口 concurrent = true # 并发请求dns服务器列表 - rules = ["qq.com", ".baidu.com", "*.taobao.com"] # "qq.com"规则可匹配"test.qq.com"、"qq.com"两种域名,".qq.com"和"*.qq.com"规则无法匹配"qq.com" - rules_file = "rules.txt" # 规则文件,每行一个规则 fastest_v4 = true # 选择ping值最低的ipv4地址作为响应,启用且使用icmp ping时建议以root权限允许本程序 tcp_ping_port = 80 # 当启用fastest_v4时,如该值大于0则使用tcp ping,小于等于0则使用icmp ping - [groups.dirty] # 必选分组,匹配GFWList的域名会归类到该组 + redirector = "oversea_ip2dirty" # 解析后判断是否需要重定向 + + [groups.dirty] + gfwlist_file = "gfwlist.txt" # 匹配到gfwlist规则时使用该组 + socks5 = "127.0.0.1:1080" # 当使用国外53端口dns解析时推荐用socks5代理解析 dns = ["8.8.8.8", "1.1.1.1"] # 如不想用socks5代理解析时推荐使用国外非53端口dns dot = ["1.0.0.1:853@cloudflare-dns.com"] # dns over tls服务器 @@ -51,4 +48,11 @@ max_ttl = 86400 # 最大ttl,单位为秒 # 比如办公网内,内外域名(company.com)用内网dns(10.1.1.1)解析 [groups.work] dns = ["10.1.1.1"] - rules = ["company.com"] \ No newline at end of file + rules = ["company.com"] + +[redirectors] + [redirectors.oversea_ip2dirty] + # 解析后如发现ip地址不匹配cnip,则重定向到dirty组解析 + type = "mismatch_cidr" + rules_file = "cnip.txt" + dst_group = "dirty" \ No newline at end of file diff --git a/ts-dns.toml b/ts-dns.toml index 97a0291..4a4ea8a 100644 --- a/ts-dns.toml +++ b/ts-dns.toml @@ -2,8 +2,6 @@ # https://github.com/wolf-joe/ts-dns listen = ":53" -gfwlist = "gfwlist.txt" -cnip = "cnip.txt" [groups] [groups.clean] @@ -12,3 +10,4 @@ cnip = "cnip.txt" [groups.dirty] dns = ["208.67.222.222:5353", "176.103.130.130:5353"] + gfwlist_file = "gfwlist.txt" diff --git a/core/utils/ctx.go b/utils/ctx.go similarity index 96% rename from core/utils/ctx.go rename to utils/ctx.go index 1dbc228..138139f 100644 --- a/core/utils/ctx.go +++ b/utils/ctx.go @@ -3,7 +3,7 @@ package utils import ( "context" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) type ctxKey string diff --git a/core/utils/ctx_test.go b/utils/ctx_test.go similarity index 86% rename from core/utils/ctx_test.go rename to utils/ctx_test.go index 172ccae..3ec0b14 100644 --- a/core/utils/ctx_test.go +++ b/utils/ctx_test.go @@ -3,7 +3,7 @@ package utils import ( "testing" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) func TestWithFields(t *testing.T) { diff --git a/core/common/dns.go b/utils/dns.go similarity index 88% rename from core/common/dns.go rename to utils/dns.go index cab981b..51d8fcd 100644 --- a/core/common/dns.go +++ b/utils/dns.go @@ -1,4 +1,4 @@ -package common +package utils import ( "fmt" @@ -8,20 +8,6 @@ import ( "github.com/miekg/dns" ) -// ExtractA 提取dns响应中的A记录 -func ExtractA(r *dns.Msg) (records []*dns.A) { - if r == nil { - return - } - for _, answer := range r.Answer { - switch answer.(type) { - case *dns.A: - records = append(records, answer.(*dns.A)) - } - } - return -} - // ParseECS 将字符串(IP/CIDR)转换为EDNS CLIENT SUBNET对象 func ParseECS(s string) (ecs *dns.EDNS0_SUBNET, err error) { if s == "" { @@ -62,9 +48,7 @@ func FormatECS(r *dns.Msg) string { switch extra.(type) { case *dns.OPT: for _, opt := range extra.(*dns.OPT).Option { - switch opt.(type) { - case *dns.EDNS0_SUBNET: - ecs := opt.(*dns.EDNS0_SUBNET) + if ecs, ok := opt.(*dns.EDNS0_SUBNET); ok { return fmt.Sprintf("%s/%d", ecs.Address, ecs.SourceNetmask) } } diff --git a/core/common/dns_test.go b/utils/dns_test.go similarity index 88% rename from core/common/dns_test.go rename to utils/dns_test.go index 0a2eef5..a0267b5 100644 --- a/core/common/dns_test.go +++ b/utils/dns_test.go @@ -1,4 +1,4 @@ -package common +package utils import ( "github.com/miekg/dns" @@ -6,18 +6,6 @@ import ( "testing" ) -func TestExtractA(t *testing.T) { - assert.Empty(t, ExtractA(nil)) - r := &dns.Msg{} - assert.Empty(t, ExtractA(r)) - r.Answer = append(r.Answer, &dns.AAAA{}) - assert.Empty(t, ExtractA(r)) - r.Answer = append(r.Answer, &dns.A{}) - assert.Equal(t, len(ExtractA(r)), 1) - r.Answer = append(r.Answer, &dns.TXT{}) - assert.Equal(t, len(ExtractA(r)), 1) -} - func TestParseECS(t *testing.T) { ecs, err := ParseECS("") assert.Nil(t, ecs) diff --git a/core/utils/logs.go b/utils/logs.go similarity index 98% rename from core/utils/logs.go rename to utils/logs.go index dff97d4..42bd6a9 100644 --- a/core/utils/logs.go +++ b/utils/logs.go @@ -6,7 +6,7 @@ import ( "path/filepath" "runtime" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) func ctxLog(ctx context.Context, level logrus.Level, format string, args ...interface{}) { diff --git a/core/utils/logs_test.go b/utils/logs_test.go similarity index 95% rename from core/utils/logs_test.go rename to utils/logs_test.go index 806b292..fa9a03d 100644 --- a/core/utils/logs_test.go +++ b/utils/logs_test.go @@ -3,7 +3,7 @@ package utils import ( "testing" - "github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" ) func TestCtxLog(t *testing.T) { diff --git a/utils/mock/group.go b/utils/mock/group.go new file mode 100644 index 0000000..eaa98c6 --- /dev/null +++ b/utils/mock/group.go @@ -0,0 +1,25 @@ +package mock + +import ( + "github.com/miekg/dns" +) + +type Group struct { + MockMatch func(msg *dns.Msg) bool + MockIsFallback func() bool + MockHandle func(req *dns.Msg) *dns.Msg + MockPostProcess func(req, resp *dns.Msg) + MockStart func(resolver dns.Handler) + MockStop func() + MockName func() string + MockString func() string +} + +func (m Group) Match(req *dns.Msg) bool { return m.MockMatch(req) } +func (m Group) IsFallback() bool { return m.MockIsFallback() } +func (m Group) Handle(req *dns.Msg) *dns.Msg { return m.MockHandle(req) } +func (m Group) PostProcess(req *dns.Msg, resp *dns.Msg) { m.MockPostProcess(req, resp) } +func (m Group) Start(resolver dns.Handler) { m.MockStart(resolver) } +func (m Group) Stop() { m.MockStop() } +func (m Group) Name() string { return m.MockName() } +func (m Group) String() string { return m.MockString() } diff --git a/core/utils/mock/mocker.go b/utils/mock/mocker.go similarity index 100% rename from core/utils/mock/mocker.go rename to utils/mock/mocker.go diff --git a/core/utils/ping.go b/utils/ping.go similarity index 81% rename from core/utils/ping.go rename to utils/ping.go index 25ea899..d5b715b 100644 --- a/core/utils/ping.go +++ b/utils/ping.go @@ -1,7 +1,6 @@ package utils import ( - "context" "errors" "net" "strconv" @@ -36,15 +35,13 @@ func PingIP(ipAddr string, tcpPort int, timeout time.Duration) error { } // FastestPingIP 向指定IP地址列表同时发起ping,返回ping值最低的IP地址和耗时 -func FastestPingIP(ctx context.Context, ipAddr []string, tcpPort int, timeout time.Duration, +func FastestPingIP(ipAddr []string, tcpPort int, timeout time.Duration, ) (string, int64, error) { pingDone := make(chan string, len(ipAddr)) begin := time.Now() for _, ip := range ipAddr { go func(addr string) { - if err := PingIP(addr, tcpPort, timeout); err != nil { - CtxDebug(ctx, "ping %s:%d error: %s", addr, tcpPort, err) - } else { + if err := PingIP(addr, tcpPort, timeout); err == nil { pingDone <- addr } }(ip) @@ -57,6 +54,6 @@ func FastestPingIP(ctx context.Context, ipAddr []string, tcpPort int, timeout ti if fastestIP == "" { return "", 0, errors.New("timeout") } - cost := time.Now().Sub(begin).Milliseconds() + cost := time.Since(begin).Milliseconds() return fastestIP, cost, nil } diff --git a/core/utils/ping_test.go b/utils/ping_test.go similarity index 84% rename from core/utils/ping_test.go rename to utils/ping_test.go index c9b6578..72c025d 100644 --- a/core/utils/ping_test.go +++ b/utils/ping_test.go @@ -3,15 +3,15 @@ package utils import ( "errors" "fmt" + "github.com/wolf-joe/ts-dns/utils/mock" "net" "testing" "time" - "github.com/Sirupsen/logrus" "github.com/agiledragon/gomonkey" + "github.com/sirupsen/logrus" "github.com/sparrc/go-ping" "github.com/stretchr/testify/assert" - "github.com/wolf-joe/ts-dns/core/utils/mock" ) func TestPingIP(t *testing.T) { @@ -37,7 +37,6 @@ func TestPingIP(t *testing.T) { func TestFastestPingIP(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) - ctx := NewCtx(nil, 0xffff) port, timeout := 80, 100*time.Millisecond mocker := mock.Mocker{} @@ -45,6 +44,7 @@ func TestFastestPingIP(t *testing.T) { target := &net.TCPConn{} mocker.Method(target, "Close", func(*net.TCPConn) error { return nil }) mocker.Func(net.DialTimeout, func(_, addr string, _ time.Duration) (net.Conn, error) { + fmt.Println(addr) if addr == fmt.Sprintf("%s:%d", "1.1.1.1", port) { return &net.TCPConn{}, nil } @@ -52,10 +52,10 @@ func TestFastestPingIP(t *testing.T) { return nil, errors.New("timeout") }) - ip, _, err := FastestPingIP(ctx, []string{"1.1.1.1", "1.1.1.2"}, port, timeout) + ip, _, err := FastestPingIP([]string{"1.1.1.1", "1.1.1.2"}, port, timeout) assert.Nil(t, err) assert.Equal(t, "1.1.1.1", ip) - ip, _, err = FastestPingIP(ctx, []string{"1.1.1.2", "1.1.1.3"}, port, timeout) + _, _, err = FastestPingIP([]string{"1.1.1.2", "1.1.1.3"}, port, timeout) assert.NotNil(t, err) } diff --git a/utils/resp_writer.go b/utils/resp_writer.go new file mode 100644 index 0000000..073d67f --- /dev/null +++ b/utils/resp_writer.go @@ -0,0 +1,40 @@ +package utils + +import ( + "net" + + "github.com/miekg/dns" +) + +type FakeRespWriter struct { + Msg *dns.Msg + Bytes []byte +} + +// NewFakeRespWriter 创建一个FakeRespWriter,用于手动请求dns.Handler时获取DNS响应 +func NewFakeRespWriter() *FakeRespWriter { + return &FakeRespWriter{} +} + +func (w *FakeRespWriter) LocalAddr() net.Addr { + return &net.IPAddr{IP: []byte{127, 0, 0, 1}} +} + +func (w *FakeRespWriter) RemoteAddr() net.Addr { + return &net.IPAddr{IP: []byte{127, 0, 0, 1}} +} + +func (w *FakeRespWriter) WriteMsg(msg *dns.Msg) error { + w.Msg = msg + return nil +} + +func (w *FakeRespWriter) Write(bytes []byte) (int, error) { + w.Bytes = bytes + return len(bytes), nil +} + +func (w *FakeRespWriter) Close() error { return nil } +func (w *FakeRespWriter) TsigStatus() error { return nil } +func (w *FakeRespWriter) TsigTimersOnly(bool) {} +func (w *FakeRespWriter) Hijack() {} diff --git a/core/utils/resp_writer_test.go b/utils/resp_writer_test.go similarity index 100% rename from core/utils/resp_writer_test.go rename to utils/resp_writer_test.go