From 64a97809a654a2080830a7ea2d0dc9433b308a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Thu, 19 Jul 2018 13:51:47 +0300 Subject: [PATCH 1/7] dashboard, p2p: extend meteredConn with peer metering --- dashboard/dashboard.go | 4 ++++ p2p/metrics.go | 48 ++++++++++++++++++++++++++++++++++++------ p2p/rlpx.go | 3 +++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index 3ba92ac73e..a965110688 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -385,6 +385,10 @@ func (db *Dashboard) collectData() { DiskWrite: ChartEntries{diskWrite}, }, }) + + for k, v := range p2p.IngressTrafficMeters { + fmt.Println(k[:6], v.Count()) + } } } } diff --git a/p2p/metrics.go b/p2p/metrics.go index 2d52fd1fd1..9ca5b86b64 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -21,20 +21,26 @@ package p2p import ( "net" + "fmt" "github.com/ethereum/go-ethereum/metrics" ) var ( - ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) - ingressTrafficMeter = metrics.NewRegisteredMeter("p2p/InboundTraffic", nil) - egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) - egressTrafficMeter = metrics.NewRegisteredMeter("p2p/OutboundTraffic", nil) + ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) + ingressTrafficMeter = metrics.NewRegisteredMeter("p2p/InboundTraffic", nil) + egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) + egressTrafficMeter = metrics.NewRegisteredMeter("p2p/OutboundTraffic", nil) + IngressTrafficMeters = make(map[string]metrics.Meter) + EgressTrafficMeters = make(map[string]metrics.Meter) ) // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { - net.Conn // Network connection to wrap with metering + net.Conn // Network connection to wrap with metering + id string + unmarkedIngress uint + unmarkedEgress uint } // newMeteredConn creates a new metered connection, also bumping the ingress or @@ -59,7 +65,12 @@ func newMeteredConn(conn net.Conn, ingress bool) net.Conn { func (c *meteredConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) ingressTrafficMeter.Mark(int64(n)) - return + if rm, ok := IngressTrafficMeters[c.id]; ok { + rm.Mark(int64(n)) + return n, err + } + c.unmarkedIngress += uint(n) + return n, err } // Write delegates a network write to the underlying connection, bumping the @@ -67,5 +78,28 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { func (c *meteredConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) egressTrafficMeter.Mark(int64(n)) - return + if rm, ok := EgressTrafficMeters[c.id]; ok { + rm.Mark(int64(n)) + return n, err + } + c.unmarkedEgress += uint(n) + return n, err +} + +func (c *meteredConn) meterIndividually(id string) { + c.id = id + IngressTrafficMeters[id] = metrics.NewRegisteredMeter(fmt.Sprintf("p2p/InboundTraffic/%s", id), nil) + EgressTrafficMeters[id] = metrics.NewRegisteredMeter(fmt.Sprintf("p2p/OutboundTraffic/%s", id), nil) + IngressTrafficMeters[id].Mark(int64(c.unmarkedIngress)) + EgressTrafficMeters[id].Mark(int64(c.unmarkedEgress)) + c.unmarkedIngress, c.unmarkedEgress = 0, 0 +} + +func (c *meteredConn) Close() error { + delete(IngressTrafficMeters, c.id) + delete(EgressTrafficMeters, c.id) + metrics.Unregister(fmt.Sprintf("p2p/InboundTraffic/%s", c.id)) + metrics.Unregister(fmt.Sprintf("p2p/OutboundTraffic/%s", c.id)) + fmt.Println("Close", c.id) + return c.Conn.Close() } diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 46b6668695..05ab8bac9c 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -586,6 +586,9 @@ func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW { // we use an all-zeroes IV for AES because the key used // for encryption is ephemeral. iv := make([]byte, encc.BlockSize()) + if c, ok := conn.(*meteredConn); ok { + c.meterIndividually(s.RemoteID.String()) + } return &rlpxFrameRW{ conn: conn, enc: cipher.NewCTR(encc, iv), From 272444d11bf026aaf4731571e55edc783d4772e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Mon, 23 Jul 2018 16:50:57 +0300 Subject: [PATCH 2/7] dashboard, p2p: store traffic meters in maps --- dashboard/dashboard.go | 6 +- p2p/metrics.go | 165 ++++++++++++++++++++++++++++++++--------- p2p/rlpx.go | 2 +- 3 files changed, 136 insertions(+), 37 deletions(-) diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index a965110688..4c2b39be43 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -385,10 +385,10 @@ func (db *Dashboard) collectData() { DiskWrite: ChartEntries{diskWrite}, }, }) - - for k, v := range p2p.IngressTrafficMeters { - fmt.Println(k[:6], v.Count()) + for _, id := range p2p.TrafficMeter.GetIDs() { + fmt.Println(id) } + fmt.Println() } } } diff --git a/p2p/metrics.go b/p2p/metrics.go index 9ca5b86b64..00211513b6 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -22,25 +22,136 @@ import ( "net" "fmt" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/syndtr/goleveldb/leveldb/errors" + "sync" + "sync/atomic" ) var ( - ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) - ingressTrafficMeter = metrics.NewRegisteredMeter("p2p/InboundTraffic", nil) - egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) - egressTrafficMeter = metrics.NewRegisteredMeter("p2p/OutboundTraffic", nil) - IngressTrafficMeters = make(map[string]metrics.Meter) - EgressTrafficMeters = make(map[string]metrics.Meter) + ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) + egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) + TrafficMeter = newTrafficMeter() + nextDefaultID uint32 ) +const ( + IngressPrefix = "p2p/InboundTraffic" + EgressPrefix = "p2p/OutboundTraffic" +) + +type trafficMeter struct { + peerIngress map[string]metrics.Meter + peerEgress map[string]metrics.Meter + commonIngress metrics.Meter + commonEgress metrics.Meter + changed map[string]string + lock sync.RWMutex + cLock sync.RWMutex +} + +// Create a new registry. +func newTrafficMeter() *trafficMeter { + return &trafficMeter{ + peerIngress: make(map[string]metrics.Meter), + peerEgress: make(map[string]metrics.Meter), + commonIngress: metrics.NewRegisteredMeter("p2p/InboundTraffic", nil), + commonEgress: metrics.NewRegisteredMeter("p2p/OutboundTraffic", nil), + changed: make(map[string]string), + } +} + +func (tm *trafficMeter) register(id string, ingressMeter, egressMeter metrics.Meter) error { + if ingressMeter == nil && egressMeter == nil { + tm.lock.Lock() + tm.peerIngress[id] = metrics.NewRegisteredMeter(fmt.Sprintf("%s/%s", IngressPrefix, id), nil) + tm.peerEgress[id] = metrics.NewRegisteredMeter(fmt.Sprintf("%s/%s", EgressPrefix, id), nil) + tm.lock.Unlock() + return nil + } + if ingressMeter == nil || egressMeter == nil { + return errors.New("Meter is not set") + } + if err := metrics.Register(fmt.Sprintf("%s/%s", IngressPrefix, id), ingressMeter); err != nil { + return err + } + if err := metrics.Register(fmt.Sprintf("%s/%s", EgressPrefix, id), egressMeter); err != nil { + metrics.Unregister(fmt.Sprintf("%s/%s", IngressPrefix, id)) + return err + } + tm.lock.Lock() + tm.peerIngress[id] = ingressMeter + tm.peerEgress[id] = egressMeter + tm.lock.Unlock() + return nil +} + +func (tm *trafficMeter) delete(id string) { + tm.lock.Lock() + delete(tm.peerIngress, id) + delete(tm.peerEgress, id) + tm.lock.Unlock() +} +func (tm *trafficMeter) unregister(old, new string) { + metrics.Unregister(fmt.Sprintf("%s/%s", IngressPrefix, old)) + metrics.Unregister(fmt.Sprintf("%s/%s", EgressPrefix, old)) + tm.delete(old) + tm.cLock.Lock() + tm.changed[old] = new + tm.cLock.Unlock() +} + +func (tm *trafficMeter) changeID(old, new string) error { + tm.lock.RLock() + irm, oki := tm.peerIngress[old] + erm, oke := tm.peerEgress[old] + tm.lock.RUnlock() + if !oki || !oke { + return errors.New(fmt.Sprintf("No meter with id %s", old)) + } + if err := tm.register(new, irm, erm); err != nil { + return err + } + tm.unregister(old, new) + return nil +} + +func (tm *trafficMeter) markIngress(id string, n int64) { + tm.commonIngress.Mark(n) + tm.lock.RLock() + rm, ok := tm.peerIngress[id] + tm.lock.RUnlock() + if ok { + rm.Mark(n) + } +} + +func (tm *trafficMeter) markEgress(id string, n int64) { + tm.commonEgress.Mark(n) + tm.lock.RLock() + rm, ok := tm.peerEgress[id] + tm.lock.RUnlock() + if ok { + rm.Mark(n) + } +} + +func (tm *trafficMeter) GetIDs() []string { + ids := make([]string, 0, len(tm.peerIngress)) + tm.lock.RLock() + for id := range tm.peerIngress { + ids = append(ids, id) + } + tm.lock.RUnlock() + return ids +} + // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { - net.Conn // Network connection to wrap with metering - id string - unmarkedIngress uint - unmarkedEgress uint + net.Conn // Network connection to wrap with metering + id string } // newMeteredConn creates a new metered connection, also bumping the ingress or @@ -57,19 +168,16 @@ func newMeteredConn(conn net.Conn, ingress bool) net.Conn { } else { egressConnectMeter.Mark(1) } - return &meteredConn{Conn: conn} + id := fmt.Sprintf("unidentified_%d", atomic.AddUint32(&nextDefaultID, 1)) + TrafficMeter.register(id, nil, nil) + return &meteredConn{Conn: conn, id: id} } // Read delegates a network read to the underlying connection, bumping the ingress // traffic meter along the way. func (c *meteredConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) - ingressTrafficMeter.Mark(int64(n)) - if rm, ok := IngressTrafficMeters[c.id]; ok { - rm.Mark(int64(n)) - return n, err - } - c.unmarkedIngress += uint(n) + TrafficMeter.markIngress(c.id, int64(n)) return n, err } @@ -77,29 +185,20 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { // egress traffic meter along the way. func (c *meteredConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) - egressTrafficMeter.Mark(int64(n)) - if rm, ok := EgressTrafficMeters[c.id]; ok { - rm.Mark(int64(n)) - return n, err - } - c.unmarkedEgress += uint(n) + TrafficMeter.markEgress(c.id, int64(n)) return n, err } -func (c *meteredConn) meterIndividually(id string) { +func (c *meteredConn) setPeerID(id string) { + if err := TrafficMeter.changeID(c.id, id); err != nil { + log.Warn("Failed to set peer id", "id", fmt.Sprintf("%s...%s", id[:6], id[len(id)-6:]), "err", err) + return + } c.id = id - IngressTrafficMeters[id] = metrics.NewRegisteredMeter(fmt.Sprintf("p2p/InboundTraffic/%s", id), nil) - EgressTrafficMeters[id] = metrics.NewRegisteredMeter(fmt.Sprintf("p2p/OutboundTraffic/%s", id), nil) - IngressTrafficMeters[id].Mark(int64(c.unmarkedIngress)) - EgressTrafficMeters[id].Mark(int64(c.unmarkedEgress)) - c.unmarkedIngress, c.unmarkedEgress = 0, 0 } func (c *meteredConn) Close() error { - delete(IngressTrafficMeters, c.id) - delete(EgressTrafficMeters, c.id) - metrics.Unregister(fmt.Sprintf("p2p/InboundTraffic/%s", c.id)) - metrics.Unregister(fmt.Sprintf("p2p/OutboundTraffic/%s", c.id)) + TrafficMeter.unregister(c.id, "") fmt.Println("Close", c.id) return c.Conn.Close() } diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 05ab8bac9c..393676b815 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -587,7 +587,7 @@ func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW { // for encryption is ephemeral. iv := make([]byte, encc.BlockSize()) if c, ok := conn.(*meteredConn); ok { - c.meterIndividually(s.RemoteID.String()) + c.setPeerID(s.RemoteID.String()) } return &rlpxFrameRW{ conn: conn, From 0125c61e781216241dda71d57aa788c01a11f03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Tue, 24 Jul 2018 15:13:56 +0300 Subject: [PATCH 3/7] dashboard, metrics, p2p: log peers on client side --- dashboard/assets/components/Dashboard.jsx | 13 +- dashboard/assets/components/Main.jsx | 8 + dashboard/assets/components/Network.jsx | 60 +++++++ dashboard/dashboard.go | 26 ++- dashboard/message.go | 9 +- metrics/registry.go | 2 +- p2p/metrics.go | 187 ++++++++++++---------- 7 files changed, 210 insertions(+), 95 deletions(-) create mode 100644 dashboard/assets/components/Network.jsx diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index 63c2186ada..4b949478e3 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -88,7 +88,10 @@ const defaultContent: () => Content = () => ({ home: {}, chain: {}, txpool: {}, - network: {}, + network: { + peers: [], + changed: [], + }, system: { activeMemory: [], virtualMemory: [], @@ -119,7 +122,10 @@ const updaters = { home: null, chain: null, txpool: null, - network: null, + network: { + peers: appender(200), + changed: appender(200), + }, system: { activeMemory: appender(200), virtualMemory: appender(200), @@ -198,6 +204,9 @@ class Dashboard extends Component { return; } this.update(msg); + if (msg.network) { + console.log(msg.network); + } }; server.onclose = () => { this.setState({server: null}); diff --git a/dashboard/assets/components/Main.jsx b/dashboard/assets/components/Main.jsx index 0018aaf75f..3102274a73 100644 --- a/dashboard/assets/components/Main.jsx +++ b/dashboard/assets/components/Main.jsx @@ -89,9 +89,17 @@ class Main extends Component { let children = null; switch (active) { case MENU.get('home').id: + children =
Work in progress.
; + break; case MENU.get('chain').id: + children =
Work in progress.
; + break; case MENU.get('txpool').id: + children =
Work in progress.
; + break; case MENU.get('network').id: + children =
Work in progress.
; + break; case MENU.get('system').id: children =
Work in progress.
; break; diff --git a/dashboard/assets/components/Network.jsx b/dashboard/assets/components/Network.jsx new file mode 100644 index 0000000000..41a73c7c03 --- /dev/null +++ b/dashboard/assets/components/Network.jsx @@ -0,0 +1,60 @@ +// @flow + +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +import React, {Component} from 'react'; + +import type {Content, Network as NetworkType} from '../types/content'; + + +// inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. +// limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. +export const inserter = (update: NetworkType, prev: LogsType) => prev; + +// styles contains the constant styles of the component. +const styles = {}; + +export type Props = { + container: Object, + content: Content, + shouldUpdate: Object, +}; + +type State = { + peers: List, +}; + +// Network renders the network page. +class Network extends Component { + constructor(props: Props) { + super(props); + this.content = React.createRef(); + this.state = { + peers: [], + }; + } + + render() { + return ( +
+ Alma +
+ ); + } +} + +export default Network; diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index 4c2b39be43..c7b6c9433b 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -42,6 +42,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/mohae/deepcopy" "golang.org/x/net/websocket" + "encoding/json" ) const ( @@ -261,8 +262,8 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) { // meterCollector returns a function, which retrieves a specific meter. func meterCollector(name string) func() int64 { - if metric := metrics.DefaultRegistry.Get(name); metric != nil { - m := metric.(metrics.Meter) + if meter := metrics.DefaultRegistry.Get(name); meter != nil { + m := meter.(metrics.Meter) return func() int64 { return m.Count() } @@ -373,6 +374,22 @@ func (db *Dashboard) collectData() { sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite) db.lock.Unlock() + peerIDs := p2p.TrafficMeterCollector.GetIDs() + nm := &NetworkMessage{ + Changed: p2p.TrafficMeterCollector.GetAndClearChanged(), + } + for _, id := range peerIDs { + nm.Peers = append(nm.Peers, &Peer{ + ID: id[len(id)-6:], + Ingress: p2p.PeerIngressRegistry.Get(id).(metrics.Meter).Count(), + Egress: p2p.PeerEgressRegistry.Get(id).(metrics.Meter).Count(), + }) + fmt.Println(metrics.DefaultRegistry.Get(fmt.Sprintf("%s/%s", p2p.IngressPrefix, id)).(metrics.Meter).Count()) + fmt.Println(metrics.DefaultRegistry.Get(fmt.Sprintf("%s/%s", p2p.EgressPrefix, id)).(metrics.Meter).Count()) + } + s, _ := json.Marshal(nm) + fmt.Println(string(s)) + db.sendToAll(&Message{ System: &SystemMessage{ ActiveMemory: ChartEntries{activeMemory}, @@ -384,11 +401,8 @@ func (db *Dashboard) collectData() { DiskRead: ChartEntries{diskRead}, DiskWrite: ChartEntries{diskWrite}, }, + Network: nm, }) - for _, id := range p2p.TrafficMeter.GetIDs() { - fmt.Println(id) - } - fmt.Println() } } } diff --git a/dashboard/message.go b/dashboard/message.go index 46856b9e6e..99050d854d 100644 --- a/dashboard/message.go +++ b/dashboard/message.go @@ -56,7 +56,14 @@ type TxPoolMessage struct { } type NetworkMessage struct { - /* TODO (kurkomisi) */ + Peers []*Peer `json:"peers,omitempty"` + Changed []string `json:"changed,omitempty"` +} + +type Peer struct { + ID string `json:"id,omitempty"` + Ingress int64 `json:"ingress,omitempty"` + Egress int64 `json:"egress,omitempty"` } type SystemMessage struct { diff --git a/metrics/registry.go b/metrics/registry.go index cc34c9dfd2..baf01c554f 100644 --- a/metrics/registry.go +++ b/metrics/registry.go @@ -311,7 +311,7 @@ func (r *PrefixedRegistry) UnregisterAll() { r.underlying.UnregisterAll() } -var DefaultRegistry Registry = NewRegistry() +var DefaultRegistry = NewRegistry() // Call the given function for each registered metric. func Each(f func(string, interface{})) { diff --git a/p2p/metrics.go b/p2p/metrics.go index 00211513b6..dfabad0873 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -29,124 +29,142 @@ import ( "sync/atomic" ) -var ( - ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) - egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) - TrafficMeter = newTrafficMeter() - nextDefaultID uint32 -) - const ( IngressPrefix = "p2p/InboundTraffic" EgressPrefix = "p2p/OutboundTraffic" ) +var ( + ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) + egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) + + PeerIngressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, IngressPrefix+"/") + PeerEgressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, EgressPrefix+"/") + TrafficMeterCollector = newTrafficMeterCollector() + + nextDefaultID uint32 +) + type trafficMeter struct { - peerIngress map[string]metrics.Meter - peerEgress map[string]metrics.Meter - commonIngress metrics.Meter - commonEgress metrics.Meter - changed map[string]string - lock sync.RWMutex - cLock sync.RWMutex + ingress metrics.Meter + egress metrics.Meter } -// Create a new registry. -func newTrafficMeter() *trafficMeter { - return &trafficMeter{ - peerIngress: make(map[string]metrics.Meter), - peerEgress: make(map[string]metrics.Meter), - commonIngress: metrics.NewRegisteredMeter("p2p/InboundTraffic", nil), - commonEgress: metrics.NewRegisteredMeter("p2p/OutboundTraffic", nil), - changed: make(map[string]string), +type trafficMeterCollector struct { + common *trafficMeter + peers map[string]*trafficMeter + changed []string + + lock sync.RWMutex + cLock sync.Mutex +} + +func newTrafficMeterCollector() *trafficMeterCollector { + return &trafficMeterCollector{ + common: &trafficMeter{ + ingress: metrics.NewRegisteredMeter(IngressPrefix, nil), + egress: metrics.NewRegisteredMeter(EgressPrefix, nil), + }, + peers: make(map[string]*trafficMeter), + changed: make([]string, 0, 128), } } -func (tm *trafficMeter) register(id string, ingressMeter, egressMeter metrics.Meter) error { - if ingressMeter == nil && egressMeter == nil { - tm.lock.Lock() - tm.peerIngress[id] = metrics.NewRegisteredMeter(fmt.Sprintf("%s/%s", IngressPrefix, id), nil) - tm.peerEgress[id] = metrics.NewRegisteredMeter(fmt.Sprintf("%s/%s", EgressPrefix, id), nil) - tm.lock.Unlock() +func (tmc *trafficMeterCollector) register(id string, tm *trafficMeter) error { + if tm == nil { + peer := &trafficMeter{ + ingress: metrics.NewRegisteredMeter(id, PeerIngressRegistry), + egress: metrics.NewRegisteredMeter(id, PeerEgressRegistry), + } + tmc.lock.Lock() + tmc.peers[id] = peer + tmc.lock.Unlock() return nil } - if ingressMeter == nil || egressMeter == nil { - return errors.New("Meter is not set") + if tm.ingress == nil || tm.egress == nil { + return errors.New("Meter is not set correctly") } - if err := metrics.Register(fmt.Sprintf("%s/%s", IngressPrefix, id), ingressMeter); err != nil { + if err := PeerIngressRegistry.Register(id, tm.ingress); err != nil { return err } - if err := metrics.Register(fmt.Sprintf("%s/%s", EgressPrefix, id), egressMeter); err != nil { - metrics.Unregister(fmt.Sprintf("%s/%s", IngressPrefix, id)) + if err := PeerEgressRegistry.Register(id, tm.egress); err != nil { + PeerIngressRegistry.Unregister(id) return err } - tm.lock.Lock() - tm.peerIngress[id] = ingressMeter - tm.peerEgress[id] = egressMeter - tm.lock.Unlock() + tmc.lock.Lock() + tmc.peers[id] = tm + tmc.lock.Unlock() return nil } -func (tm *trafficMeter) delete(id string) { - tm.lock.Lock() - delete(tm.peerIngress, id) - delete(tm.peerEgress, id) - tm.lock.Unlock() -} -func (tm *trafficMeter) unregister(old, new string) { - metrics.Unregister(fmt.Sprintf("%s/%s", IngressPrefix, old)) - metrics.Unregister(fmt.Sprintf("%s/%s", EgressPrefix, old)) - tm.delete(old) - tm.cLock.Lock() - tm.changed[old] = new - tm.cLock.Unlock() +func (tmc *trafficMeterCollector) unregister(old, new string) { + PeerIngressRegistry.Unregister(old) + PeerEgressRegistry.Unregister(old) + + tmc.lock.Lock() + delete(tmc.peers, old) + tmc.lock.Unlock() + + tmc.cLock.Lock() + tmc.changed = append(tmc.changed, old, new) + tmc.cLock.Unlock() } -func (tm *trafficMeter) changeID(old, new string) error { - tm.lock.RLock() - irm, oki := tm.peerIngress[old] - erm, oke := tm.peerEgress[old] - tm.lock.RUnlock() - if !oki || !oke { +func (tmc *trafficMeterCollector) changeID(old, new string) error { + tmc.lock.RLock() + peer, ok := tmc.peers[old] + tmc.lock.RUnlock() + if !ok { return errors.New(fmt.Sprintf("No meter with id %s", old)) } - if err := tm.register(new, irm, erm); err != nil { + if err := tmc.register(new, peer); err != nil { return err } - tm.unregister(old, new) + tmc.unregister(old, new) return nil } -func (tm *trafficMeter) markIngress(id string, n int64) { - tm.commonIngress.Mark(n) - tm.lock.RLock() - rm, ok := tm.peerIngress[id] - tm.lock.RUnlock() +func (tmc *trafficMeterCollector) markIngress(id string, n int64) { + tmc.common.ingress.Mark(n) + tmc.lock.RLock() + peer, ok := tmc.peers[id] + tmc.lock.RUnlock() if ok { - rm.Mark(n) + peer.ingress.Mark(n) } } -func (tm *trafficMeter) markEgress(id string, n int64) { - tm.commonEgress.Mark(n) - tm.lock.RLock() - rm, ok := tm.peerEgress[id] - tm.lock.RUnlock() +func (tmc *trafficMeterCollector) markEgress(id string, n int64) { + tmc.common.egress.Mark(n) + tmc.lock.RLock() + peer, ok := tmc.peers[id] + tmc.lock.RUnlock() if ok { - rm.Mark(n) + peer.egress.Mark(n) } } -func (tm *trafficMeter) GetIDs() []string { - ids := make([]string, 0, len(tm.peerIngress)) - tm.lock.RLock() - for id := range tm.peerIngress { +func (tmc *trafficMeterCollector) GetIDs() []string { + tmc.lock.RLock() + ids := make([]string, 0, len(tmc.peers)) + for id := range tmc.peers { ids = append(ids, id) } - tm.lock.RUnlock() + tmc.lock.RUnlock() return ids } +func (tmc *trafficMeterCollector) GetAndClearChanged() []string { + tmc.cLock.Lock() + defer tmc.cLock.Unlock() + + changed := make([]string, len(tmc.changed)) + copy(changed, tmc.changed) + tmc.changed = tmc.changed[:0] + + return changed +} + // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { @@ -169,7 +187,7 @@ func newMeteredConn(conn net.Conn, ingress bool) net.Conn { egressConnectMeter.Mark(1) } id := fmt.Sprintf("unidentified_%d", atomic.AddUint32(&nextDefaultID, 1)) - TrafficMeter.register(id, nil, nil) + TrafficMeterCollector.register(id, nil) return &meteredConn{Conn: conn, id: id} } @@ -177,7 +195,7 @@ func newMeteredConn(conn net.Conn, ingress bool) net.Conn { // traffic meter along the way. func (c *meteredConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) - TrafficMeter.markIngress(c.id, int64(n)) + TrafficMeterCollector.markIngress(c.id, int64(n)) return n, err } @@ -185,20 +203,19 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { // egress traffic meter along the way. func (c *meteredConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) - TrafficMeter.markEgress(c.id, int64(n)) + TrafficMeterCollector.markEgress(c.id, int64(n)) return n, err } +func (c *meteredConn) Close() error { + TrafficMeterCollector.unregister(c.id, "") + return c.Conn.Close() +} + func (c *meteredConn) setPeerID(id string) { - if err := TrafficMeter.changeID(c.id, id); err != nil { + if err := TrafficMeterCollector.changeID(c.id, id); err != nil { log.Warn("Failed to set peer id", "id", fmt.Sprintf("%s...%s", id[:6], id[len(id)-6:]), "err", err) return } c.id = id } - -func (c *meteredConn) Close() error { - TrafficMeter.unregister(c.id, "") - fmt.Println("Close", c.id) - return c.Conn.Close() -} From 82ae16192a28328d8519f83d4f670ffd1bbc9c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Fri, 3 Aug 2018 13:03:25 +0300 Subject: [PATCH 4/7] dashboard, p2p: send peer metrics to the dashboard --- dashboard/assets/components/Dashboard.jsx | 16 +- dashboard/assets/components/Logs.jsx | 6 +- dashboard/assets/components/Main.jsx | 3 +- dashboard/assets/components/Network.jsx | 65 +++-- dashboard/assets/types/content.jsx | 24 +- dashboard/dashboard.go | 120 +++++++-- dashboard/geoip.go | 79 ++++++ dashboard/message.go | 30 ++- p2p/dial.go | 2 +- p2p/metrics.go | 292 ++++++++++++---------- p2p/rlpx.go | 3 - p2p/server.go | 13 +- 12 files changed, 447 insertions(+), 206 deletions(-) create mode 100644 dashboard/geoip.go diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index 4b949478e3..c11c40c195 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -24,7 +24,8 @@ import Header from './Header'; import Body from './Body'; import {MENU} from '../common'; import type {Content} from '../types/content'; -import {inserter as logInserter} from './Logs'; +import {inserter as logInserter, SAME} from './Logs'; +import {inserter as peerInserter} from './Network'; // deepUpdate updates an object corresponding to the given update data, which has // the shape of the same structure as the original object. updater also has the same @@ -89,8 +90,7 @@ const defaultContent: () => Content = () => ({ chain: {}, txpool: {}, network: { - peers: [], - changed: [], + peers: {}, }, system: { activeMemory: [], @@ -106,8 +106,8 @@ const defaultContent: () => Content = () => ({ chunks: [], endTop: false, endBottom: true, - topChanged: 0, - bottomChanged: 0, + topChanged: SAME, + bottomChanged: SAME, }, }); @@ -123,8 +123,7 @@ const updaters = { chain: null, txpool: null, network: { - peers: appender(200), - changed: appender(200), + peers: peerInserter, }, system: { activeMemory: appender(200), @@ -204,9 +203,6 @@ class Dashboard extends Component { return; } this.update(msg); - if (msg.network) { - console.log(msg.network); - } }; server.onclose = () => { this.setState({server: null}); diff --git a/dashboard/assets/components/Logs.jsx b/dashboard/assets/components/Logs.jsx index 57d6328d35..ce0e4396bc 100644 --- a/dashboard/assets/components/Logs.jsx +++ b/dashboard/assets/components/Logs.jsx @@ -104,9 +104,9 @@ const createChunk = (records: Array) => { // ADDED, SAME and REMOVED are used to track the change of the log chunk array. // The scroll position is set using these values. -const ADDED = 1; -const SAME = 0; -const REMOVED = -1; +export const ADDED = 1; +export const SAME = 0; +export const REMOVED = -1; // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. diff --git a/dashboard/assets/components/Main.jsx b/dashboard/assets/components/Main.jsx index 3102274a73..77c569fa69 100644 --- a/dashboard/assets/components/Main.jsx +++ b/dashboard/assets/components/Main.jsx @@ -21,6 +21,7 @@ import React, {Component} from 'react'; import withStyles from 'material-ui/styles/withStyles'; import {MENU} from '../common'; +import Network from './Network'; import Logs from './Logs'; import Footer from './Footer'; import type {Content} from '../types/content'; @@ -98,7 +99,7 @@ class Main extends Component { children =
Work in progress.
; break; case MENU.get('network').id: - children =
Work in progress.
; + children = ; break; case MENU.get('system').id: children =
Work in progress.
; diff --git a/dashboard/assets/components/Network.jsx b/dashboard/assets/components/Network.jsx index 41a73c7c03..19773a8a34 100644 --- a/dashboard/assets/components/Network.jsx +++ b/dashboard/assets/components/Network.jsx @@ -18,41 +18,68 @@ import React, {Component} from 'react'; -import type {Content, Network as NetworkType} from '../types/content'; - +import Table, {TableBody, TableHeader, TableHeaderColumn, TableRow, TableCell} from 'material-ui/Table'; +import type {Network as NetworkType, Peer} from '../types/content'; // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. -export const inserter = (update: NetworkType, prev: LogsType) => prev; +export const inserter = (update: {[number]: Peer}, prev: {[number]: Peer}) => { + Object.keys(update).forEach((k) => { + if (!prev[k]) { + prev[k] = update[k]; + return; + } + const u: Peer = update[k]; + const p: Peer = prev[k]; + if (u.id) { + p.id = u.id; + } + if (u.ip) { + p.ip = u.ip; + } + if (u.lifecycle) { + if (u.lifecycle.handshake) { + p.lifecycle.handshake = u.lifecycle.handshake; + } + if (u.lifecycle.disconnected) { + p.lifecycle.disconnected = u.lifecycle.disconnected; + } + } + p.ingress = [...p.ingress, ...u.ingress].slice(-200); + p.egress = [...p.egress, ...u.egress].slice(-200); + prev[k] = p; + }); + return prev; +}; // styles contains the constant styles of the component. const styles = {}; export type Props = { container: Object, - content: Content, + content: NetworkType, shouldUpdate: Object, }; -type State = { - peers: List, -}; - // Network renders the network page. class Network extends Component { - constructor(props: Props) { - super(props); - this.content = React.createRef(); - this.state = { - peers: [], - }; - } - render() { return ( -
- Alma -
+ + + {Object.entries(this.props.content.peers).map(([k, v]) => ( + + {k} + {v.id ? v.id.substring(0, 6) : ''} + {v.ip} + {v.ingress.value} + {v.egress.value} + {JSON.stringify(v.location)} + {JSON.stringify(v.lifecycle)} + + ))} + +
); } } diff --git a/dashboard/assets/types/content.jsx b/dashboard/assets/types/content.jsx index ff82c3572d..691db4987a 100644 --- a/dashboard/assets/types/content.jsx +++ b/dashboard/assets/types/content.jsx @@ -51,7 +51,29 @@ export type TxPool = { }; export type Network = { - /* TODO (kurkomisi) */ + peers: {[number]: Peer}, +}; + +export type Peer = { + id: string, + ip: string, + location: PeerLocation, + lifecycle: PeerLifecycle, + ingress: ChartEntries, + egress: ChartEntries, +}; + +export type PeerLocation = { + country: string, + city: string, + latitude: number, + longitude: number, +}; + +export type PeerLifecycle = { + connected: Date, + handshake: Date, + disconnected: Date, }; export type System = { diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index c7b6c9433b..f92a4c6d64 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -42,7 +42,6 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/mohae/deepcopy" "golang.org/x/net/websocket" - "encoding/json" ) const ( @@ -54,6 +53,10 @@ const ( systemCPUSampleLimit = 200 // Maximum number of system cpu data samples diskReadSampleLimit = 200 // Maximum number of disk read data samples diskWriteSampleLimit = 200 // Maximum number of disk write data samples + + storedDisconnectedPeerLimit = p2p.MeteredPeerLimit - p2p.DefaultMaxPendingPeers + peerIngressSampleLimit = 200 + peerEgressSampleLimit = 200 ) var nextID uint32 // Next connection id @@ -67,7 +70,9 @@ type Dashboard struct { history *Message lock sync.RWMutex // Lock protecting the dashboard's internals - logdir string + geodb *GeoDB + disconnectedPeerIDs chan uint + logdir string quit chan chan error // Channel used for graceful exit wg sync.WaitGroup @@ -106,8 +111,12 @@ func New(config *Config, commit string, logdir string) *Dashboard { DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), }, + Network: &NetworkMessage{ + Peers: make(map[uint]*Peer), + }, }, - logdir: logdir, + disconnectedPeerIDs: make(chan uint, storedDisconnectedPeerLimit), + logdir: logdir, } } @@ -133,6 +142,13 @@ func (db *Dashboard) APIs() []rpc.API { return nil } func (db *Dashboard) Start(server *p2p.Server) error { log.Info("Starting dashboard") + var err error + db.geodb, err = OpenGeoDB() + if err != nil { + log.Warn("Failed to open geodb", "err", err) + return err + } + db.wg.Add(2) go db.collectData() go db.streamLogs() @@ -154,6 +170,8 @@ func (db *Dashboard) Start(server *p2p.Server) error { // Stop stops the data collection thread and the connection listener of the dashboard. // Implements the node.Service interface. func (db *Dashboard) Stop() error { + db.geodb.Close() + // Close the connection listener. var errs []error if err := db.listener.Close(); err != nil { @@ -262,7 +280,7 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) { // meterCollector returns a function, which retrieves a specific meter. func meterCollector(name string) func() int64 { - if meter := metrics.DefaultRegistry.Get(name); meter != nil { + if meter := metrics.Get(name); meter != nil { m := meter.(metrics.Meter) return func() int64 { return m.Count() @@ -282,8 +300,8 @@ func (db *Dashboard) collectData() { var ( mem runtime.MemStats - collectNetworkIngress = meterCollector("p2p/InboundTraffic") - collectNetworkEgress = meterCollector("p2p/OutboundTraffic") + collectNetworkIngress = meterCollector(p2p.MetricsInboundTraffic) + collectNetworkEgress = meterCollector(p2p.MetricsOutboundTraffic) collectDiskRead = meterCollector("eth/db/chaindata/disk/read") collectDiskWrite = meterCollector("eth/db/chaindata/disk/write") @@ -374,21 +392,83 @@ func (db *Dashboard) collectData() { sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite) db.lock.Unlock() - peerIDs := p2p.TrafficMeterCollector.GetIDs() - nm := &NetworkMessage{ - Changed: p2p.TrafficMeterCollector.GetAndClearChanged(), + peers := p2p.PeerTrafficMeters.Peers() + network := new(NetworkMessage) + if len(peers) > 0 { + network.Peers = make(map[uint]*Peer) } - for _, id := range peerIDs { - nm.Peers = append(nm.Peers, &Peer{ - ID: id[len(id)-6:], - Ingress: p2p.PeerIngressRegistry.Get(id).(metrics.Meter).Count(), - Egress: p2p.PeerEgressRegistry.Get(id).(metrics.Meter).Count(), - }) - fmt.Println(metrics.DefaultRegistry.Get(fmt.Sprintf("%s/%s", p2p.IngressPrefix, id)).(metrics.Meter).Count()) - fmt.Println(metrics.DefaultRegistry.Get(fmt.Sprintf("%s/%s", p2p.EgressPrefix, id)).(metrics.Meter).Count()) + for id, peer := range peers { + peerIngress := &ChartEntry{ + Time: now, + Value: float64(peer.Ingress), + } + peerEgress := &ChartEntry{ + Time: now, + Value: float64(peer.Egress), + } + p := &Peer{ + Ingress: ChartEntries{peerIngress}, + Egress: ChartEntries{peerEgress}, + } + if prevMetrics, ok := db.history.Network.Peers[id]; !ok { + p.ID = peer.ID + p.IP = peer.IP + location := db.geodb.Lookup(peer.IP) + p.Location = &PeerLocation{ + Country: location.Country.Names.English, + City: location.City.Names.English, + Latitude: location.Location.Latitude, + Longitude: location.Location.Longitude, + } + p.Lifecycle = &PeerLifecycle{ + Connected: peer.Connected, + } + if peer.Handshake != nil { + p.Lifecycle.Handshake = peer.Handshake + } + if peer.Disconnected != nil { + p.Lifecycle.Disconnected = peer.Disconnected + } + db.history.Network.Peers[id] = p + } else { + if prevMetrics.ID != peer.ID { + prevMetrics.ID, p.ID = peer.ID, peer.ID + } + if prevMetrics.Lifecycle.Handshake == nil && peer.Handshake != nil { + p.Lifecycle = new(PeerLifecycle) + prevMetrics.Lifecycle.Handshake, p.Lifecycle.Handshake = peer.Handshake, peer.Handshake + } + if prevMetrics.Lifecycle.Disconnected == nil && peer.Disconnected != nil { + if p.Lifecycle == nil { + p.Lifecycle = new(PeerLifecycle) + } + prevMetrics.Lifecycle.Disconnected, p.Lifecycle.Disconnected = peer.Disconnected, peer.Disconnected + } + first := 0 + if len(prevMetrics.Ingress) >= peerIngressSampleLimit { + first = len(prevMetrics.Ingress) - peerIngressSampleLimit + 1 + } + prevMetrics.Ingress = append(prevMetrics.Ingress[first:], peerIngress) + first = 0 + if len(prevMetrics.Egress) >= peerEgressSampleLimit { + first = len(prevMetrics.Egress) - peerEgressSampleLimit + 1 + } + prevMetrics.Egress = append(prevMetrics.Egress[first:], peerEgress) + } + if p.Lifecycle != nil && p.Lifecycle.Disconnected != nil { + select { + case db.disconnectedPeerIDs <- id: + default: + // if the number of the stored disconnected peers exceeds the limit, remove the firstly disconnected peer + delete(db.history.Network.Peers, <-db.disconnectedPeerIDs) + db.disconnectedPeerIDs <- id + } + } + network.Peers[id] = p } - s, _ := json.Marshal(nm) - fmt.Println(string(s)) + db.lock.Unlock() + //s, _ := json.MarshalIndent(network, "", " ") + //fmt.Println(string(s)) db.sendToAll(&Message{ System: &SystemMessage{ @@ -401,7 +481,7 @@ func (db *Dashboard) collectData() { DiskRead: ChartEntries{diskRead}, DiskWrite: ChartEntries{diskWrite}, }, - Network: nm, + Network: network, }) } } diff --git a/dashboard/geoip.go b/dashboard/geoip.go new file mode 100644 index 0000000000..cc702dac90 --- /dev/null +++ b/dashboard/geoip.go @@ -0,0 +1,79 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package dashboard + +import ( + "github.com/apilayer/freegeoip" + "time" + "net" +) +// Package geoip contains utility methods for converting IPs to geographical data. + +// GeoDBInfo contains all the geographical information we could extract based on an IP +// address. +type GeoDBInfo struct { + Country struct { + Names struct { + English string `maxminddb:"en" json:"en,omitempty"` + } `maxminddb:"names" json:"names,omitempty"` + } `maxminddb:"country" json:"country,omitempty"` + City struct { + Names struct { + English string `maxminddb:"en" json:"en,omitempty"` + } `maxminddb:"names" json:"names,omitempty"` + } `maxminddb:"city" json:"city,omitempty"` + Location struct { + Latitude float64 `maxminddb:"latitude" json:"latitude,omitempty"` + Longitude float64 `maxminddb:"longitude" json:"longitude,omitempty"` + } `maxminddb:"location" json:"location,omitempty"` +} + +// GeoDB represents a geoip database that can be queried for IP to geographical +// information conversions. +type GeoDB struct { + geodb *freegeoip.DB +} + +// Open creats a new geoip database with an up-to-date database from the internet. +func OpenGeoDB() (*GeoDB, error) { + // Initiate a geoip database to cross reference locations + db, err := freegeoip.OpenURL(freegeoip.MaxMindDB, 24*time.Hour, time.Hour) + if err != nil { + return nil, err + } + // Wait until the database is updated to the latest data + select { + case <-db.NotifyOpen(): + case err := <-db.NotifyError(): + return nil, err + } + // Assemble and return our custom wrapper + return &GeoDB{geodb: db}, nil +} + +// Close terminates the database background updater. +func (db *GeoDB) Close() error { + db.geodb.Close() + return nil +} + +// Lookup converts an IP address to a geographical location. +func (db *GeoDB) Lookup(ip net.IP) *GeoDBInfo { + result := new(GeoDBInfo) + db.geodb.Lookup(ip, result) + return result +} diff --git a/dashboard/message.go b/dashboard/message.go index 99050d854d..c1c6a32445 100644 --- a/dashboard/message.go +++ b/dashboard/message.go @@ -18,6 +18,7 @@ package dashboard import ( "encoding/json" + "net" "time" ) @@ -34,8 +35,8 @@ type Message struct { type ChartEntries []*ChartEntry type ChartEntry struct { - Time time.Time `json:"time,omitempty"` - Value float64 `json:"value,omitempty"` + Time time.Time `json:"time"` + Value float64 `json:"value"` } type GeneralMessage struct { @@ -56,14 +57,29 @@ type TxPoolMessage struct { } type NetworkMessage struct { - Peers []*Peer `json:"peers,omitempty"` - Changed []string `json:"changed,omitempty"` + Peers map[uint]*Peer `json:"peers,omitempty"` } type Peer struct { - ID string `json:"id,omitempty"` - Ingress int64 `json:"ingress,omitempty"` - Egress int64 `json:"egress,omitempty"` + ID string `json:"id,omitempty"` + IP net.IP `json:"ip,omitempty"` + Location *PeerLocation `json:"location,omitempty"` + Lifecycle *PeerLifecycle `json:"lifecycle,omitempty"` + Ingress ChartEntries `json:"ingress,omitempty"` + Egress ChartEntries `json:"egress,omitempty"` +} + +type PeerLocation struct { + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` +} + +type PeerLifecycle struct { + Connected *time.Time `json:"connected,omitempty"` + Handshake *time.Time `json:"handshake,omitempty"` + Disconnected *time.Time `json:"disconnected,omitempty"` } type SystemMessage struct { diff --git a/p2p/dial.go b/p2p/dial.go index d8feceb9f3..e2aba5587b 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -350,7 +350,7 @@ func (t *dialTask) dial(srv *Server, dest *discover.Node) error { if err != nil { return &dialError{err} } - mfd := newMeteredConn(fd, false) + mfd := newMeteredConn(fd, false, dest.IP) return srv.SetupConn(mfd, t.flags, dest) } diff --git a/p2p/metrics.go b/p2p/metrics.go index dfabad0873..2ab133698c 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -21,181 +21,172 @@ package p2p import ( "net" - "fmt" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" - "github.com/syndtr/goleveldb/leveldb/errors" + "github.com/mohae/deepcopy" "sync" "sync/atomic" + "time" ) const ( - IngressPrefix = "p2p/InboundTraffic" - EgressPrefix = "p2p/OutboundTraffic" + MetricsInboundTraffic = "p2p/InboundTraffic" + MetricsInboundConnects = "p2p/InboundConnects" + MetricsOutboundTraffic = "p2p/OutboundTraffic" + MetricsOutboundConnects = "p2p/OutboundConnects" + + MetricsRegistryIngressPrefix = MetricsInboundTraffic + "/" + MetricsRegistryEgressPrefix = MetricsOutboundTraffic + "/" + + MeteredPeerLimit = 16384 ) var ( - ingressConnectMeter = metrics.NewRegisteredMeter("p2p/InboundConnects", nil) - egressConnectMeter = metrics.NewRegisteredMeter("p2p/OutboundConnects", nil) + ingressConnectMeter = metrics.NewRegisteredMeter(MetricsInboundConnects, nil) + ingressTrafficMeter = metrics.NewRegisteredMeter(MetricsInboundTraffic, nil) + egressConnectMeter = metrics.NewRegisteredMeter(MetricsOutboundConnects, nil) + egressTrafficMeter = metrics.NewRegisteredMeter(MetricsOutboundTraffic, nil) - PeerIngressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, IngressPrefix+"/") - PeerEgressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, EgressPrefix+"/") - TrafficMeterCollector = newTrafficMeterCollector() + PeerIngressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, MetricsRegistryIngressPrefix) + PeerEgressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, MetricsRegistryEgressPrefix) + PeerTrafficMeters = newPeerTrafficMeters() nextDefaultID uint32 ) -type trafficMeter struct { +type PeerMetrics struct { + ID string + IP net.IP + + // TODO: -* + Connected *time.Time + Handshake *time.Time + Disconnected *time.Time + + Ingress int64 + Egress int64 + + traffic func() (ingress, egress int64) +} + +type peerTrafficMeters struct { + peers map[uint]*PeerMetrics + lock sync.RWMutex +} + +func newPeerTrafficMeters() *peerTrafficMeters { + return &peerTrafficMeters{ + peers: make(map[uint]*PeerMetrics), + } +} + +func (m *peerTrafficMeters) register(id uint, ip net.IP, traffic func() (ingress, egress int64)) error { + now := time.Now() + peer := &PeerMetrics{ + IP: ip, + Connected: &now, + traffic: traffic, + } + m.lock.Lock() + m.peers[id] = peer + m.lock.Unlock() + + return nil +} + +func (m *peerTrafficMeters) handshakeDone(id uint, peerID string, traffic func() (ingress, egress int64)) { + now := time.Now() + m.lock.Lock() + if peer, ok := m.peers[id]; ok { + peer.Handshake = &now + peer.ID = peerID + peer.traffic = traffic + } + m.lock.Unlock() +} + +func (m *peerTrafficMeters) close(id uint) { + now := time.Now() + m.lock.Lock() + m.peers[id].Disconnected = &now + m.lock.Unlock() +} + +func (m *peerTrafficMeters) Peers() map[uint]*PeerMetrics { + peers := make(map[uint]*PeerMetrics) + m.lock.Lock() + for id, peer := range m.peers { + peer.Ingress, peer.Egress = peer.traffic() + peers[id] = deepcopy.Copy(peer).(*PeerMetrics) + if peer.Disconnected != nil { + PeerIngressRegistry.Unregister(peer.ID) + PeerEgressRegistry.Unregister(peer.ID) + delete(m.peers, id) + } + } + m.lock.Unlock() + return peers +} + +type networkMeter struct { ingress metrics.Meter egress metrics.Meter } -type trafficMeterCollector struct { - common *trafficMeter - peers map[string]*trafficMeter - changed []string - - lock sync.RWMutex - cLock sync.Mutex -} - -func newTrafficMeterCollector() *trafficMeterCollector { - return &trafficMeterCollector{ - common: &trafficMeter{ - ingress: metrics.NewRegisteredMeter(IngressPrefix, nil), - egress: metrics.NewRegisteredMeter(EgressPrefix, nil), - }, - peers: make(map[string]*trafficMeter), - changed: make([]string, 0, 128), - } -} - -func (tmc *trafficMeterCollector) register(id string, tm *trafficMeter) error { - if tm == nil { - peer := &trafficMeter{ - ingress: metrics.NewRegisteredMeter(id, PeerIngressRegistry), - egress: metrics.NewRegisteredMeter(id, PeerEgressRegistry), - } - tmc.lock.Lock() - tmc.peers[id] = peer - tmc.lock.Unlock() - return nil - } - if tm.ingress == nil || tm.egress == nil { - return errors.New("Meter is not set correctly") - } - if err := PeerIngressRegistry.Register(id, tm.ingress); err != nil { - return err - } - if err := PeerEgressRegistry.Register(id, tm.egress); err != nil { - PeerIngressRegistry.Unregister(id) - return err - } - tmc.lock.Lock() - tmc.peers[id] = tm - tmc.lock.Unlock() - return nil -} - -func (tmc *trafficMeterCollector) unregister(old, new string) { - PeerIngressRegistry.Unregister(old) - PeerEgressRegistry.Unregister(old) - - tmc.lock.Lock() - delete(tmc.peers, old) - tmc.lock.Unlock() - - tmc.cLock.Lock() - tmc.changed = append(tmc.changed, old, new) - tmc.cLock.Unlock() -} - -func (tmc *trafficMeterCollector) changeID(old, new string) error { - tmc.lock.RLock() - peer, ok := tmc.peers[old] - tmc.lock.RUnlock() - if !ok { - return errors.New(fmt.Sprintf("No meter with id %s", old)) - } - if err := tmc.register(new, peer); err != nil { - return err - } - tmc.unregister(old, new) - return nil -} - -func (tmc *trafficMeterCollector) markIngress(id string, n int64) { - tmc.common.ingress.Mark(n) - tmc.lock.RLock() - peer, ok := tmc.peers[id] - tmc.lock.RUnlock() - if ok { - peer.ingress.Mark(n) - } -} - -func (tmc *trafficMeterCollector) markEgress(id string, n int64) { - tmc.common.egress.Mark(n) - tmc.lock.RLock() - peer, ok := tmc.peers[id] - tmc.lock.RUnlock() - if ok { - peer.egress.Mark(n) - } -} - -func (tmc *trafficMeterCollector) GetIDs() []string { - tmc.lock.RLock() - ids := make([]string, 0, len(tmc.peers)) - for id := range tmc.peers { - ids = append(ids, id) - } - tmc.lock.RUnlock() - return ids -} - -func (tmc *trafficMeterCollector) GetAndClearChanged() []string { - tmc.cLock.Lock() - defer tmc.cLock.Unlock() - - changed := make([]string, len(tmc.changed)) - copy(changed, tmc.changed) - tmc.changed = tmc.changed[:0] - - return changed -} - // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { net.Conn // Network connection to wrap with metering - id string + id uint + meter *networkMeter + + ingressBeforeHandshake int64 + egressBeforeHandshake int64 + + lock sync.RWMutex } // newMeteredConn creates a new metered connection, also bumping the ingress or // egress connection meter. If the metrics system is disabled, this function // returns the original object. -func newMeteredConn(conn net.Conn, ingress bool) net.Conn { +func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn { // Short circuit if metrics are disabled if !metrics.Enabled { return conn } + if len(PeerTrafficMeters.peers) >= MeteredPeerLimit { + log.Warn("Metered peer limit exceeded") + return conn + } // Otherwise bump the connection counters and wrap the connection if ingress { ingressConnectMeter.Mark(1) } else { egressConnectMeter.Mark(1) } - id := fmt.Sprintf("unidentified_%d", atomic.AddUint32(&nextDefaultID, 1)) - TrafficMeterCollector.register(id, nil) - return &meteredConn{Conn: conn, id: id} + id := uint(atomic.AddUint32(&nextDefaultID, 1)) + c := &meteredConn{ + Conn: conn, + id: id, + } + PeerTrafficMeters.register(id, ip, func() (ingress, egress int64) { + return atomic.LoadInt64(&c.ingressBeforeHandshake), atomic.LoadInt64(&c.egressBeforeHandshake) + }) + return c } // Read delegates a network read to the underlying connection, bumping the ingress // traffic meter along the way. func (c *meteredConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) - TrafficMeterCollector.markIngress(c.id, int64(n)) + ingressTrafficMeter.Mark(int64(n)) + c.lock.RLock() + if c.meter == nil { + atomic.AddInt64(&c.ingressBeforeHandshake, int64(n)) + } else { + c.meter.ingress.Mark(int64(n)) + } + c.lock.RUnlock() return n, err } @@ -203,19 +194,44 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { // egress traffic meter along the way. func (c *meteredConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) - TrafficMeterCollector.markEgress(c.id, int64(n)) + egressTrafficMeter.Mark(int64(n)) + c.lock.RLock() + if c.meter == nil { + atomic.AddInt64(&c.egressBeforeHandshake, int64(n)) + } else { + c.meter.egress.Mark(int64(n)) + } + c.lock.RUnlock() return n, err } func (c *meteredConn) Close() error { - TrafficMeterCollector.unregister(c.id, "") + PeerTrafficMeters.close(c.id) return c.Conn.Close() } -func (c *meteredConn) setPeerID(id string) { - if err := TrafficMeterCollector.changeID(c.id, id); err != nil { - log.Warn("Failed to set peer id", "id", fmt.Sprintf("%s...%s", id[:6], id[len(id)-6:]), "err", err) - return +func (c *meteredConn) handshakeDone(peerID string) { + m := &networkMeter{ + ingress: metrics.NewRegisteredMeter(peerID, PeerIngressRegistry), + egress: metrics.NewRegisteredMeter(peerID, PeerEgressRegistry), } - c.id = id + c.lock.Lock() + m.ingress.Mark(atomic.LoadInt64(&c.ingressBeforeHandshake)) + m.egress.Mark(atomic.LoadInt64(&c.egressBeforeHandshake)) + c.meter = m + c.lock.Unlock() + + ingressMeter, oki := PeerIngressRegistry.Get(peerID).(metrics.Meter) + egressMeter, oke := PeerEgressRegistry.Get(peerID).(metrics.Meter) + traffic := func() (ingress, egress int64) { + return 0, 0 + } + if oki && oke { + traffic = func() (ingress, egress int64) { + return ingressMeter.Count(), egressMeter.Count() + } + } else { + log.Warn("Failed to get traffic meter", "peerID", peerID) + } + PeerTrafficMeters.handshakeDone(c.id, peerID, traffic) } diff --git a/p2p/rlpx.go b/p2p/rlpx.go index 393676b815..46b6668695 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -586,9 +586,6 @@ func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW { // we use an all-zeroes IV for AES because the key used // for encryption is ephemeral. iv := make([]byte, encc.BlockSize()) - if c, ok := conn.(*meteredConn); ok { - c.setPeerID(s.RemoteID.String()) - } return &rlpxFrameRW{ conn: conn, enc: cipher.NewCTR(encc, iv), diff --git a/p2p/server.go b/p2p/server.go index 8f3a511f31..c5087710f8 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -41,7 +41,7 @@ const ( // Connectivity defaults. maxActiveDialTasks = 16 - defaultMaxPendingPeers = 50 + DefaultMaxPendingPeers = 50 defaultDialRatio = 3 // Maximum time allowed for reading a complete message. @@ -799,7 +799,7 @@ func (srv *Server) listenLoop() { defer srv.loopWG.Done() srv.log.Info("RLPx listener up", "self", srv.makeSelf(srv.listener, srv.ntab)) - tokens := defaultMaxPendingPeers + tokens := DefaultMaxPendingPeers if srv.MaxPendingPeers > 0 { tokens = srv.MaxPendingPeers } @@ -838,7 +838,11 @@ func (srv *Server) listenLoop() { } } - fd = newMeteredConn(fd, true) + var ip net.IP + if tcp, ok := fd.RemoteAddr().(*net.TCPAddr); ok { + ip = tcp.IP + } + fd = newMeteredConn(fd, true, ip) srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr()) go func() { srv.SetupConn(fd, inboundConn, nil) @@ -878,6 +882,9 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) e srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err) return err } + if conn, ok := c.fd.(*meteredConn); ok { + conn.handshakeDone(c.id.String()) + } clog := srv.log.New("id", c.id, "addr", c.fd.RemoteAddr(), "conn", c.flags) // For dialed connections, check that the remote public key matches. if dialDest != nil && c.id != dialDest.ID { From 8fd0e4ad1eb38a8cc3e2cadf60c72788ee0d2a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Tue, 7 Aug 2018 14:28:25 +0300 Subject: [PATCH 5/7] dashboard, p2p: initial version of peer traffic metering with event feeds --- dashboard/assets/components/Network.jsx | 129 +++++++--- dashboard/assets/types/content.jsx | 20 +- dashboard/dashboard.go | 128 ++-------- dashboard/message.go | 24 +- dashboard/peers.go | 301 ++++++++++++++++++++++++ p2p/metrics.go | 208 +++++++--------- p2p/server.go | 1 + 7 files changed, 533 insertions(+), 278 deletions(-) create mode 100644 dashboard/peers.go diff --git a/dashboard/assets/components/Network.jsx b/dashboard/assets/components/Network.jsx index 19773a8a34..aad95403f6 100644 --- a/dashboard/assets/components/Network.jsx +++ b/dashboard/assets/components/Network.jsx @@ -18,36 +18,59 @@ import React, {Component} from 'react'; -import Table, {TableBody, TableHeader, TableHeaderColumn, TableRow, TableCell} from 'material-ui/Table'; +import Table, {TableHead, TableBody, TableRow, TableCell} from 'material-ui/Table'; import type {Network as NetworkType, Peer} from '../types/content'; // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. -export const inserter = (update: {[number]: Peer}, prev: {[number]: Peer}) => { - Object.keys(update).forEach((k) => { - if (!prev[k]) { - prev[k] = update[k]; +export const inserter = (update: {[string]: {[string]: Peer}}, prev: {[string]: {[string]: Peer}}) => { + Object.keys(update).forEach((ip) => { + if (!prev[ip]) { + prev[ip] = update[ip]; return; } - const u: Peer = update[k]; - const p: Peer = prev[k]; - if (u.id) { - p.id = u.id; + if (!update[ip]) { + return; } - if (u.ip) { - p.ip = u.ip; - } - if (u.lifecycle) { - if (u.lifecycle.handshake) { - p.lifecycle.handshake = u.lifecycle.handshake; + Object.keys(update[ip]).forEach((id) => { + if (!prev[ip][id]) { + prev[ip][id] = update[ip][id]; + return; } - if (u.lifecycle.disconnected) { - p.lifecycle.disconnected = u.lifecycle.disconnected; + const u: Peer = update[ip][id]; + const p: Peer = prev[ip][id]; + if (u.connected) { + if (!Array.isArray(p.connected)) { + p.connected = []; + } + p.connected = [...p.connected, ...u.connected]; } - } - p.ingress = [...p.ingress, ...u.ingress].slice(-200); - p.egress = [...p.egress, ...u.egress].slice(-200); - prev[k] = p; + if (u.handshake) { + if (!Array.isArray(p.handshake)) { + p.handshake = []; + } + p.handshake = [...p.handshake, ...u.handshake]; + } + if (u.disconnected) { + if (!Array.isArray(p.disconnected)) { + p.disconnected = []; + } + p.disconnected = [...p.disconnected, ...u.disconnected]; + } + if (Array.isArray(u.ingress)) { + if (!Array.isArray(p.ingress)) { + p.ingress = []; + } + p.ingress = [...p.ingress, ...u.ingress].slice(-200); + } + if (Array.isArray(u.egress)) { + if (!Array.isArray(p.egress)) { + p.egress = []; + } + p.egress = [...p.egress, ...u.egress].slice(-200); + } + prev[ip][id] = p; + }); }); return prev; }; @@ -63,19 +86,65 @@ export type Props = { // Network renders the network page. class Network extends Component { + formatTime = (t) => { + const time = new Date(t); + if (isNaN(time)) { + return ''; + } + const month = `0${time.getMonth() + 1}`.slice(-2); + const date = `0${time.getDate()}`.slice(-2); + const hours = `0${time.getHours()}`.slice(-2); + const minutes = `0${time.getMinutes()}`.slice(-2); + const seconds = `0${time.getSeconds()}`.slice(-2); + return `${month}/${date}/${hours}:${minutes}:${seconds}`; + }; + render() { return ( + + + IP + Peer ID + Location + Ingress + Egress + Connected + Handshake + Disconnected + + - {Object.entries(this.props.content.peers).map(([k, v]) => ( - - {k} - {v.id ? v.id.substring(0, 6) : ''} - {v.ip} - {v.ingress.value} - {v.egress.value} - {JSON.stringify(v.location)} - {JSON.stringify(v.lifecycle)} + {Object.entries(this.props.content.peers).map(([ip, peers]) => ( + + {ip} + + {Object.keys(peers).map(id => id.substring(0, 10)).join(' ')} + + + {(() => { + const k = Object.keys(peers)[0]; + return k && peers[k].location ? (() => { + const l = peers[k].location; + return `${l.country}${l.city ? `/${l.city}` : ''} ${l.latitude} ${l.longitude}`; + })() : ''; + })()} + + + {Object.keys(peers).map((id) => peers[id].ingress && peers[id].ingress.map(sample => sample.value).join(' ')).join(', ')} + + + {Object.keys(peers).map((id) => peers[id].egress && peers[id].egress.map(sample => sample.value).join(' ')).join(', ')} + + + {Object.keys(peers).map((id) => peers[id].connected && peers[id].connected.map(time => this.formatTime(time)).join(' ')).join(', ')} + + + {Object.keys(peers).map((id) => peers[id].handshake && peers[id].handshake.map(time => this.formatTime(time)).join(' ')).join(', ')} + + + {Object.keys(peers).map((id) => peers[id].disconnected && peers[id].disconnected.map(time => this.formatTime(time)).join(' ')).join(', ')} + ))} diff --git a/dashboard/assets/types/content.jsx b/dashboard/assets/types/content.jsx index 691db4987a..94f70537a7 100644 --- a/dashboard/assets/types/content.jsx +++ b/dashboard/assets/types/content.jsx @@ -51,16 +51,16 @@ export type TxPool = { }; export type Network = { - peers: {[number]: Peer}, + peers: {[string]: {[string]: Peer}}, }; export type Peer = { - id: string, - ip: string, - location: PeerLocation, - lifecycle: PeerLifecycle, - ingress: ChartEntries, - egress: ChartEntries, + location: PeerLocation, + connected: Array, + handshake: Array, + disconnected: Array, + ingress: ChartEntries, + egress: ChartEntries, }; export type PeerLocation = { @@ -70,12 +70,6 @@ export type PeerLocation = { longitude: number, }; -export type PeerLifecycle = { - connected: Date, - handshake: Date, - disconnected: Date, -}; - export type System = { activeMemory: ChartEntries, virtualMemory: ChartEntries, diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index f92a4c6d64..dbd44b0620 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -54,9 +54,9 @@ const ( diskReadSampleLimit = 200 // Maximum number of disk read data samples diskWriteSampleLimit = 200 // Maximum number of disk write data samples - storedDisconnectedPeerLimit = p2p.MeteredPeerLimit - p2p.DefaultMaxPendingPeers - peerIngressSampleLimit = 200 - peerEgressSampleLimit = 200 + peerTrafficSampleLimit = 200 + peerIngressSampleLimit = peerTrafficSampleLimit + peerEgressSampleLimit = peerTrafficSampleLimit ) var nextID uint32 // Next connection id @@ -68,11 +68,14 @@ type Dashboard struct { listener net.Listener conns map[uint32]*client // Currently live websocket connections history *Message + peerHistory *NetworkMessage lock sync.RWMutex // Lock protecting the dashboard's internals + peerLock sync.RWMutex - geodb *GeoDB - disconnectedPeerIDs chan uint - logdir string + geodb *GeoDB + peersByIP map[string]*Peer + peersByID map[string]*Peer + logdir string quit chan chan error // Channel used for graceful exit wg sync.WaitGroup @@ -111,12 +114,11 @@ func New(config *Config, commit string, logdir string) *Dashboard { DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), }, - Network: &NetworkMessage{ - Peers: make(map[uint]*Peer), - }, }, - disconnectedPeerIDs: make(chan uint, storedDisconnectedPeerLimit), - logdir: logdir, + peerHistory: &NetworkMessage{ + Peers: make(map[string]map[string]*Peer), + }, + logdir: logdir, } } @@ -142,16 +144,10 @@ func (db *Dashboard) APIs() []rpc.API { return nil } func (db *Dashboard) Start(server *p2p.Server) error { log.Info("Starting dashboard") - var err error - db.geodb, err = OpenGeoDB() - if err != nil { - log.Warn("Failed to open geodb", "err", err) - return err - } - - db.wg.Add(2) + db.wg.Add(3) go db.collectData() go db.streamLogs() + go db.collectPeerData() http.HandleFunc("/", db.webHandler) http.Handle("/api", websocket.Handler(db.apiHandler)) @@ -170,8 +166,6 @@ func (db *Dashboard) Start(server *p2p.Server) error { // Stop stops the data collection thread and the connection listener of the dashboard. // Implements the node.Service interface. func (db *Dashboard) Stop() error { - db.geodb.Close() - // Close the connection listener. var errs []error if err := db.listener.Close(); err != nil { @@ -179,7 +173,7 @@ func (db *Dashboard) Stop() error { } // Close the collectors. errc := make(chan error, 1) - for i := 0; i < 2; i++ { + for i := 0; i < 3; i++ { db.quit <- errc if err := <-errc; err != nil { errs = append(errs, err) @@ -252,10 +246,17 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) { } }() - db.lock.Lock() // Send the past data. - client.msg <- deepcopy.Copy(db.history).(*Message) + db.lock.RLock() + h := deepcopy.Copy(db.history).(*Message) + db.lock.RUnlock() + db.peerLock.RLock() + h.Network = deepcopy.Copy(db.peerHistory).(*NetworkMessage) + db.peerLock.RUnlock() + client.msg <- h + // Start tracking the connection and drop at connection loss. + db.lock.Lock() db.conns[id] = client db.lock.Unlock() defer func() { @@ -392,84 +393,6 @@ func (db *Dashboard) collectData() { sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite) db.lock.Unlock() - peers := p2p.PeerTrafficMeters.Peers() - network := new(NetworkMessage) - if len(peers) > 0 { - network.Peers = make(map[uint]*Peer) - } - for id, peer := range peers { - peerIngress := &ChartEntry{ - Time: now, - Value: float64(peer.Ingress), - } - peerEgress := &ChartEntry{ - Time: now, - Value: float64(peer.Egress), - } - p := &Peer{ - Ingress: ChartEntries{peerIngress}, - Egress: ChartEntries{peerEgress}, - } - if prevMetrics, ok := db.history.Network.Peers[id]; !ok { - p.ID = peer.ID - p.IP = peer.IP - location := db.geodb.Lookup(peer.IP) - p.Location = &PeerLocation{ - Country: location.Country.Names.English, - City: location.City.Names.English, - Latitude: location.Location.Latitude, - Longitude: location.Location.Longitude, - } - p.Lifecycle = &PeerLifecycle{ - Connected: peer.Connected, - } - if peer.Handshake != nil { - p.Lifecycle.Handshake = peer.Handshake - } - if peer.Disconnected != nil { - p.Lifecycle.Disconnected = peer.Disconnected - } - db.history.Network.Peers[id] = p - } else { - if prevMetrics.ID != peer.ID { - prevMetrics.ID, p.ID = peer.ID, peer.ID - } - if prevMetrics.Lifecycle.Handshake == nil && peer.Handshake != nil { - p.Lifecycle = new(PeerLifecycle) - prevMetrics.Lifecycle.Handshake, p.Lifecycle.Handshake = peer.Handshake, peer.Handshake - } - if prevMetrics.Lifecycle.Disconnected == nil && peer.Disconnected != nil { - if p.Lifecycle == nil { - p.Lifecycle = new(PeerLifecycle) - } - prevMetrics.Lifecycle.Disconnected, p.Lifecycle.Disconnected = peer.Disconnected, peer.Disconnected - } - first := 0 - if len(prevMetrics.Ingress) >= peerIngressSampleLimit { - first = len(prevMetrics.Ingress) - peerIngressSampleLimit + 1 - } - prevMetrics.Ingress = append(prevMetrics.Ingress[first:], peerIngress) - first = 0 - if len(prevMetrics.Egress) >= peerEgressSampleLimit { - first = len(prevMetrics.Egress) - peerEgressSampleLimit + 1 - } - prevMetrics.Egress = append(prevMetrics.Egress[first:], peerEgress) - } - if p.Lifecycle != nil && p.Lifecycle.Disconnected != nil { - select { - case db.disconnectedPeerIDs <- id: - default: - // if the number of the stored disconnected peers exceeds the limit, remove the firstly disconnected peer - delete(db.history.Network.Peers, <-db.disconnectedPeerIDs) - db.disconnectedPeerIDs <- id - } - } - network.Peers[id] = p - } - db.lock.Unlock() - //s, _ := json.MarshalIndent(network, "", " ") - //fmt.Println(string(s)) - db.sendToAll(&Message{ System: &SystemMessage{ ActiveMemory: ChartEntries{activeMemory}, @@ -481,7 +404,6 @@ func (db *Dashboard) collectData() { DiskRead: ChartEntries{diskRead}, DiskWrite: ChartEntries{diskWrite}, }, - Network: network, }) } } diff --git a/dashboard/message.go b/dashboard/message.go index c1c6a32445..cb9bd0cc8b 100644 --- a/dashboard/message.go +++ b/dashboard/message.go @@ -18,7 +18,6 @@ package dashboard import ( "encoding/json" - "net" "time" ) @@ -56,17 +55,18 @@ type TxPoolMessage struct { /* TODO (kurkomisi) */ } +// k1: IP, k2: ID type NetworkMessage struct { - Peers map[uint]*Peer `json:"peers,omitempty"` + Peers map[string]map[string]*Peer `json:"peers,omitempty"` } type Peer struct { - ID string `json:"id,omitempty"` - IP net.IP `json:"ip,omitempty"` - Location *PeerLocation `json:"location,omitempty"` - Lifecycle *PeerLifecycle `json:"lifecycle,omitempty"` - Ingress ChartEntries `json:"ingress,omitempty"` - Egress ChartEntries `json:"egress,omitempty"` + Location *PeerLocation `json:"location,omitempty"` + Connected []time.Time `json:"connected,omitempty"` + Handshake []time.Time `json:"handshake,omitempty"` + Disconnected []time.Time `json:"disconnected,omitempty"` + Ingress ChartEntries `json:"ingress,omitempty"` + Egress ChartEntries `json:"egress,omitempty"` } type PeerLocation struct { @@ -76,12 +76,6 @@ type PeerLocation struct { Longitude float64 `json:"longitude,omitempty"` } -type PeerLifecycle struct { - Connected *time.Time `json:"connected,omitempty"` - Handshake *time.Time `json:"handshake,omitempty"` - Disconnected *time.Time `json:"disconnected,omitempty"` -} - type SystemMessage struct { ActiveMemory ChartEntries `json:"activeMemory,omitempty"` VirtualMemory ChartEntries `json:"virtualMemory,omitempty"` @@ -93,7 +87,7 @@ type SystemMessage struct { DiskWrite ChartEntries `json:"diskWrite,omitempty"` } -// LogsMessage wraps up a log chunk. If Source isn't present, the chunk is a stream chunk. +// LogsMessage wraps up a log chunk. If 'Source' isn't present, the chunk is a stream chunk. type LogsMessage struct { Source *LogFile `json:"source,omitempty"` // Attributes of the log file. Chunk json.RawMessage `json:"chunk"` // Contains log records. diff --git a/dashboard/peers.go b/dashboard/peers.go new file mode 100644 index 0000000000..06d80a0704 --- /dev/null +++ b/dashboard/peers.go @@ -0,0 +1,301 @@ +package dashboard + +import ( + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "time" + "github.com/mohae/deepcopy" +) + +const eventBufferLimit = 128 + +func getOrInitPeer(m *NetworkMessage, ip, id string) *Peer { + if _, ok := m.Peers[ip]; !ok { + m.Peers[ip] = make(map[string]*Peer) + } + if _, ok := m.Peers[ip][id]; !ok { + m.Peers[ip][id] = new(Peer) + } + return m.Peers[ip][id] +} + +func (db *Dashboard) collectPeerData() { + defer db.wg.Done() + + var err error + db.geodb, err = OpenGeoDB() + if err != nil { + log.Warn("Failed to open geodb", "err", err) + return + } + defer db.geodb.Close() + + var ( + quit = make(chan struct{}) + connectCh = make(chan *p2p.PeerConnectEvent, eventBufferLimit) + handshakeCh = make(chan *p2p.PeerHandshakeEvent, eventBufferLimit) + disconnectCh = make(chan *p2p.PeerDisconnectEvent, eventBufferLimit) + readCh = make(chan *p2p.PeerReadEvent, eventBufferLimit) + writeCh = make(chan *p2p.PeerWriteEvent, eventBufferLimit) + ) + go func() { + var ( + peerConnectEventCh = make(chan p2p.PeerConnectEvent, eventBufferLimit) + peerHandshakeEventCh = make(chan p2p.PeerHandshakeEvent, eventBufferLimit) + peerDisconnectEventCh = make(chan p2p.PeerDisconnectEvent, eventBufferLimit) + peerReadEventCh = make(chan p2p.PeerReadEvent, eventBufferLimit) + peerWriteEventCh = make(chan p2p.PeerWriteEvent, eventBufferLimit) + + subConnect = p2p.SubscribePeerConnectEvent(peerConnectEventCh) + subHandshake = p2p.SubscribePeerHandshakeEvent(peerHandshakeEventCh) + subDisconnect = p2p.SubscribePeerDisconnectEvent(peerDisconnectEventCh) + subRead = p2p.SubscribePeerReadEvent(peerReadEventCh) + subWrite = p2p.SubscribePeerWriteEvent(peerWriteEventCh) + ) + defer func() { + subConnect.Unsubscribe() + subHandshake.Unsubscribe() + subDisconnect.Unsubscribe() + subRead.Unsubscribe() + subWrite.Unsubscribe() + }() + for { + select { + case event := <-peerConnectEventCh: + select { + case connectCh <- &event: + default: + log.Warn("Failed to handle connect event", "event", event) + } + case event := <-peerHandshakeEventCh: + select { + case handshakeCh <- &event: + default: + log.Warn("Failed to handle handshake event", "event", event) + } + case event := <-peerDisconnectEventCh: + select { + case disconnectCh <- &event: + default: + log.Warn("Failed to handle disconnect event", "event", event) + } + case event := <-peerReadEventCh: + select { + case readCh <- &event: + default: + log.Warn("Failed to handle read event", "event", event) + } + case event := <-peerWriteEventCh: + select { + case writeCh <- &event: + default: + log.Warn("Failed to handle write event", "event", event) + } + case <-quit: + return + } + } + }() + go db.cleanPeerHistory(quit) + + ticker := time.NewTicker(db.config.Refresh) + defer ticker.Stop() + + network := &NetworkMessage{ + Peers: make(map[string]map[string]*Peer), + } + for { + select { + case event := <-connectCh: + ip := event.IP.String() + p := getOrInitPeer(network, ip, event.ID) + if p.Location == nil { + db.peerLock.RLock() + peers := db.peerHistory.Peers + lookup := peers[ip] == nil || peers[ip][event.ID] == nil || peers[ip][event.ID].Location == nil + db.peerLock.RUnlock() + if lookup { + location := db.geodb.Lookup(event.IP) + p.Location = &PeerLocation{ + Country: location.Country.Names.English, + City: location.City.Names.English, + Latitude: location.Location.Latitude, + Longitude: location.Location.Longitude, + } + } + } + if p.Connected == nil { + p.Connected = []time.Time{event.Connected} + } else { + p.Connected = append(p.Connected, event.Connected) + } + case event := <-handshakeCh: + ip := event.IP.String() + p := getOrInitPeer(network, ip, event.DefaultID) + if p.Handshake == nil { + p.Handshake = []time.Time{event.Handshake} + } else { + p.Handshake = append(p.Handshake, event.Handshake) + } + delete(network.Peers[ip], event.DefaultID) + getOrInitPeer(network, ip, event.ID) + network.Peers[ip][event.ID] = p // interleave instead + // Remove the peer from history in case the metering was before the handshake. + db.peerLock.RLock() + stored := db.peerHistory.Peers[ip] != nil && db.peerHistory.Peers[ip][event.DefaultID] != nil + db.peerLock.RUnlock() + if stored { + db.peerLock.Lock() + hp := getOrInitPeer(db.peerHistory, ip, event.DefaultID) + delete(db.peerHistory.Peers[ip], event.DefaultID) + getOrInitPeer(db.peerHistory, ip, event.ID) + db.peerHistory.Peers[ip][event.ID] = hp // interleave instead + db.peerLock.Unlock() + } + case event := <-disconnectCh: + p := getOrInitPeer(network, event.IP.String(), event.ID) + if p.Disconnected == nil { + p.Disconnected = []time.Time{event.Disconnected} + } else { + p.Disconnected = append(p.Disconnected, event.Disconnected) + } + case event := <-readCh: + // Sum up the ingress between two updates. + p := getOrInitPeer(network, event.IP.String(), event.ID) + if len(p.Ingress) <= 0 { + p.Ingress = ChartEntries{&ChartEntry{Value: float64(event.Ingress)}} + } else { + p.Ingress[0].Value += float64(event.Ingress) + } + case event := <-writeCh: + // Sum up the egress between two updates. + p := getOrInitPeer(network, event.IP.String(), event.ID) + if len(p.Egress) <= 0 { + p.Egress = ChartEntries{&ChartEntry{Value: float64(event.Egress)}} + } else { + p.Egress[0].Value += float64(event.Egress) + } + case <-ticker.C: + now := time.Now() + db.peerLock.Lock() + for ip, peers := range network.Peers { + for id, peer := range peers { + peerHistory := getOrInitPeer(db.peerHistory, ip, id) + if peer.Location != nil { + peerHistory.Location = peer.Location + } + if peer.Connected != nil { + peerHistory.Connected = append(peerHistory.Connected, peer.Connected...) + } + if peer.Handshake != nil { + peerHistory.Handshake = append(peerHistory.Handshake, peer.Handshake...) + } + if peer.Disconnected != nil { + peerHistory.Disconnected = append(peerHistory.Disconnected, peer.Disconnected...) + } + ingress := &ChartEntry{ + Time: now, + } + if len(peer.Ingress) > 0 { + ingress.Value = peer.Ingress[0].Value + } + if peerHistory.Ingress == nil { + peer.Ingress = append(emptyChartEntries(now.Add(-db.config.Refresh), peerIngressSampleLimit-1, db.config.Refresh), ingress) + peerHistory.Ingress = peer.Ingress + //peerHistory.Ingress = ChartEntries{ingress} + } else { + peer.Ingress = ChartEntries{ingress} + peerHistory.Ingress = append(peerHistory.Ingress[1:], ingress) + //peerHistory.Ingress = append(peerHistory.Ingress, ingress) + } + egress := &ChartEntry{ + Time: now, + } + if len(peer.Egress) > 0 { + egress.Value = peer.Egress[0].Value + } + if peerHistory.Egress == nil { + peer.Egress = append(emptyChartEntries(now.Add(-db.config.Refresh), peerEgressSampleLimit-1, db.config.Refresh), egress) + peerHistory.Egress = peer.Egress + //peerHistory.Egress = ChartEntries{egress} + } else { + peer.Egress = ChartEntries{egress} + peerHistory.Egress = append(peerHistory.Egress[1:], egress) + //peerHistory.Egress = append(peerHistory.Egress, egress) + } + } + } + db.peerLock.Unlock() + db.sendToAll(&Message{Network: deepcopy.Copy(network).(*NetworkMessage)}) + + //fmt.Println() + //s, _ := json.MarshalIndent(network, "", " ") + //fmt.Println(string(s)) + + for ip, peers := range network.Peers { + for id := range peers { + peers[id] = nil + delete(peers, id) + } + delete(network.Peers, ip) + } + case errc := <-db.quit: + close(quit) + errc <- nil + return + } + } +} + +func (db *Dashboard) cleanPeerHistory(quit chan struct{}) { + cleanRate := db.config.Refresh * peerTrafficSampleLimit + for { + select { + case <-time.After(cleanRate): + // clear disconnected + validAfter := time.Now().Add(-cleanRate) + db.peerLock.Lock() + for ip, peers := range db.peerHistory.Peers { + for id, peer := range peers { + if len(peer.Disconnected) > 0 && peer.Disconnected[len(peer.Disconnected)-1].Before(validAfter) { + db.peerHistory.Peers[ip][id].Location = nil + db.peerHistory.Peers[ip][id] = nil + delete(db.peerHistory.Peers[ip], id) + } + } + if len(peers) <= 0 { + delete(db.peerHistory.Peers, ip) + } + } + var lenCount int + for _, peers := range db.peerHistory.Peers { + lenCount += len(peers) + } + if lenCount > p2p.MeteredPeerLimit { + outerLoop: + for ip, peers := range db.peerHistory.Peers { + for id, peer := range peers { + if peer.Disconnected != nil { + db.peerHistory.Peers[ip][id].Location = nil + db.peerHistory.Peers[ip][id] = nil + delete(db.peerHistory.Peers[ip], id) + lenCount-- + if lenCount <= p2p.MeteredPeerLimit { + if len(peers) <= 0 { + delete(db.peerHistory.Peers, ip) + } + break outerLoop + } + } + } + if len(peers) <= 0 { + delete(db.peerHistory.Peers, ip) + } + } + } + db.peerLock.Unlock() + case <-quit: + return + } + } +} diff --git a/p2p/metrics.go b/p2p/metrics.go index 2ab133698c..836ab560fe 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -21,12 +21,13 @@ package p2p import ( "net" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/metrics" + "sync" + "time" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/mohae/deepcopy" - "sync" "sync/atomic" - "time" + "fmt" ) const ( @@ -35,9 +36,6 @@ const ( MetricsOutboundTraffic = "p2p/OutboundTraffic" MetricsOutboundConnects = "p2p/OutboundConnects" - MetricsRegistryIngressPrefix = MetricsInboundTraffic + "/" - MetricsRegistryEgressPrefix = MetricsOutboundTraffic + "/" - MeteredPeerLimit = 16384 ) @@ -47,101 +45,79 @@ var ( egressConnectMeter = metrics.NewRegisteredMeter(MetricsOutboundConnects, nil) egressTrafficMeter = metrics.NewRegisteredMeter(MetricsOutboundTraffic, nil) - PeerIngressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, MetricsRegistryIngressPrefix) - PeerEgressRegistry = metrics.NewPrefixedChildRegistry(metrics.DefaultRegistry, MetricsRegistryEgressPrefix) - PeerTrafficMeters = newPeerTrafficMeters() - - nextDefaultID uint32 + NME = &networkMeterEvents{} ) -type PeerMetrics struct { - ID string - IP net.IP +type networkMeterEvents struct { + connectFeed event.Feed + handshakeFeed event.Feed + disconnectFeed event.Feed - // TODO: -* - Connected *time.Time - Handshake *time.Time - Disconnected *time.Time + readFeed event.Feed + writeFeed event.Feed - Ingress int64 - Egress int64 + scope event.SubscriptionScope - traffic func() (ingress, egress int64) + defaultID uint64 } -type peerTrafficMeters struct { - peers map[uint]*PeerMetrics - lock sync.RWMutex +type PeerConnectEvent struct { + IP net.IP + ID string + Connected time.Time } -func newPeerTrafficMeters() *peerTrafficMeters { - return &peerTrafficMeters{ - peers: make(map[uint]*PeerMetrics), - } +type PeerHandshakeEvent struct { + IP net.IP + DefaultID string + ID string + Handshake time.Time } -func (m *peerTrafficMeters) register(id uint, ip net.IP, traffic func() (ingress, egress int64)) error { - now := time.Now() - peer := &PeerMetrics{ - IP: ip, - Connected: &now, - traffic: traffic, - } - m.lock.Lock() - m.peers[id] = peer - m.lock.Unlock() - - return nil +type PeerDisconnectEvent struct { + IP net.IP + ID string + Disconnected time.Time } -func (m *peerTrafficMeters) handshakeDone(id uint, peerID string, traffic func() (ingress, egress int64)) { - now := time.Now() - m.lock.Lock() - if peer, ok := m.peers[id]; ok { - peer.Handshake = &now - peer.ID = peerID - peer.traffic = traffic - } - m.lock.Unlock() +type PeerReadEvent struct { + IP net.IP + ID string + Ingress int } -func (m *peerTrafficMeters) close(id uint) { - now := time.Now() - m.lock.Lock() - m.peers[id].Disconnected = &now - m.lock.Unlock() +type PeerWriteEvent struct { + IP net.IP + ID string + Egress int } -func (m *peerTrafficMeters) Peers() map[uint]*PeerMetrics { - peers := make(map[uint]*PeerMetrics) - m.lock.Lock() - for id, peer := range m.peers { - peer.Ingress, peer.Egress = peer.traffic() - peers[id] = deepcopy.Copy(peer).(*PeerMetrics) - if peer.Disconnected != nil { - PeerIngressRegistry.Unregister(peer.ID) - PeerEgressRegistry.Unregister(peer.ID) - delete(m.peers, id) - } - } - m.lock.Unlock() - return peers +func SubscribePeerConnectEvent(ch chan<- PeerConnectEvent) event.Subscription { + return NME.scope.Track(NME.connectFeed.Subscribe(ch)) +} +func SubscribePeerHandshakeEvent(ch chan<- PeerHandshakeEvent) event.Subscription { + return NME.scope.Track(NME.handshakeFeed.Subscribe(ch)) +} +func SubscribePeerDisconnectEvent(ch chan<- PeerDisconnectEvent) event.Subscription { + return NME.scope.Track(NME.disconnectFeed.Subscribe(ch)) +} +func SubscribePeerReadEvent(ch chan<- PeerReadEvent) event.Subscription { + return NME.scope.Track(NME.readFeed.Subscribe(ch)) +} +func SubscribePeerWriteEvent(ch chan<- PeerWriteEvent) event.Subscription { + return NME.scope.Track(NME.writeFeed.Subscribe(ch)) } -type networkMeter struct { - ingress metrics.Meter - egress metrics.Meter +func closeNME() { + NME.scope.Close() } // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { net.Conn // Network connection to wrap with metering - id uint - meter *networkMeter - - ingressBeforeHandshake int64 - egressBeforeHandshake int64 + ip net.IP + id string lock sync.RWMutex } @@ -154,8 +130,8 @@ func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn { if !metrics.Enabled { return conn } - if len(PeerTrafficMeters.peers) >= MeteredPeerLimit { - log.Warn("Metered peer limit exceeded") + if ip.IsUnspecified() { + log.Warn("peer IP is unspecified") return conn } // Otherwise bump the connection counters and wrap the connection @@ -164,15 +140,17 @@ func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn { } else { egressConnectMeter.Mark(1) } - id := uint(atomic.AddUint32(&nextDefaultID, 1)) - c := &meteredConn{ + id := fmt.Sprintf("peer_%d", atomic.AddUint64(&NME.defaultID, 1)) + NME.connectFeed.Send(PeerConnectEvent{ + IP: ip, + ID: id, + Connected: time.Now(), + }) + return &meteredConn{ Conn: conn, + ip: ip, id: id, } - PeerTrafficMeters.register(id, ip, func() (ingress, egress int64) { - return atomic.LoadInt64(&c.ingressBeforeHandshake), atomic.LoadInt64(&c.egressBeforeHandshake) - }) - return c } // Read delegates a network read to the underlying connection, bumping the ingress @@ -181,12 +159,13 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) ingressTrafficMeter.Mark(int64(n)) c.lock.RLock() - if c.meter == nil { - atomic.AddInt64(&c.ingressBeforeHandshake, int64(n)) - } else { - c.meter.ingress.Mark(int64(n)) - } + id := c.id c.lock.RUnlock() + NME.readFeed.Send(PeerReadEvent{ + IP: c.ip, + ID: id, + Ingress: n, + }) return n, err } @@ -196,42 +175,37 @@ func (c *meteredConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) egressTrafficMeter.Mark(int64(n)) c.lock.RLock() - if c.meter == nil { - atomic.AddInt64(&c.egressBeforeHandshake, int64(n)) - } else { - c.meter.egress.Mark(int64(n)) - } + id := c.id c.lock.RUnlock() + NME.writeFeed.Send(PeerWriteEvent{ + IP: c.ip, + ID: id, + Egress: n, + }) return n, err } func (c *meteredConn) Close() error { - PeerTrafficMeters.close(c.id) + c.lock.RLock() + id := c.id + c.lock.RUnlock() + NME.disconnectFeed.Send(PeerDisconnectEvent{ + IP: c.ip, + ID: id, + Disconnected: time.Now(), + }) return c.Conn.Close() } func (c *meteredConn) handshakeDone(peerID string) { - m := &networkMeter{ - ingress: metrics.NewRegisteredMeter(peerID, PeerIngressRegistry), - egress: metrics.NewRegisteredMeter(peerID, PeerEgressRegistry), - } c.lock.Lock() - m.ingress.Mark(atomic.LoadInt64(&c.ingressBeforeHandshake)) - m.egress.Mark(atomic.LoadInt64(&c.egressBeforeHandshake)) - c.meter = m + defaultID := c.id + c.id = peerID c.lock.Unlock() - - ingressMeter, oki := PeerIngressRegistry.Get(peerID).(metrics.Meter) - egressMeter, oke := PeerEgressRegistry.Get(peerID).(metrics.Meter) - traffic := func() (ingress, egress int64) { - return 0, 0 - } - if oki && oke { - traffic = func() (ingress, egress int64) { - return ingressMeter.Count(), egressMeter.Count() - } - } else { - log.Warn("Failed to get traffic meter", "peerID", peerID) - } - PeerTrafficMeters.handshakeDone(c.id, peerID, traffic) + NME.handshakeFeed.Send(PeerHandshakeEvent{ + IP: c.ip, + DefaultID: defaultID, + ID: peerID, + Handshake: time.Now(), + }) } diff --git a/p2p/server.go b/p2p/server.go index c5087710f8..11fd9b6dfc 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -388,6 +388,7 @@ func (srv *Server) Stop() { close(srv.quit) srv.lock.Unlock() srv.loopWG.Wait() + closeNME() } // sharedUDPConn implements a shared connection. Write sends messages to the underlying connection while read returns From 278efd11729cabb32573d5f19b40f51a3caca3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Wed, 8 Aug 2018 16:26:07 +0300 Subject: [PATCH 6/7] dashboard, p2p, vendor: code polishing and documentation --- dashboard/assets/components/Dashboard.jsx | 4 +- dashboard/assets/components/Network.jsx | 56 +- dashboard/assets/types/content.jsx | 11 +- dashboard/dashboard.go | 205 +---- dashboard/geoip.go | 8 +- dashboard/log.go | 42 +- dashboard/message.go | 32 +- dashboard/peers.go | 207 +++-- dashboard/system.go | 155 ++++ p2p/metrics.go | 127 +-- p2p/server.go | 8 +- vendor/github.com/apilayer/freegeoip/AUTHORS | 11 + .../apilayer/freegeoip/CONTRIBUTORS | 22 + .../github.com/apilayer/freegeoip/Dockerfile | 25 + .../github.com/apilayer/freegeoip/HISTORY.md | 55 ++ vendor/github.com/apilayer/freegeoip/LICENSE | 27 + vendor/github.com/apilayer/freegeoip/Procfile | 1 + .../github.com/apilayer/freegeoip/README.md | 259 +++++++ vendor/github.com/apilayer/freegeoip/app.json | 7 + vendor/github.com/apilayer/freegeoip/db.go | 453 +++++++++++ vendor/github.com/apilayer/freegeoip/doc.go | 14 + .../apilayer/freegeoip/freegeo-warning.png | Bin 0 -> 14752 bytes .../oschwald/maxminddb-golang/LICENSE | 15 + .../oschwald/maxminddb-golang/README.md | 38 + .../oschwald/maxminddb-golang/appveyor.yml | 19 + .../oschwald/maxminddb-golang/decoder.go | 721 ++++++++++++++++++ .../oschwald/maxminddb-golang/errors.go | 42 + .../oschwald/maxminddb-golang/mmap_unix.go | 15 + .../oschwald/maxminddb-golang/mmap_windows.go | 85 +++ .../oschwald/maxminddb-golang/reader.go | 259 +++++++ .../maxminddb-golang/reader_appengine.go | 28 + .../oschwald/maxminddb-golang/reader_other.go | 63 ++ .../oschwald/maxminddb-golang/traverse.go | 108 +++ .../oschwald/maxminddb-golang/verifier.go | 185 +++++ vendor/vendor.json | 12 + 35 files changed, 2955 insertions(+), 364 deletions(-) create mode 100644 dashboard/system.go create mode 100644 vendor/github.com/apilayer/freegeoip/AUTHORS create mode 100644 vendor/github.com/apilayer/freegeoip/CONTRIBUTORS create mode 100644 vendor/github.com/apilayer/freegeoip/Dockerfile create mode 100644 vendor/github.com/apilayer/freegeoip/HISTORY.md create mode 100644 vendor/github.com/apilayer/freegeoip/LICENSE create mode 100644 vendor/github.com/apilayer/freegeoip/Procfile create mode 100644 vendor/github.com/apilayer/freegeoip/README.md create mode 100644 vendor/github.com/apilayer/freegeoip/app.json create mode 100644 vendor/github.com/apilayer/freegeoip/db.go create mode 100644 vendor/github.com/apilayer/freegeoip/doc.go create mode 100644 vendor/github.com/apilayer/freegeoip/freegeo-warning.png create mode 100644 vendor/github.com/oschwald/maxminddb-golang/LICENSE create mode 100644 vendor/github.com/oschwald/maxminddb-golang/README.md create mode 100644 vendor/github.com/oschwald/maxminddb-golang/appveyor.yml create mode 100644 vendor/github.com/oschwald/maxminddb-golang/decoder.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/errors.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/mmap_unix.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/mmap_windows.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/reader.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/reader_appengine.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/reader_other.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/traverse.go create mode 100644 vendor/github.com/oschwald/maxminddb-golang/verifier.go diff --git a/dashboard/assets/components/Dashboard.jsx b/dashboard/assets/components/Dashboard.jsx index c11c40c195..b2c262f0a1 100644 --- a/dashboard/assets/components/Dashboard.jsx +++ b/dashboard/assets/components/Dashboard.jsx @@ -90,7 +90,7 @@ const defaultContent: () => Content = () => ({ chain: {}, txpool: {}, network: { - peers: {}, + peerBundles: {}, }, system: { activeMemory: [], @@ -123,7 +123,7 @@ const updaters = { chain: null, txpool: null, network: { - peers: peerInserter, + peerBundles: peerInserter, }, system: { activeMemory: appender(200), diff --git a/dashboard/assets/components/Network.jsx b/dashboard/assets/components/Network.jsx index aad95403f6..9e1d212258 100644 --- a/dashboard/assets/components/Network.jsx +++ b/dashboard/assets/components/Network.jsx @@ -19,26 +19,37 @@ import React, {Component} from 'react'; import Table, {TableHead, TableBody, TableRow, TableCell} from 'material-ui/Table'; -import type {Network as NetworkType, Peer} from '../types/content'; +import type {Network as NetworkType, PeerBundle, Peer} from '../types/content'; // inserter is a state updater function for the main component, which inserts the new log chunk into the chunk array. // limit is the maximum length of the chunk array, used in order to prevent the browser from OOM. -export const inserter = (update: {[string]: {[string]: Peer}}, prev: {[string]: {[string]: Peer}}) => { +export const inserter = (update: {[string]: PeerBundle}, prev: {[string]: PeerBundle}) => { Object.keys(update).forEach((ip) => { + if (!update[ip]) { + return; + } if (!prev[ip]) { prev[ip] = update[ip]; return; } - if (!update[ip]) { + if (update[ip].location) { + prev[ip].location = update[ip].location; + } + if (!update[ip].peers) { return; } - Object.keys(update[ip]).forEach((id) => { - if (!prev[ip][id]) { - prev[ip][id] = update[ip][id]; + Object.entries(update[ip].peers).forEach(([id, u]) => { + if (!prev[ip].peers[id]) { + prev[ip].peers[id] = u; return; } - const u: Peer = update[ip][id]; - const p: Peer = prev[ip][id]; + // If the handshake was between two metering + if (u.defaultID && prev[ip].peers[u.defaultID]) { + // TODO (kurkomisi): merge the two in order to keep the previous connection. + prev[ip].peers[id] = prev[ip].peers[u.defaultID]; + delete prev[ip].peers[u.defaultID]; + } + const p: Peer = prev[ip].peers[id]; if (u.connected) { if (!Array.isArray(p.connected)) { p.connected = []; @@ -69,7 +80,7 @@ export const inserter = (update: {[string]: {[string]: Peer}}, prev: {[string]: } p.egress = [...p.egress, ...u.egress].slice(-200); } - prev[ip][id] = p; + prev[ip].peers[id] = p; }); }); return prev; @@ -105,8 +116,8 @@ class Network extends Component { IP - Peer ID Location + Peer ID Ingress Egress Connected @@ -115,35 +126,32 @@ class Network extends Component { - {Object.entries(this.props.content.peers).map(([ip, peers]) => ( + {Object.entries(this.props.content.peerBundles).map(([ip, bundle]) => ( {ip} - {Object.keys(peers).map(id => id.substring(0, 10)).join(' ')} + {bundle.location ? (() => { + const l = bundle.location; + return `${l.country ? l.country : ''}${l.city ? `/${l.city}` : ''} ${l.latitude} ${l.longitude}`; + })() : ''} - {(() => { - const k = Object.keys(peers)[0]; - return k && peers[k].location ? (() => { - const l = peers[k].location; - return `${l.country}${l.city ? `/${l.city}` : ''} ${l.latitude} ${l.longitude}`; - })() : ''; - })()} + {Object.keys(bundle.peers).map(id => id.substring(0, 10)).join(' ')} - {Object.keys(peers).map((id) => peers[id].ingress && peers[id].ingress.map(sample => sample.value).join(' ')).join(', ')} + {Object.values(bundle.peers).map(peer => peer.ingress && peer.ingress.map(sample => sample.value).join(' ')).join(', ')} - {Object.keys(peers).map((id) => peers[id].egress && peers[id].egress.map(sample => sample.value).join(' ')).join(', ')} + {Object.values(bundle.peers).map(peer => peer.egress && peer.egress.map(sample => sample.value).join(' ')).join(', ')} - {Object.keys(peers).map((id) => peers[id].connected && peers[id].connected.map(time => this.formatTime(time)).join(' ')).join(', ')} + {Object.values(bundle.peers).map(peer => peer.connected && peer.connected.map(time => this.formatTime(time)).join(' ')).join(', ')} - {Object.keys(peers).map((id) => peers[id].handshake && peers[id].handshake.map(time => this.formatTime(time)).join(' ')).join(', ')} + {Object.values(bundle.peers).map(peer => peer.handshake && peer.handshake.map(time => this.formatTime(time)).join(' ')).join(', ')} - {Object.keys(peers).map((id) => peers[id].disconnected && peers[id].disconnected.map(time => this.formatTime(time)).join(' ')).join(', ')} + {Object.values(bundle.peers).map(peer => peer.disconnected && peer.disconnected.map(time => this.formatTime(time)).join(' ')).join(', ')} ))} diff --git a/dashboard/assets/types/content.jsx b/dashboard/assets/types/content.jsx index 94f70537a7..a8324dbca5 100644 --- a/dashboard/assets/types/content.jsx +++ b/dashboard/assets/types/content.jsx @@ -51,19 +51,24 @@ export type TxPool = { }; export type Network = { - peers: {[string]: {[string]: Peer}}, + peerBundles: {[string]: PeerBundle}, +}; + +export type PeerBundle = { + location: GeoLocation, + peers: {[string]: Peer}, }; export type Peer = { - location: PeerLocation, connected: Array, handshake: Array, disconnected: Array, ingress: ChartEntries, egress: ChartEntries, + defaultID: string, }; -export type PeerLocation = { +export type GeoLocation = { country: string, city: string, latitude: number, diff --git a/dashboard/dashboard.go b/dashboard/dashboard.go index dbd44b0620..f33b233f36 100644 --- a/dashboard/dashboard.go +++ b/dashboard/dashboard.go @@ -27,16 +27,13 @@ import ( "fmt" "net" "net/http" - "runtime" "sync" "sync/atomic" "time" "io" - "github.com/elastic/gosigar" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -54,31 +51,35 @@ const ( diskReadSampleLimit = 200 // Maximum number of disk read data samples diskWriteSampleLimit = 200 // Maximum number of disk write data samples - peerTrafficSampleLimit = 200 - peerIngressSampleLimit = peerTrafficSampleLimit - peerEgressSampleLimit = peerTrafficSampleLimit + peerLimit = 1000 // Maximum number of metered peers + peerTrafficSampleLimit = 200 // Maximum number of traffic data samples for a peer + peerIngressSampleLimit = peerTrafficSampleLimit // Maximum number of ingress data samples for a peer + peerEgressSampleLimit = peerTrafficSampleLimit // Maximum number of egress data samples for a peer ) -var nextID uint32 // Next connection id - // Dashboard contains the dashboard internals. type Dashboard struct { - config *Config + config *Config // Configuration values for the dashboard + + listener net.Listener // Network listener listening for dashboard clients + conns map[uint32]*client // Currently live websocket connections + nextConnID uint32 // Next connection id + + history *Message // Stored general data + sysHistory *SystemMessage // Stored system data + networkHistory *NetworkMessage // Stored peer data + logHistory *LogsMessage // Stored log data - listener net.Listener - conns map[uint32]*client // Currently live websocket connections - history *Message - peerHistory *NetworkMessage lock sync.RWMutex // Lock protecting the dashboard's internals - peerLock sync.RWMutex + sysLock sync.RWMutex // Lock protecting the stored system data + peerLock sync.RWMutex // Lock protecting the stored peer data + logLock sync.RWMutex // Lock protecting the stored log data - geodb *GeoDB - peersByIP map[string]*Peer - peersByID map[string]*Peer - logdir string + geodb *GeoDB // geoip database instance for IP to geographical information conversions + logdir string // Directory containing the log files quit chan chan error // Channel used for graceful exit - wg sync.WaitGroup + wg sync.WaitGroup // Wait group used to close the data collector threads } // client represents active websocket connection with a remote browser. @@ -104,19 +105,19 @@ func New(config *Config, commit string, logdir string) *Dashboard { Commit: commit, Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta), }, - System: &SystemMessage{ - ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh), - VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh), - NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh), - NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh), - ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh), - SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh), - DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), - DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), - }, }, - peerHistory: &NetworkMessage{ - Peers: make(map[string]map[string]*Peer), + sysHistory: &SystemMessage{ + ActiveMemory: emptyChartEntries(now, activeMemorySampleLimit, config.Refresh), + VirtualMemory: emptyChartEntries(now, virtualMemorySampleLimit, config.Refresh), + NetworkIngress: emptyChartEntries(now, networkIngressSampleLimit, config.Refresh), + NetworkEgress: emptyChartEntries(now, networkEgressSampleLimit, config.Refresh), + ProcessCPU: emptyChartEntries(now, processCPUSampleLimit, config.Refresh), + SystemCPU: emptyChartEntries(now, systemCPUSampleLimit, config.Refresh), + DiskRead: emptyChartEntries(now, diskReadSampleLimit, config.Refresh), + DiskWrite: emptyChartEntries(now, diskWriteSampleLimit, config.Refresh), + }, + networkHistory: &NetworkMessage{ + PeerBundles: make(map[string]*PeerBundle), }, logdir: logdir, } @@ -145,7 +146,7 @@ func (db *Dashboard) Start(server *p2p.Server) error { log.Info("Starting dashboard") db.wg.Add(3) - go db.collectData() + go db.collectSystemData() go db.streamLogs() go db.collectPeerData() @@ -219,7 +220,7 @@ func (db *Dashboard) webHandler(w http.ResponseWriter, r *http.Request) { // apiHandler handles requests for the dashboard. func (db *Dashboard) apiHandler(conn *websocket.Conn) { - id := atomic.AddUint32(&nextID, 1) + id := atomic.AddUint32(&db.nextConnID, 1) client := &client{ conn: conn, msg: make(chan *Message, 128), @@ -250,9 +251,15 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) { db.lock.RLock() h := deepcopy.Copy(db.history).(*Message) db.lock.RUnlock() + db.sysLock.RLock() + h.System = deepcopy.Copy(db.sysHistory).(*SystemMessage) + db.sysLock.RUnlock() db.peerLock.RLock() - h.Network = deepcopy.Copy(db.peerHistory).(*NetworkMessage) + h.Network = deepcopy.Copy(db.networkHistory).(*NetworkMessage) db.peerLock.RUnlock() + db.logLock.RLock() + h.Logs = deepcopy.Copy(db.logHistory).(*LogsMessage) + db.logLock.RUnlock() client.msg <- h // Start tracking the connection and drop at connection loss. @@ -279,136 +286,6 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) { } } -// meterCollector returns a function, which retrieves a specific meter. -func meterCollector(name string) func() int64 { - if meter := metrics.Get(name); meter != nil { - m := meter.(metrics.Meter) - return func() int64 { - return m.Count() - } - } - return func() int64 { - return 0 - } -} - -// collectData collects the required data to plot on the dashboard. -func (db *Dashboard) collectData() { - defer db.wg.Done() - - systemCPUUsage := gosigar.Cpu{} - systemCPUUsage.Get() - var ( - mem runtime.MemStats - - collectNetworkIngress = meterCollector(p2p.MetricsInboundTraffic) - collectNetworkEgress = meterCollector(p2p.MetricsOutboundTraffic) - collectDiskRead = meterCollector("eth/db/chaindata/disk/read") - collectDiskWrite = meterCollector("eth/db/chaindata/disk/write") - - prevNetworkIngress = collectNetworkIngress() - prevNetworkEgress = collectNetworkEgress() - prevProcessCPUTime = getProcessCPUTime() - prevSystemCPUUsage = systemCPUUsage - prevDiskRead = collectDiskRead() - prevDiskWrite = collectDiskWrite() - - frequency = float64(db.config.Refresh / time.Second) - numCPU = float64(runtime.NumCPU()) - ) - - for { - select { - case errc := <-db.quit: - errc <- nil - return - case <-time.After(db.config.Refresh): - systemCPUUsage.Get() - var ( - curNetworkIngress = collectNetworkIngress() - curNetworkEgress = collectNetworkEgress() - curProcessCPUTime = getProcessCPUTime() - curSystemCPUUsage = systemCPUUsage - curDiskRead = collectDiskRead() - curDiskWrite = collectDiskWrite() - - deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress) - deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress) - deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime - deltaSystemCPUUsage = curSystemCPUUsage.Delta(prevSystemCPUUsage) - deltaDiskRead = curDiskRead - prevDiskRead - deltaDiskWrite = curDiskWrite - prevDiskWrite - ) - prevNetworkIngress = curNetworkIngress - prevNetworkEgress = curNetworkEgress - prevProcessCPUTime = curProcessCPUTime - prevSystemCPUUsage = curSystemCPUUsage - prevDiskRead = curDiskRead - prevDiskWrite = curDiskWrite - - now := time.Now() - - runtime.ReadMemStats(&mem) - activeMemory := &ChartEntry{ - Time: now, - Value: float64(mem.Alloc) / frequency, - } - virtualMemory := &ChartEntry{ - Time: now, - Value: float64(mem.Sys) / frequency, - } - networkIngress := &ChartEntry{ - Time: now, - Value: deltaNetworkIngress / frequency, - } - networkEgress := &ChartEntry{ - Time: now, - Value: deltaNetworkEgress / frequency, - } - processCPU := &ChartEntry{ - Time: now, - Value: deltaProcessCPUTime / frequency / numCPU * 100, - } - systemCPU := &ChartEntry{ - Time: now, - Value: float64(deltaSystemCPUUsage.Sys+deltaSystemCPUUsage.User) / frequency / numCPU, - } - diskRead := &ChartEntry{ - Time: now, - Value: float64(deltaDiskRead) / frequency, - } - diskWrite := &ChartEntry{ - Time: now, - Value: float64(deltaDiskWrite) / frequency, - } - sys := db.history.System - db.lock.Lock() - sys.ActiveMemory = append(sys.ActiveMemory[1:], activeMemory) - sys.VirtualMemory = append(sys.VirtualMemory[1:], virtualMemory) - sys.NetworkIngress = append(sys.NetworkIngress[1:], networkIngress) - sys.NetworkEgress = append(sys.NetworkEgress[1:], networkEgress) - sys.ProcessCPU = append(sys.ProcessCPU[1:], processCPU) - sys.SystemCPU = append(sys.SystemCPU[1:], systemCPU) - sys.DiskRead = append(sys.DiskRead[1:], diskRead) - sys.DiskWrite = append(sys.DiskWrite[1:], diskWrite) - db.lock.Unlock() - - db.sendToAll(&Message{ - System: &SystemMessage{ - ActiveMemory: ChartEntries{activeMemory}, - VirtualMemory: ChartEntries{virtualMemory}, - NetworkIngress: ChartEntries{networkIngress}, - NetworkEgress: ChartEntries{networkEgress}, - ProcessCPU: ChartEntries{processCPU}, - SystemCPU: ChartEntries{systemCPU}, - DiskRead: ChartEntries{diskRead}, - DiskWrite: ChartEntries{diskWrite}, - }, - }) - } - } -} - // sendToAll sends the given message to the active dashboards. func (db *Dashboard) sendToAll(msg *Message) { db.lock.Lock() diff --git a/dashboard/geoip.go b/dashboard/geoip.go index cc702dac90..ac6036e51f 100644 --- a/dashboard/geoip.go +++ b/dashboard/geoip.go @@ -17,11 +17,11 @@ package dashboard import ( - "github.com/apilayer/freegeoip" - "time" "net" + "time" + + "github.com/apilayer/freegeoip" ) -// Package geoip contains utility methods for converting IPs to geographical data. // GeoDBInfo contains all the geographical information we could extract based on an IP // address. @@ -48,7 +48,7 @@ type GeoDB struct { geodb *freegeoip.DB } -// Open creats a new geoip database with an up-to-date database from the internet. +// Open creates a new geoip database with an up-to-date database from the internet. func OpenGeoDB() (*GeoDB, error) { // Initiate a geoip database to cross reference locations db, err := freegeoip.OpenURL(freegeoip.MaxMindDB, 24*time.Hour, time.Hour) diff --git a/dashboard/log.go b/dashboard/log.go index 5d852d60a4..be245d5424 100644 --- a/dashboard/log.go +++ b/dashboard/log.go @@ -94,13 +94,13 @@ func (db *Dashboard) handleLogRequest(r *LogsRequest, c *client) { // The last file is continuously updated, and its chunks are streamed, // so in order to avoid log record duplication on the client side, it is // handled differently. Its actual content is always saved in the history. - db.lock.Lock() - if db.history.Logs != nil { + db.logLock.RLock() + if db.logHistory != nil { c.msg <- &Message{ - Logs: db.history.Logs, + Logs: deepcopy.Copy(db.logHistory).(*LogsMessage), } } - db.lock.Unlock() + db.logLock.RUnlock() return case fileNames[idx] == r.Name: idx++ @@ -174,15 +174,15 @@ func (db *Dashboard) streamLogs() { log.Warn("Problem with file", "name", opened.Name(), "err", err) return } - db.lock.Lock() - db.history.Logs = &LogsMessage{ + db.logLock.Lock() + db.logHistory = &LogsMessage{ Source: &LogFile{ Name: fi.Name(), Last: true, }, Chunk: emptyChunk, } - db.lock.Unlock() + db.logLock.Unlock() watcher := make(chan notify.EventInfo, 10) if err := notify.Watch(db.logdir, watcher, notify.Create); err != nil { @@ -240,10 +240,10 @@ loop: log.Warn("Problem with file", "name", opened.Name(), "err", err) break loop } - db.lock.Lock() - db.history.Logs.Source.Name = fi.Name() - db.history.Logs.Chunk = emptyChunk - db.lock.Unlock() + db.logLock.Lock() + db.logHistory.Source.Name = fi.Name() + db.logHistory.Chunk = emptyChunk + db.logLock.Unlock() case <-ticker.C: // Send log updates to the client. if opened == nil { log.Warn("The last log file is not opened") @@ -266,19 +266,19 @@ loop: var l *LogsMessage // Update the history. - db.lock.Lock() - if bytes.Equal(db.history.Logs.Chunk, emptyChunk) { - db.history.Logs.Chunk = chunk - l = deepcopy.Copy(db.history.Logs).(*LogsMessage) + db.logLock.Lock() + if bytes.Equal(db.logHistory.Chunk, emptyChunk) { + db.logHistory.Chunk = chunk + l = deepcopy.Copy(db.logHistory).(*LogsMessage) } else { - b = make([]byte, len(db.history.Logs.Chunk)+len(chunk)-1) - copy(b, db.history.Logs.Chunk) - b[len(db.history.Logs.Chunk)-1] = ',' - copy(b[len(db.history.Logs.Chunk):], chunk[1:]) - db.history.Logs.Chunk = b + b = make([]byte, len(db.logHistory.Chunk)+len(chunk)-1) + copy(b, db.logHistory.Chunk) + b[len(db.logHistory.Chunk)-1] = ',' + copy(b[len(db.logHistory.Chunk):], chunk[1:]) + db.logHistory.Chunk = b l = &LogsMessage{Chunk: chunk} } - db.lock.Unlock() + db.logLock.Unlock() db.sendToAll(&Message{Logs: l}) case errc = <-db.quit: diff --git a/dashboard/message.go b/dashboard/message.go index cb9bd0cc8b..4e047025b1 100644 --- a/dashboard/message.go +++ b/dashboard/message.go @@ -55,27 +55,38 @@ type TxPoolMessage struct { /* TODO (kurkomisi) */ } -// k1: IP, k2: ID +// NetworkMessage contains information about the peers organized based on the IP address. type NetworkMessage struct { - Peers map[string]map[string]*Peer `json:"peers,omitempty"` + PeerBundles map[string]*PeerBundle `json:"peerBundles,omitempty"` } -type Peer struct { - Location *PeerLocation `json:"location,omitempty"` - Connected []time.Time `json:"connected,omitempty"` - Handshake []time.Time `json:"handshake,omitempty"` - Disconnected []time.Time `json:"disconnected,omitempty"` - Ingress ChartEntries `json:"ingress,omitempty"` - Egress ChartEntries `json:"egress,omitempty"` +// PeerBundle contains information about the peers pertaining to an IP address. +type PeerBundle struct { + Location *GeoLocation `json:"location,omitempty"` // geographical information based on IP + Peers map[string]*Peer `json:"peers,omitempty"` // the peers' node id is used as key } -type PeerLocation struct { +// GeoLocation contains geographical information. +type GeoLocation struct { Country string `json:"country,omitempty"` City string `json:"city,omitempty"` Latitude float64 `json:"latitude,omitempty"` Longitude float64 `json:"longitude,omitempty"` } +// Peer contains lifecycle timestamps and traffic information of a given peer. +type Peer struct { + Connected []time.Time `json:"connected,omitempty"` + Handshake []time.Time `json:"handshake,omitempty"` + Disconnected []time.Time `json:"disconnected,omitempty"` + + Ingress ChartEntries `json:"ingress,omitempty"` + Egress ChartEntries `json:"egress,omitempty"` + + DefaultID string `json:"defaultID,omitempty"` +} + +// SystemMessage contains the metered system data samples. type SystemMessage struct { ActiveMemory ChartEntries `json:"activeMemory,omitempty"` VirtualMemory ChartEntries `json:"virtualMemory,omitempty"` @@ -104,6 +115,7 @@ type Request struct { Logs *LogsRequest `json:"logs,omitempty"` } +// LogsRequest contains the attributes of the log file the client wants to receive. type LogsRequest struct { Name string `json:"name"` // The request handler searches for log file based on this file name. Past bool `json:"past"` // Denotes whether the client wants the previous or the next file. diff --git a/dashboard/peers.go b/dashboard/peers.go index 06d80a0704..fb7445426e 100644 --- a/dashboard/peers.go +++ b/dashboard/peers.go @@ -1,27 +1,57 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + package dashboard import ( + "time" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" - "time" "github.com/mohae/deepcopy" ) -const eventBufferLimit = 128 +const eventBufferLimit = 128 // Maximum number of buffered peer events for each event type -func getOrInitPeer(m *NetworkMessage, ip, id string) *Peer { - if _, ok := m.Peers[ip]; !ok { - m.Peers[ip] = make(map[string]*Peer) +// getOrInitBundle returns the peer bundle belonging to the given IP, or +// initializes the bundle if it doesn't exist. +func getOrInitBundle(m *NetworkMessage, ip string) *PeerBundle { + if _, ok := m.PeerBundles[ip]; !ok { + m.PeerBundles[ip] = &PeerBundle{ + Peers: make(map[string]*Peer), + } } - if _, ok := m.Peers[ip][id]; !ok { - m.Peers[ip][id] = new(Peer) - } - return m.Peers[ip][id] + return m.PeerBundles[ip] } +// getOrInitPeer returns the peer belonging to the given IP and node id, or +// initializes the peer if it doesn't exist. +func getOrInitPeer(m *NetworkMessage, ip, id string) *Peer { + b := getOrInitBundle(m, ip) + if _, ok := b.Peers[id]; !ok { + b.Peers[id] = new(Peer) + } + return b.Peers[id] +} + +// collectPeerData gathers data about the peers and sends it to the clients. func (db *Dashboard) collectPeerData() { defer db.wg.Done() + // Open the geodb database for IP to geographical information conversions. var err error db.geodb, err = OpenGeoDB() if err != nil { @@ -31,7 +61,9 @@ func (db *Dashboard) collectPeerData() { defer db.geodb.Close() var ( - quit = make(chan struct{}) + quit = make(chan struct{}) + + // Channels used for avoiding the blocking of the event feeds. connectCh = make(chan *p2p.PeerConnectEvent, eventBufferLimit) handshakeCh = make(chan *p2p.PeerHandshakeEvent, eventBufferLimit) disconnectCh = make(chan *p2p.PeerDisconnectEvent, eventBufferLimit) @@ -40,12 +72,14 @@ func (db *Dashboard) collectPeerData() { ) go func() { var ( + // Peer event channels. peerConnectEventCh = make(chan p2p.PeerConnectEvent, eventBufferLimit) peerHandshakeEventCh = make(chan p2p.PeerHandshakeEvent, eventBufferLimit) peerDisconnectEventCh = make(chan p2p.PeerDisconnectEvent, eventBufferLimit) peerReadEventCh = make(chan p2p.PeerReadEvent, eventBufferLimit) peerWriteEventCh = make(chan p2p.PeerWriteEvent, eventBufferLimit) + // Subscribe to peer events. subConnect = p2p.SubscribePeerConnectEvent(peerConnectEventCh) subHandshake = p2p.SubscribePeerHandshakeEvent(peerHandshakeEventCh) subDisconnect = p2p.SubscribePeerDisconnectEvent(peerDisconnectEventCh) @@ -53,70 +87,87 @@ func (db *Dashboard) collectPeerData() { subWrite = p2p.SubscribePeerWriteEvent(peerWriteEventCh) ) defer func() { + // Unsubscribe at the end. subConnect.Unsubscribe() subHandshake.Unsubscribe() subDisconnect.Unsubscribe() subRead.Unsubscribe() subWrite.Unsubscribe() }() + // Waiting for peer events. for { select { case event := <-peerConnectEventCh: select { case connectCh <- &event: default: - log.Warn("Failed to handle connect event", "event", event) + log.Warn("Failed to handle peer connect event", "event", event) } case event := <-peerHandshakeEventCh: select { case handshakeCh <- &event: default: - log.Warn("Failed to handle handshake event", "event", event) + log.Warn("Failed to handle peer handshake event", "event", event) } case event := <-peerDisconnectEventCh: select { case disconnectCh <- &event: default: - log.Warn("Failed to handle disconnect event", "event", event) + log.Warn("Failed to handle peer disconnect event", "event", event) } case event := <-peerReadEventCh: select { case readCh <- &event: default: - log.Warn("Failed to handle read event", "event", event) + log.Warn("Failed to handle peer read event", "event", event) } case event := <-peerWriteEventCh: select { case writeCh <- &event: default: - log.Warn("Failed to handle write event", "event", event) + log.Warn("Failed to handle peer write event", "event", event) } + case err := <-subConnect.Err(): + log.Warn("Peer connect subscription error", "err", err) + return + case err := <-subHandshake.Err(): + log.Warn("Peer handshake subscription error", "err", err) + return + case err := <-subDisconnect.Err(): + log.Warn("Peer disconnect subscription error", "err", err) + return + case err := <-subRead.Err(): + log.Warn("Peer read subscription error", "err", err) + return + case err := <-subWrite.Err(): + log.Warn("Peer write subscription error", "err", err) + return case <-quit: return } } }() - go db.cleanPeerHistory(quit) + go db.keepPeerHistoryClean(quit) ticker := time.NewTicker(db.config.Refresh) defer ticker.Stop() - network := &NetworkMessage{ - Peers: make(map[string]map[string]*Peer), + // Listen for events, and prepare the difference between two metering. + diff := &NetworkMessage{ + PeerBundles: make(map[string]*PeerBundle), } for { select { case event := <-connectCh: ip := event.IP.String() - p := getOrInitPeer(network, ip, event.ID) - if p.Location == nil { + p := getOrInitPeer(diff, ip, event.ID) + if diff.PeerBundles[ip].Location == nil { db.peerLock.RLock() - peers := db.peerHistory.Peers - lookup := peers[ip] == nil || peers[ip][event.ID] == nil || peers[ip][event.ID].Location == nil + lookup := db.networkHistory.PeerBundles[ip] == nil || db.networkHistory.PeerBundles[ip].Location == nil db.peerLock.RUnlock() if lookup { location := db.geodb.Lookup(event.IP) - p.Location = &PeerLocation{ + diff.PeerBundles[ip].Location = &GeoLocation{ Country: location.Country.Names.English, City: location.City.Names.English, Latitude: location.Location.Latitude, @@ -131,29 +182,30 @@ func (db *Dashboard) collectPeerData() { } case event := <-handshakeCh: ip := event.IP.String() - p := getOrInitPeer(network, ip, event.DefaultID) + p := getOrInitPeer(diff, ip, event.DefaultID) + p.DefaultID = event.DefaultID if p.Handshake == nil { p.Handshake = []time.Time{event.Handshake} } else { p.Handshake = append(p.Handshake, event.Handshake) } - delete(network.Peers[ip], event.DefaultID) - getOrInitPeer(network, ip, event.ID) - network.Peers[ip][event.ID] = p // interleave instead + delete(diff.PeerBundles[ip].Peers, event.DefaultID) + getOrInitPeer(diff, ip, event.ID) + diff.PeerBundles[ip].Peers[event.ID] = p // TODO (kurkomisi): Merge instead in order to keep the previous connection. // Remove the peer from history in case the metering was before the handshake. db.peerLock.RLock() - stored := db.peerHistory.Peers[ip] != nil && db.peerHistory.Peers[ip][event.DefaultID] != nil + stored := db.networkHistory.PeerBundles[ip] != nil && db.networkHistory.PeerBundles[ip].Peers[event.DefaultID] != nil db.peerLock.RUnlock() if stored { db.peerLock.Lock() - hp := getOrInitPeer(db.peerHistory, ip, event.DefaultID) - delete(db.peerHistory.Peers[ip], event.DefaultID) - getOrInitPeer(db.peerHistory, ip, event.ID) - db.peerHistory.Peers[ip][event.ID] = hp // interleave instead + hp := getOrInitPeer(db.networkHistory, ip, event.DefaultID) + delete(db.networkHistory.PeerBundles[ip].Peers, event.DefaultID) + getOrInitPeer(db.networkHistory, ip, event.ID) + db.networkHistory.PeerBundles[ip].Peers[event.ID] = hp // TODO (kurkomisi): Merge. db.peerLock.Unlock() } case event := <-disconnectCh: - p := getOrInitPeer(network, event.IP.String(), event.ID) + p := getOrInitPeer(diff, event.IP.String(), event.ID) if p.Disconnected == nil { p.Disconnected = []time.Time{event.Disconnected} } else { @@ -161,7 +213,7 @@ func (db *Dashboard) collectPeerData() { } case event := <-readCh: // Sum up the ingress between two updates. - p := getOrInitPeer(network, event.IP.String(), event.ID) + p := getOrInitPeer(diff, event.IP.String(), event.ID) if len(p.Ingress) <= 0 { p.Ingress = ChartEntries{&ChartEntry{Value: float64(event.Ingress)}} } else { @@ -169,7 +221,7 @@ func (db *Dashboard) collectPeerData() { } case event := <-writeCh: // Sum up the egress between two updates. - p := getOrInitPeer(network, event.IP.String(), event.ID) + p := getOrInitPeer(diff, event.IP.String(), event.ID) if len(p.Egress) <= 0 { p.Egress = ChartEntries{&ChartEntry{Value: float64(event.Egress)}} } else { @@ -177,13 +229,15 @@ func (db *Dashboard) collectPeerData() { } case <-ticker.C: now := time.Now() + // Merge the diff with the history. db.peerLock.Lock() - for ip, peers := range network.Peers { - for id, peer := range peers { - peerHistory := getOrInitPeer(db.peerHistory, ip, id) - if peer.Location != nil { - peerHistory.Location = peer.Location - } + for ip, bundle := range diff.PeerBundles { + if bundle.Location != nil { + b := getOrInitBundle(db.networkHistory, ip) + b.Location = bundle.Location + } + for id, peer := range bundle.Peers { + peerHistory := getOrInitPeer(db.networkHistory, ip, id) if peer.Connected != nil { peerHistory.Connected = append(peerHistory.Connected, peer.Connected...) } @@ -202,11 +256,9 @@ func (db *Dashboard) collectPeerData() { if peerHistory.Ingress == nil { peer.Ingress = append(emptyChartEntries(now.Add(-db.config.Refresh), peerIngressSampleLimit-1, db.config.Refresh), ingress) peerHistory.Ingress = peer.Ingress - //peerHistory.Ingress = ChartEntries{ingress} } else { peer.Ingress = ChartEntries{ingress} peerHistory.Ingress = append(peerHistory.Ingress[1:], ingress) - //peerHistory.Ingress = append(peerHistory.Ingress, ingress) } egress := &ChartEntry{ Time: now, @@ -217,27 +269,23 @@ func (db *Dashboard) collectPeerData() { if peerHistory.Egress == nil { peer.Egress = append(emptyChartEntries(now.Add(-db.config.Refresh), peerEgressSampleLimit-1, db.config.Refresh), egress) peerHistory.Egress = peer.Egress - //peerHistory.Egress = ChartEntries{egress} } else { peer.Egress = ChartEntries{egress} peerHistory.Egress = append(peerHistory.Egress[1:], egress) - //peerHistory.Egress = append(peerHistory.Egress, egress) } } } db.peerLock.Unlock() - db.sendToAll(&Message{Network: deepcopy.Copy(network).(*NetworkMessage)}) + // Send the diff to the clients. + db.sendToAll(&Message{Network: deepcopy.Copy(diff).(*NetworkMessage)}) - //fmt.Println() - //s, _ := json.MarshalIndent(network, "", " ") - //fmt.Println(string(s)) - - for ip, peers := range network.Peers { - for id := range peers { - peers[id] = nil - delete(peers, id) + // Prepare for the next metering, clear the diff variable. + for ip, bundle := range diff.PeerBundles { + for id := range bundle.Peers { + bundle.Peers[id] = nil + delete(bundle.Peers, id) } - delete(network.Peers, ip) + delete(diff.PeerBundles, ip) } case errc := <-db.quit: close(quit) @@ -247,49 +295,54 @@ func (db *Dashboard) collectPeerData() { } } -func (db *Dashboard) cleanPeerHistory(quit chan struct{}) { +// keepPeerHistoryClean purges the stored peer metrics with a given rate in +// order to decrease the load. The inactive peers that disconnected before +// the calculated time will be deleted. If the total amount of peers exceeds +// the limit, the surplus will be chosen from the disconnected ones in the +// iteration order, and will be deleted as well. +func (db *Dashboard) keepPeerHistoryClean(quit chan struct{}) { cleanRate := db.config.Refresh * peerTrafficSampleLimit for { select { case <-time.After(cleanRate): - // clear disconnected validAfter := time.Now().Add(-cleanRate) db.peerLock.Lock() - for ip, peers := range db.peerHistory.Peers { - for id, peer := range peers { + for ip, bundle := range db.networkHistory.PeerBundles { + bundle.Location = nil + for id, peer := range bundle.Peers { if len(peer.Disconnected) > 0 && peer.Disconnected[len(peer.Disconnected)-1].Before(validAfter) { - db.peerHistory.Peers[ip][id].Location = nil - db.peerHistory.Peers[ip][id] = nil - delete(db.peerHistory.Peers[ip], id) + bundle.Peers[id] = nil + delete(bundle.Peers, id) } } - if len(peers) <= 0 { - delete(db.peerHistory.Peers, ip) + if len(bundle.Peers) <= 0 { + delete(db.networkHistory.PeerBundles, ip) } } + // TODO (kurkomisi): Check the limit during the insertion. var lenCount int - for _, peers := range db.peerHistory.Peers { - lenCount += len(peers) + for _, bundle := range db.networkHistory.PeerBundles { + lenCount += len(bundle.Peers) } - if lenCount > p2p.MeteredPeerLimit { + if lenCount > peerLimit { outerLoop: - for ip, peers := range db.peerHistory.Peers { - for id, peer := range peers { + for ip, bundle := range db.networkHistory.PeerBundles { + bundle.Location = nil + for id, peer := range bundle.Peers { if peer.Disconnected != nil { - db.peerHistory.Peers[ip][id].Location = nil - db.peerHistory.Peers[ip][id] = nil - delete(db.peerHistory.Peers[ip], id) + bundle.Peers[id] = nil + delete(bundle.Peers, id) lenCount-- - if lenCount <= p2p.MeteredPeerLimit { - if len(peers) <= 0 { - delete(db.peerHistory.Peers, ip) + if lenCount <= peerLimit { + if len(bundle.Peers) <= 0 { + delete(bundle.Peers, ip) } break outerLoop } } } - if len(peers) <= 0 { - delete(db.peerHistory.Peers, ip) + if len(bundle.Peers) <= 0 { + delete(db.networkHistory.PeerBundles, ip) } } } diff --git a/dashboard/system.go b/dashboard/system.go new file mode 100644 index 0000000000..4842a6d841 --- /dev/null +++ b/dashboard/system.go @@ -0,0 +1,155 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package dashboard + +import ( + "runtime" + "time" + + "github.com/elastic/gosigar" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/p2p" +) + +// meterCollector returns a function, which retrieves the count of a specific meter. +func meterCollector(name string) func() int64 { + if meter := metrics.Get(name); meter != nil { + m := meter.(metrics.Meter) + return func() int64 { + return m.Count() + } + } + return func() int64 { + return 0 + } +} + +// collectSystemData gathers data about the system and sends it to the clients. +func (db *Dashboard) collectSystemData() { + defer db.wg.Done() + + systemCPUUsage := gosigar.Cpu{} + systemCPUUsage.Get() + var ( + mem runtime.MemStats + + collectNetworkIngress = meterCollector(p2p.MetricsInboundTraffic) + collectNetworkEgress = meterCollector(p2p.MetricsOutboundTraffic) + collectDiskRead = meterCollector("eth/db/chaindata/disk/read") + collectDiskWrite = meterCollector("eth/db/chaindata/disk/write") + + prevNetworkIngress = collectNetworkIngress() + prevNetworkEgress = collectNetworkEgress() + prevProcessCPUTime = getProcessCPUTime() + prevSystemCPUUsage = systemCPUUsage + prevDiskRead = collectDiskRead() + prevDiskWrite = collectDiskWrite() + + frequency = float64(db.config.Refresh / time.Second) + numCPU = float64(runtime.NumCPU()) + ) + + for { + select { + case errc := <-db.quit: + errc <- nil + return + case <-time.After(db.config.Refresh): + systemCPUUsage.Get() + var ( + curNetworkIngress = collectNetworkIngress() + curNetworkEgress = collectNetworkEgress() + curProcessCPUTime = getProcessCPUTime() + curSystemCPUUsage = systemCPUUsage + curDiskRead = collectDiskRead() + curDiskWrite = collectDiskWrite() + + deltaNetworkIngress = float64(curNetworkIngress - prevNetworkIngress) + deltaNetworkEgress = float64(curNetworkEgress - prevNetworkEgress) + deltaProcessCPUTime = curProcessCPUTime - prevProcessCPUTime + deltaSystemCPUUsage = curSystemCPUUsage.Delta(prevSystemCPUUsage) + deltaDiskRead = curDiskRead - prevDiskRead + deltaDiskWrite = curDiskWrite - prevDiskWrite + ) + prevNetworkIngress = curNetworkIngress + prevNetworkEgress = curNetworkEgress + prevProcessCPUTime = curProcessCPUTime + prevSystemCPUUsage = curSystemCPUUsage + prevDiskRead = curDiskRead + prevDiskWrite = curDiskWrite + + now := time.Now() + + runtime.ReadMemStats(&mem) + activeMemory := &ChartEntry{ + Time: now, + Value: float64(mem.Alloc) / frequency, + } + virtualMemory := &ChartEntry{ + Time: now, + Value: float64(mem.Sys) / frequency, + } + networkIngress := &ChartEntry{ + Time: now, + Value: deltaNetworkIngress / frequency, + } + networkEgress := &ChartEntry{ + Time: now, + Value: deltaNetworkEgress / frequency, + } + processCPU := &ChartEntry{ + Time: now, + Value: deltaProcessCPUTime / frequency / numCPU * 100, + } + systemCPU := &ChartEntry{ + Time: now, + Value: float64(deltaSystemCPUUsage.Sys+deltaSystemCPUUsage.User) / frequency / numCPU, + } + diskRead := &ChartEntry{ + Time: now, + Value: float64(deltaDiskRead) / frequency, + } + diskWrite := &ChartEntry{ + Time: now, + Value: float64(deltaDiskWrite) / frequency, + } + db.sysLock.Lock() + db.sysHistory.ActiveMemory = append(db.sysHistory.ActiveMemory[1:], activeMemory) + db.sysHistory.VirtualMemory = append(db.sysHistory.VirtualMemory[1:], virtualMemory) + db.sysHistory.NetworkIngress = append(db.sysHistory.NetworkIngress[1:], networkIngress) + db.sysHistory.NetworkEgress = append(db.sysHistory.NetworkEgress[1:], networkEgress) + db.sysHistory.ProcessCPU = append(db.sysHistory.ProcessCPU[1:], processCPU) + db.sysHistory.SystemCPU = append(db.sysHistory.SystemCPU[1:], systemCPU) + db.sysHistory.DiskRead = append(db.sysHistory.DiskRead[1:], diskRead) + db.sysHistory.DiskWrite = append(db.sysHistory.DiskWrite[1:], diskWrite) + db.sysLock.Unlock() + + db.sendToAll(&Message{ + System: &SystemMessage{ + ActiveMemory: ChartEntries{activeMemory}, + VirtualMemory: ChartEntries{virtualMemory}, + NetworkIngress: ChartEntries{networkIngress}, + NetworkEgress: ChartEntries{networkEgress}, + ProcessCPU: ChartEntries{processCPU}, + SystemCPU: ChartEntries{systemCPU}, + DiskRead: ChartEntries{diskRead}, + DiskWrite: ChartEntries{diskWrite}, + }, + }) + } + } +} diff --git a/p2p/metrics.go b/p2p/metrics.go index 836ab560fe..134f689bdf 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -21,52 +21,54 @@ package p2p import ( "net" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/metrics" - "sync" - "time" - "github.com/ethereum/go-ethereum/log" - "sync/atomic" "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/p2p/discover" ) const ( - MetricsInboundTraffic = "p2p/InboundTraffic" - MetricsInboundConnects = "p2p/InboundConnects" - MetricsOutboundTraffic = "p2p/OutboundTraffic" - MetricsOutboundConnects = "p2p/OutboundConnects" - - MeteredPeerLimit = 16384 + MetricsInboundConnects = "p2p/InboundConnects" // Name for the registered inbound connects meter + MetricsInboundTraffic = "p2p/InboundTraffic" // Name for the registered inbound traffic meter + MetricsOutboundConnects = "p2p/OutboundConnects" // Name for the registered outbound connects meter + MetricsOutboundTraffic = "p2p/OutboundTraffic" // Name for the registered outbound traffic meter ) var ( - ingressConnectMeter = metrics.NewRegisteredMeter(MetricsInboundConnects, nil) - ingressTrafficMeter = metrics.NewRegisteredMeter(MetricsInboundTraffic, nil) - egressConnectMeter = metrics.NewRegisteredMeter(MetricsOutboundConnects, nil) - egressTrafficMeter = metrics.NewRegisteredMeter(MetricsOutboundTraffic, nil) + ingressConnectMeter = metrics.NewRegisteredMeter(MetricsInboundConnects, nil) // meter counting the ingress connections + ingressTrafficMeter = metrics.NewRegisteredMeter(MetricsInboundTraffic, nil) // meter metering the cumulative ingress traffic + egressConnectMeter = metrics.NewRegisteredMeter(MetricsOutboundConnects, nil) // meter counting the egress connections + egressTrafficMeter = metrics.NewRegisteredMeter(MetricsOutboundTraffic, nil) // meter metering the cumulative egress traffic - NME = &networkMeterEvents{} + metricsFeed = new(peerMetricsFeed) // Peer event feed for metrics + + defaultMeteredPeerID uint64 // Used to create unique id for the metered connection before the handshake ) -type networkMeterEvents struct { - connectFeed event.Feed - handshakeFeed event.Feed - disconnectFeed event.Feed +// peerMetricsFeed delivers the peer metrics to the subscribed channels. +type peerMetricsFeed struct { + connect event.Feed // Event feed to notify the connection of a peer + handshake event.Feed // Event feed to notify the handshake with a peer + disconnect event.Feed // Event feed to notify the disconnection of a peer + read event.Feed // Event feed to notify the amount of read bytes of a peer + write event.Feed // Event feed to notify the amount of written bytes of a peer - readFeed event.Feed - writeFeed event.Feed - - scope event.SubscriptionScope - - defaultID uint64 + scope event.SubscriptionScope // Facility to unsubscribe all the subscriptions at once } +// PeerConnectEvent contains information about the connection of a peer. type PeerConnectEvent struct { IP net.IP ID string Connected time.Time } +// PeerHandshakeEvent contains information about the handshake with a peer. type PeerHandshakeEvent struct { IP net.IP DefaultID string @@ -74,52 +76,65 @@ type PeerHandshakeEvent struct { Handshake time.Time } +// PeerDisconnectEvent contains information about the disconnection of a peer. type PeerDisconnectEvent struct { IP net.IP ID string Disconnected time.Time } +// PeerReadEvent contains information about the read operation of a peer. type PeerReadEvent struct { IP net.IP ID string Ingress int } +// PeerWriteEvent contains information about the write operation of a peer. type PeerWriteEvent struct { IP net.IP ID string Egress int } +// SubscribePeerConnectEvent registers a subscription of PeerConnectEvent func SubscribePeerConnectEvent(ch chan<- PeerConnectEvent) event.Subscription { - return NME.scope.Track(NME.connectFeed.Subscribe(ch)) -} -func SubscribePeerHandshakeEvent(ch chan<- PeerHandshakeEvent) event.Subscription { - return NME.scope.Track(NME.handshakeFeed.Subscribe(ch)) -} -func SubscribePeerDisconnectEvent(ch chan<- PeerDisconnectEvent) event.Subscription { - return NME.scope.Track(NME.disconnectFeed.Subscribe(ch)) -} -func SubscribePeerReadEvent(ch chan<- PeerReadEvent) event.Subscription { - return NME.scope.Track(NME.readFeed.Subscribe(ch)) -} -func SubscribePeerWriteEvent(ch chan<- PeerWriteEvent) event.Subscription { - return NME.scope.Track(NME.writeFeed.Subscribe(ch)) + return metricsFeed.scope.Track(metricsFeed.connect.Subscribe(ch)) } -func closeNME() { - NME.scope.Close() +// SubscribePeerHandshakeEvent registers a subscription of PeerHandshakeEvent +func SubscribePeerHandshakeEvent(ch chan<- PeerHandshakeEvent) event.Subscription { + return metricsFeed.scope.Track(metricsFeed.handshake.Subscribe(ch)) +} + +// SubscribePeerDisconnectEvent registers a subscription of PeerDisconnectEvent +func SubscribePeerDisconnectEvent(ch chan<- PeerDisconnectEvent) event.Subscription { + return metricsFeed.scope.Track(metricsFeed.disconnect.Subscribe(ch)) +} + +// SubscribePeerReadEvent registers a subscription of PeerReadEvent +func SubscribePeerReadEvent(ch chan<- PeerReadEvent) event.Subscription { + return metricsFeed.scope.Track(metricsFeed.read.Subscribe(ch)) +} + +// SubscribePeerWriteEvent registers a subscription of PeerWriteEvent +func SubscribePeerWriteEvent(ch chan<- PeerWriteEvent) event.Subscription { + return metricsFeed.scope.Track(metricsFeed.write.Subscribe(ch)) +} + +// closeMetricsFeed closes all the tracked subscriptions. +func closeMetricsFeed() { + metricsFeed.scope.Close() } // meteredConn is a wrapper around a net.Conn that meters both the // inbound and outbound network traffic. type meteredConn struct { - net.Conn // Network connection to wrap with metering - ip net.IP - id string + net.Conn // Network connection to wrap with metering + ip net.IP // The IP address of the peer + id string // The node id of the peer - lock sync.RWMutex + lock sync.RWMutex // Lock protecting the metered connection's internals } // newMeteredConn creates a new metered connection, also bumping the ingress or @@ -140,8 +155,8 @@ func newMeteredConn(conn net.Conn, ingress bool, ip net.IP) net.Conn { } else { egressConnectMeter.Mark(1) } - id := fmt.Sprintf("peer_%d", atomic.AddUint64(&NME.defaultID, 1)) - NME.connectFeed.Send(PeerConnectEvent{ + id := fmt.Sprintf("peer_%d", atomic.AddUint64(&defaultMeteredPeerID, 1)) + metricsFeed.connect.Send(PeerConnectEvent{ IP: ip, ID: id, Connected: time.Now(), @@ -161,7 +176,7 @@ func (c *meteredConn) Read(b []byte) (n int, err error) { c.lock.RLock() id := c.id c.lock.RUnlock() - NME.readFeed.Send(PeerReadEvent{ + metricsFeed.read.Send(PeerReadEvent{ IP: c.ip, ID: id, Ingress: n, @@ -177,7 +192,7 @@ func (c *meteredConn) Write(b []byte) (n int, err error) { c.lock.RLock() id := c.id c.lock.RUnlock() - NME.writeFeed.Send(PeerWriteEvent{ + metricsFeed.write.Send(PeerWriteEvent{ IP: c.ip, ID: id, Egress: n, @@ -185,11 +200,12 @@ func (c *meteredConn) Write(b []byte) (n int, err error) { return n, err } +// Close closes the underlying connection. func (c *meteredConn) Close() error { c.lock.RLock() id := c.id c.lock.RUnlock() - NME.disconnectFeed.Send(PeerDisconnectEvent{ + metricsFeed.disconnect.Send(PeerDisconnectEvent{ IP: c.ip, ID: id, Disconnected: time.Now(), @@ -197,15 +213,16 @@ func (c *meteredConn) Close() error { return c.Conn.Close() } -func (c *meteredConn) handshakeDone(peerID string) { +// handshakeDone changes the default id to the peer's node id. +func (c *meteredConn) handshakeDone(id discover.NodeID) { c.lock.Lock() defaultID := c.id - c.id = peerID + c.id = id.String() c.lock.Unlock() - NME.handshakeFeed.Send(PeerHandshakeEvent{ + metricsFeed.handshake.Send(PeerHandshakeEvent{ IP: c.ip, DefaultID: defaultID, - ID: peerID, + ID: id.String(), Handshake: time.Now(), }) } diff --git a/p2p/server.go b/p2p/server.go index 11fd9b6dfc..c1a1ac9952 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -41,7 +41,7 @@ const ( // Connectivity defaults. maxActiveDialTasks = 16 - DefaultMaxPendingPeers = 50 + defaultMaxPendingPeers = 50 defaultDialRatio = 3 // Maximum time allowed for reading a complete message. @@ -388,7 +388,7 @@ func (srv *Server) Stop() { close(srv.quit) srv.lock.Unlock() srv.loopWG.Wait() - closeNME() + closeMetricsFeed() } // sharedUDPConn implements a shared connection. Write sends messages to the underlying connection while read returns @@ -800,7 +800,7 @@ func (srv *Server) listenLoop() { defer srv.loopWG.Done() srv.log.Info("RLPx listener up", "self", srv.makeSelf(srv.listener, srv.ntab)) - tokens := DefaultMaxPendingPeers + tokens := defaultMaxPendingPeers if srv.MaxPendingPeers > 0 { tokens = srv.MaxPendingPeers } @@ -884,7 +884,7 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) e return err } if conn, ok := c.fd.(*meteredConn); ok { - conn.handshakeDone(c.id.String()) + conn.handshakeDone(c.id) } clog := srv.log.New("id", c.id, "addr", c.fd.RemoteAddr(), "conn", c.flags) // For dialed connections, check that the remote public key matches. diff --git a/vendor/github.com/apilayer/freegeoip/AUTHORS b/vendor/github.com/apilayer/freegeoip/AUTHORS new file mode 100644 index 0000000000..7d80c4d211 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of freegeoip authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS file. +# +# Names should be added to this file as +# Name or Organization +# +# The email address is not required for organizations. +# +# Please keep the list sorted. + +Alexandre Fiori diff --git a/vendor/github.com/apilayer/freegeoip/CONTRIBUTORS b/vendor/github.com/apilayer/freegeoip/CONTRIBUTORS new file mode 100644 index 0000000000..a460460e4e --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/CONTRIBUTORS @@ -0,0 +1,22 @@ +# This is the official list of freegeoip contributors for copyright purposes. +# This file is distinct from the AUTHORS file. +# +# Names should be added to this file as +# Name or Organization +# +# Please keep the list sorted. +# +# Use the following command to generate the list: +# +# git shortlog -se | awk '{print $2 " " $3 " " $4}' +# +# The email address is not required for organizations. + +Alex Goretoy +Gleicon Moraes +Leandro Pereira +Lucas Fontes +Matthias Nehlsen +Melchi +Nick Muerdter +Vladimir Agafonkin diff --git a/vendor/github.com/apilayer/freegeoip/Dockerfile b/vendor/github.com/apilayer/freegeoip/Dockerfile new file mode 100644 index 0000000000..ac3b5e5a2e --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/Dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.9 + +COPY cmd/freegeoip/public /var/www + +ADD . /go/src/github.com/apilayer/freegeoip +RUN \ + cd /go/src/github.com/apilayer/freegeoip/cmd/freegeoip && \ + go get -d && go install && \ + apt-get update && apt-get install -y libcap2-bin && \ + setcap cap_net_bind_service=+ep /go/bin/freegeoip && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + useradd -ms /bin/bash freegeoip + +USER freegeoip +ENTRYPOINT ["/go/bin/freegeoip"] + +EXPOSE 8080 + +# CMD instructions: +# Add "-use-x-forwarded-for" if your server is behind a reverse proxy +# Add "-public", "/var/www" to enable the web front-end +# Add "-internal-server", "8888" to enable the pprof+metrics server +# +# Example: +# CMD ["-use-x-forwarded-for", "-public", "/var/www", "-internal-server", "8888"] diff --git a/vendor/github.com/apilayer/freegeoip/HISTORY.md b/vendor/github.com/apilayer/freegeoip/HISTORY.md new file mode 100644 index 0000000000..1a8e68db76 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/HISTORY.md @@ -0,0 +1,55 @@ +# History of freegeoip.net + +The freegeoip software is the result of a web server research project that +started in 2009, written in Python and hosted on +[Google App Engine](http://appengine.google.com). It was rapidly adopted by +many developers around the world due to its simplistic and straightforward +HTTP API, causing the free account on GAE to exceed its quota every day +after few hours of operation. + +A year later freegeoip 1.0 was released, and the freegeoip.net domain +moved over to its own server infrastructure. The software was rewritten +using the [Cyclone](http://cyclone.io) web framework, backed by +[Twisted](http://twistedmatrix.com) and [PyPy](http://pypy.org) in +production. That's when the first database management tool was created, +a script that would download many pieces of information from the Internet +to create the IP database, an sqlite flat file used by the server. + +This version of the Python server shipped with a much better front-end as +well, but still as a server-side rendered template inherited from the GAE +version. It was only circa 2011 that freegeoip got its first standalone +front-end based on jQuery, and is when Twitter bootstrap was first used. + +Python played an important role in the early life of freegeoip and +allowed the service to grow and evolve fast. It provided a lot of +flexibility in building and maintaining the IP database using multiple +sources of data. This version of the server lasted until 2013, when +it was once again rewritten from scratch, this time in Go. The database +tool, however, remained intact. + +In 2013 the Go version was released as freegeoip 2.0 and this version +had many iterations. The first versions of the server written in Go were +very rustic, practically a verbatim transcription of the Python server. +Took a while until it started looking more like common Go code, and to +have tests. + +Another important change that shipped with v2 was a front-end based on +AngularJS, but still mixed with some jQuery. The Google map in the front +page was made optional to put more focus on the HTTP API. The popularity +of freegeoip has increased considerably over the years of 2013 and 2014, +calling for more. + +Enter freegeoip 3.0, an evolution of the Go server. The foundation of +freegeoip, which is the IP database and HTTP API, now lives in a Go +package that other developers can leverage. The freegeoip web server is +built on this package making its code cleaner, the server faster, +and requires zero maintenance for the IP database. The server downloads +the file from MaxMind and keep it up to date in background. + +This and other changes make it very Docker friendly. + +The front-end has been trimmed down to a single index.html file that loads +CSS and JS from CDNs on the internet. The JS part is based on AngularJS +and handles the search request and response of the public site. The +optional map has become a link to Google Maps following the lat/long +of the query results. diff --git a/vendor/github.com/apilayer/freegeoip/LICENSE b/vendor/github.com/apilayer/freegeoip/LICENSE new file mode 100644 index 0000000000..12d6a5cd53 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The freegeoip authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * The names of authors or contributors may NOT be used to endorse or +promote products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/apilayer/freegeoip/Procfile b/vendor/github.com/apilayer/freegeoip/Procfile new file mode 100644 index 0000000000..de810e15d9 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/Procfile @@ -0,0 +1 @@ +web: freegeoip -http :${PORT} -use-x-forwarded-for -public /app/cmd/freegeoip/public -quota-backend map -quota-max 10000 diff --git a/vendor/github.com/apilayer/freegeoip/README.md b/vendor/github.com/apilayer/freegeoip/README.md new file mode 100644 index 0000000000..d89310d8c1 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/README.md @@ -0,0 +1,259 @@ +![freegeoip ipstack](https://raw.githubusercontent.com/apilayer/freegeoip/master/freegeo-warning.png) + +# freegeoip - Important Announcement + +*[The old freegeoip API is now deprecated and will be discontinued on July 1st, 2018]* + +Launched more than 6 years ago, the freegeoip.net API has grown into one of the biggest and most widely used APIs for IP to location services worldwide. The API is used by thousands of developers, SMBs and large corporations around the globe and is currently handling more than 2 billion requests per day. After years of operation and the API remaining almost unchanged, today we announce the complete re-launch of freegeoip into a faster, more advanced and more scalable API service called ipstack (https://ipstack.com). All users that wish to continue using our IP to location service will be required to sign up to obtain a free API access key and perform a few simple changes to their integration. While the new API offers the ability to return data in the same structure as the old freegeoip API, the new API structure offers various options of delivering much more advanced data for IP Addresses. + +## Required Changes to Legacy Integrations (freegeoip.net/json/xml) + +As of March 31 2018 the old freegeoip API is deprecated and a completely re-designed API is now accessible at http://api.ipstack.com. While the new API offers the same capabilities as the old one and also has the option of returning data in the legacy format, the API URL has now changed and all users are required to sign up for a free API Access Key to use the service. + +1. Get a free ipstack Account and Access Key + +Head over to https://ipstack.com and follow the instructions to create your account and obtain your access token. If you only need basic IP to Geolocation data and do not require more than 10,000 requests per month, you can use the free account. If you'd like more advanced features or more requests than included in the free account you will need to choose one of the paid options. You can find an overview of all available plans at https://ipstack.com/product + +2. Integrate the new API URL + +The new API comes with a completely new endpoint (api.ipstack.com) and requires you to append your API Access Key to the URL as a GET parameter. For complete integration instructions, please head over to the API Documentation at https://ipstack.com/documentation. While the new API offers a completely reworked response structure with many additional data points, we also offer the option to receive results in the old freegeoip.net format in JSON or XML. + +To receive your API results in the old freegeoip format, please simply append &legacy=1 to the new API URL. + +JSON Example: http://api.ipstack.com/186.116.207.169?access_key=YOUR_ACCESS_KEY&output=json&legacy=1 + +XML Example: http://api.ipstack.com/186.116.207.169?access_key=YOUR_ACCESS_KEY&output=xml&legacy=1 + +## New features with ipstack +While the new ipstack service now runs on a commercial/freemium model, we have worked hard at building a faster, more scalable, and more advanced IP to location API product. You can read more about all the new features by navigating to https://ipstack.com, but here's a list of the most important changes and additions: + +- We're still free for basic usage + +While we now offer paid / premium options for our more advanced users, our core product and IP to Country/Region/City product is still completely free of charge for up to 10,000 requests per month. If you need more advanced data or more requests, you can choose one of the paid plans listed at https://ipstack.com/product + +- Batch Requests + +Need to validate more than 1 IP Address in a single API Call? Our new Bulk Lookup Feature (available on our paid plans) allows you to geolocate up to 50 IP Addresses in a single API Call. + +- Much more Data + +While the old freegeoip API was limited to provide only the most basic IP to location data, our new API provides more than 20 additional data points including Language, Time Zone, Current Time, Currencies, Connection & ASN Information, and much more. To learn more about all the data points available, please head over to the ipstack website. + +- Security & Fraud Prevention Tools + +Do you want to prevent fraudulent traffic from arriving at your website or from abusing your service? Easily spot malicious / proxy / VPN traffic by using our new Security Module, which outputs a lot of valuable security information about an IP Address. + +Next Steps + +- Deprecation of the old API + +While we want to keep the disruption to our current users as minimal as possible, we are planning to shut the old API down on July 1st, 2018. This should give all users enough time to adapt to changes, and should we still see high volumes of traffic going to the old API by that date, we may decide to extend it further. In any case, we highly recommend you switch to the new API as soon as possible. We will keep you posted here about any changes to the planned shutdown date. + +- Any Questions? Please get in touch! + +It's very important to ensure a smooth transition to ipstack for all freegeoip API users. If you are a developer that has published a plugin/addon that includes the legacy API, we recommend you get in touch with us and also share this announcement with your users. If you have any questions about the transition or the new API, please get in touch with us at support@ipstack.com + + + + + + + + + + + +# freegeoip - Deprecated Documentation + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +This is the source code of the freegeoip software. It contains both the web server that empowers freegeoip.net, and a package for the [Go](http://golang.org) programming language that enables any web server to support IP geolocation with a simple and clean API. + +See http://en.wikipedia.org/wiki/Geolocation for details about geolocation. + +Developers looking for the Go API can skip to the [Package freegeoip](#packagefreegeoip) section below. + +## Running + +This section is for people who desire to run the freegeoip web server on their own infrastructure. The easiest and most generic way of doing this is by using Docker. All examples below use Docker. + +### Docker + +#### Install Docker + +Docker has [install instructions for many platforms](https://docs.docker.com/engine/installation/), +including +- [Ubuntu](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/) +- [CentOS](https://docs.docker.com/engine/installation/linux/docker-ce/centos/) +- [Mac](https://docs.docker.com/docker-for-mac/install/) + +#### Run the API in a container + +```bash +docker run --restart=always -p 8080:8080 -d apilayer/freegeoip +``` + +#### Test + +```bash +curl localhost:8080/json/1.2.3.4 +# => {"ip":"1.2.3.4","country_code":"US","country_name":"United States", # ... +``` + +### Other Linux, OS X, FreeBSD, and Windows + +There are [pre-compiled binaries](https://github.com/apilayer/freegeoip/releases) available. + +### Production configuration + +For production workloads you may want to use different configuration for the freegeoip web server, for example: + +* Enabling the "internal server" for collecting metrics and profiling/tracing the freegeoip web server on demand +* Monitoring the internal server using [Prometheus](https://prometheus.io), or exporting your metrics to [New Relic](https://newrelic.com) +* Serving the freegeoip API over HTTPS (TLS) using your own certificates, or provisioned automatically using [LetsEncrypt.org](https://letsencrypt.org) +* Configuring [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) to restrict your browser clients to always use HTTPS +* Configuring the read and write timeouts to avoid stale clients consuming server resources +* Configuring the freegeoip web server to read the client IP (for logs, etc) from the X-Forwarded-For header when running behind a reverse proxy +* Configuring [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) to restrict access to your API to specific domains +* Configuring a specific endpoint path prefix other than the default "/" (thus /json, /xml, /csv) to serve the API alongside other APIs on the same host +* Optimizing your round trips by enabling [TCP Fast Open](https://en.wikipedia.org/wiki/TCP_Fast_Open) on your OS and the freegeoip web server +* Setting up usage limits (quotas) for your clients (per client IP) based on requests per time interval; we support various backends such as in-memory map (for single instance), or redis or memcache for distributed deployments +* Serve the default [GeoLite2 City](http://dev.maxmind.com/geoip/geoip2/geolite2/) free database that is downloaded and updated automatically in background on a configurable schedule, or +* Serve the commercial [GeoIP2 City](https://www.maxmind.com/en/geoip2-city) database from MaxMind, either as a local file that you provide and update periodically (so the server can reload it), or configured to be downloaded periodically using your API key + +See the [Server Options](#serveroptions) section below for more information on configuring the server. + +For automation, check out the [freegeoip chef cookbook](https://supermarket.chef.io/cookbooks/freegeoip) or the (legacy) [Ansible Playbook](./cmd/freegeoip/ansible-playbook) for Ubuntu 14.04 LTS. + + + +### Server Options + +To see all the available options, use the `-help` option: + +```bash +docker run --rm -it apilayer/freegeoip -help +``` + +If you're using LetsEncrypt.org to provision your TLS certificates, you have to listen for HTTPS on port 443. Following is an example of the server listening on 3 different ports: metrics + pprof (8888), http (80), and https (443): + +```bash +docker run -p 8888:8888 -p 80:8080 -p 443:8443 -d apilayer/freegeoip \ + -internal-server=:8888 \ + -http=:8080 \ + -https=:8443 \ + -hsts=max-age=31536000 \ + -letsencrypt \ + -letsencrypt-hosts=myfancydomain.io +``` + + You can configure the freegeiop web server via command line flags or environment variables. The names of environment variables are the same for command line flags, but prefixed with FREEGEOIP, all upperscase, separated by underscores. If you want to use environment variables instead: + +```bash +$ cat prod.env +FREEGEOIP_INTERNAL_SERVER=:8888 +FREEGEOIP_HTTP=:8080 +FREEGEOIP_HTTPS=:8443 +FREEGEOIP_HSTS=max-age=31536000 +FREEGEOIP_LETSENCRYPT=true +FREEGEOIP_LETSENCRYPT_HOSTS=myfancydomain.io + +$ docker run --env-file=prod.env -p 8888:8888 -p 80:8080 -p 443:8443 -d apilayer/freegeoip +``` + +By default, HTTP/2 is enabled over HTTPS. You can disable by passing the `-http2=false` flag. + +Also, the Docker image of freegeoip does not provide the web page from freegeiop.net, it only provides the API. If you want to serve that page, you can pass the `-public=/var/www` parameter in the command line. You can also tell Docker to mount that directory as a volume on the host machine and have it serve your own page, using Docker's `-v` parameter. + +If the freegeoip web server is running behind a reverse proxy or load balancer, you have to run it passing the `-use-x-forwarded-for` parameter and provide the `X-Forwarded-For` HTTP header in all requests. This is for the freegeoip web server be able to log the client IP, and to perform geolocation lookups when an IP is not provided to the API, e.g. `/json/` (uses client IP) vs `/json/1.2.3.4`. + +## Database + +The current implementation uses the free [GeoLite2 City](http://dev.maxmind.com/geoip/geoip2/geolite2/) database from MaxMind. + +In the past we had databases from other providers, and at some point even our own database comprised of data from different sources. This means it might change in the future. + +If you have purchased the commercial database from MaxMind, you can point the freegeoip web server or (Go API, for dev) to the URL containing the file, or local file, and the server will use it. + +In case of files on disk, you can replace the file with a newer version and the freegeoip web server will reload it automatically in background. If instead of a file you use a URL (the default), we periodically check the URL in background to see if there's a new database version available, then download the reload it automatically. + +All responses from the freegeiop API contain the date that the database was downloaded in the X-Database-Date HTTP header. + +## API + +The freegeoip API is served by endpoints that encode the response in different formats. + +Example: + +```bash +curl freegeoip.net/json/ +``` + +Returns the geolocation information of your own IP address, the source IP address of the connection. + +You can pass a different IP or hostname. For example, to lookup the geolocation of `github.com` the server resolves the name first, then uses the first IP address available, which might be IPv4 or IPv6: + +```bash +curl freegeoip.net/json/github.com +``` + +Same semantics are available for the `/xml/{ip}` and `/csv/{ip}` endpoints. + +JSON responses can be encoded as JSONP, by adding the `callback` parameter: + +```bash +curl freegeoip.net/json/?callback=foobar +``` + +The callback parameter is ignored on all other endpoints. + +## Metrics and profiling + +The freegeoip web server can provide metrics about its usage, and also supports runtime profiling and tracing. + +Both are disabled by default, but can be enabled by passing the `-internal-server` parameter in the command line. Metrics are generated for [Prometheus](http://prometheus.io) and can be queried at `/metrics` even with curl. + +HTTP pprof is available at `/debug/pprof` and the examples from the [pprof](https://golang.org/pkg/net/http/pprof/) package documentation should work on the freegeiop web server. + + + +## Package freegeoip + +The freegeoip package for the Go programming language provides two APIs: + +- A database API that requires zero maintenance of the IP database; +- A geolocation `http.Handler` that can be used/served by any http server. + +tl;dr if all you want is code then see the `example_test.go` file. + +Otherwise check out the godoc reference. + +[![GoDoc](https://godoc.org/github.com/apilayer/freegeoip?status.svg)](https://godoc.org/github.com/apilayer/freegeoip) +[![Build Status](https://secure.travis-ci.org/apilayer/freegeoip.png)](http://travis-ci.org/apilayer/freegeoip) +[![GoReportCard](https://goreportcard.com/badge/github.com/apilayer/freegeoip)](https://goreportcard.com/report/github.com/apilayer/freegeoip) + +### Features + +- Zero maintenance + +The DB object alone can download an IP database file from the internet and service lookups to your program right away. It will auto-update the file in background and always magically work. + +- DevOps friendly + +If you do care about the database and have the commercial version of the MaxMind database, you can update the database file with your program running and the DB object will load it in background. You can focus on your stuff. + +- Extensible + +Besides the database part, the package provides an `http.Handler` object that you can add to your HTTP server to service IP geolocation lookups with the same simplistic API of freegeoip.net. There's also an interface for crafting your own HTTP responses encoded in any format. + +### Install + +Download the package: + + go get -d github.com/apilayer/freegeoip/... + +Install the web server: + + go install github.com/apilayer/freegeoip/cmd/freegeoip + +Test coverage is quite good, and test code may help you find the stuff you need. diff --git a/vendor/github.com/apilayer/freegeoip/app.json b/vendor/github.com/apilayer/freegeoip/app.json new file mode 100644 index 0000000000..99495bc8e6 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/app.json @@ -0,0 +1,7 @@ +{ + "name": "freegeoip", + "description": "IP geolocation web server", + "website": "https://github.com/apilayer/freegeoip", + "success_url": "/", + "keywords": ["golang", "geoip", "api"] +} diff --git a/vendor/github.com/apilayer/freegeoip/db.go b/vendor/github.com/apilayer/freegeoip/db.go new file mode 100644 index 0000000000..2810a7ed50 --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/db.go @@ -0,0 +1,453 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package freegeoip + +import ( + "compress/gzip" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "sync" + "time" + + "github.com/howeyc/fsnotify" + "github.com/oschwald/maxminddb-golang" +) + +var ( + // ErrUnavailable may be returned by DB.Lookup when the database + // points to a URL and is not yet available because it's being + // downloaded in background. + ErrUnavailable = errors.New("no database available") + + // Local cached copy of a database downloaded from a URL. + defaultDB = filepath.Join(os.TempDir(), "freegeoip", "db.gz") + + // MaxMindDB is the URL of the free MaxMind GeoLite2 database. + MaxMindDB = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" +) + +// DB is the IP geolocation database. +type DB struct { + file string // Database file name. + checksum string // MD5 of the unzipped database file + reader *maxminddb.Reader // Actual db object. + notifyQuit chan struct{} // Stop auto-update and watch goroutines. + notifyOpen chan string // Notify when a db file is open. + notifyError chan error // Notify when an error occurs. + notifyInfo chan string // Notify random actions for logging + closed bool // Mark this db as closed. + lastUpdated time.Time // Last time the db was updated. + mu sync.RWMutex // Protects all the above. + + updateInterval time.Duration // Update interval. + maxRetryInterval time.Duration // Max retry interval in case of failure. +} + +// Open creates and initializes a DB from a local file. +// +// The database file is monitored by fsnotify and automatically +// reloads when the file is updated or overwritten. +func Open(dsn string) (*DB, error) { + db := &DB{ + file: dsn, + notifyQuit: make(chan struct{}), + notifyOpen: make(chan string, 1), + notifyError: make(chan error, 1), + notifyInfo: make(chan string, 1), + } + err := db.openFile() + if err != nil { + db.Close() + return nil, err + } + err = db.watchFile() + if err != nil { + db.Close() + return nil, fmt.Errorf("fsnotify failed for %s: %s", dsn, err) + } + return db, nil +} + +// MaxMindUpdateURL generates the URL for MaxMind paid databases. +func MaxMindUpdateURL(hostname, productID, userID, licenseKey string) (string, error) { + limiter := func(r io.Reader) *io.LimitedReader { + return &io.LimitedReader{R: r, N: 1 << 30} + } + baseurl := "https://" + hostname + "/app/" + // Get the file name for the product ID. + u := baseurl + "update_getfilename?product_id=" + productID + resp, err := http.Get(u) + if err != nil { + return "", err + } + defer resp.Body.Close() + md5hash := md5.New() + _, err = io.Copy(md5hash, limiter(resp.Body)) + if err != nil { + return "", err + } + sum := md5hash.Sum(nil) + hexdigest1 := hex.EncodeToString(sum[:]) + // Get our client IP address. + resp, err = http.Get(baseurl + "update_getipaddr") + if err != nil { + return "", err + } + defer resp.Body.Close() + md5hash = md5.New() + io.WriteString(md5hash, licenseKey) + _, err = io.Copy(md5hash, limiter(resp.Body)) + if err != nil { + return "", err + } + sum = md5hash.Sum(nil) + hexdigest2 := hex.EncodeToString(sum[:]) + // Generate the URL. + params := url.Values{ + "db_md5": {hexdigest1}, + "challenge_md5": {hexdigest2}, + "user_id": {userID}, + "edition_id": {productID}, + } + u = baseurl + "update_secure?" + params.Encode() + return u, nil +} + +// OpenURL creates and initializes a DB from a URL. +// It automatically downloads and updates the file in background, and +// keeps a local copy on $TMPDIR. +func OpenURL(url string, updateInterval, maxRetryInterval time.Duration) (*DB, error) { + db := &DB{ + file: defaultDB, + notifyQuit: make(chan struct{}), + notifyOpen: make(chan string, 1), + notifyError: make(chan error, 1), + notifyInfo: make(chan string, 1), + updateInterval: updateInterval, + maxRetryInterval: maxRetryInterval, + } + db.openFile() // Optional, might fail. + go db.autoUpdate(url) + err := db.watchFile() + if err != nil { + db.Close() + return nil, fmt.Errorf("fsnotify failed for %s: %s", db.file, err) + } + return db, nil +} + +func (db *DB) watchFile() error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + dbdir, err := db.makeDir() + if err != nil { + return err + } + go db.watchEvents(watcher) + return watcher.Watch(dbdir) +} + +func (db *DB) watchEvents(watcher *fsnotify.Watcher) { + for { + select { + case ev := <-watcher.Event: + if ev.Name == db.file && (ev.IsCreate() || ev.IsModify()) { + db.openFile() + } + case <-watcher.Error: + case <-db.notifyQuit: + watcher.Close() + return + } + time.Sleep(time.Second) // Suppress high-rate events. + } +} + +func (db *DB) openFile() error { + reader, checksum, err := db.newReader(db.file) + if err != nil { + return err + } + stat, err := os.Stat(db.file) + if err != nil { + return err + } + db.setReader(reader, stat.ModTime(), checksum) + return nil +} + +func (db *DB) newReader(dbfile string) (*maxminddb.Reader, string, error) { + f, err := os.Open(dbfile) + if err != nil { + return nil, "", err + } + defer f.Close() + gzf, err := gzip.NewReader(f) + if err != nil { + return nil, "", err + } + defer gzf.Close() + b, err := ioutil.ReadAll(gzf) + if err != nil { + return nil, "", err + } + checksum := fmt.Sprintf("%x", md5.Sum(b)) + mmdb, err := maxminddb.FromBytes(b) + return mmdb, checksum, err +} + +func (db *DB) setReader(reader *maxminddb.Reader, modtime time.Time, checksum string) { + db.mu.Lock() + defer db.mu.Unlock() + if db.closed { + reader.Close() + return + } + if db.reader != nil { + db.reader.Close() + } + db.reader = reader + db.lastUpdated = modtime.UTC() + db.checksum = checksum + select { + case db.notifyOpen <- db.file: + default: + } +} + +func (db *DB) autoUpdate(url string) { + backoff := time.Second + for { + db.sendInfo("starting update") + err := db.runUpdate(url) + if err != nil { + bs := backoff.Seconds() + ms := db.maxRetryInterval.Seconds() + backoff = time.Duration(math.Min(bs*math.E, ms)) * time.Second + db.sendError(fmt.Errorf("download failed (will retry in %s): %s", backoff, err)) + } else { + backoff = db.updateInterval + } + db.sendInfo("finished update") + select { + case <-db.notifyQuit: + return + case <-time.After(backoff): + // Sleep till time for the next update attempt. + } + } +} + +func (db *DB) runUpdate(url string) error { + yes, err := db.needUpdate(url) + if err != nil { + return err + } + if !yes { + return nil + } + tmpfile, err := db.download(url) + if err != nil { + return err + } + err = db.renameFile(tmpfile) + if err != nil { + // Cleanup the tempfile if renaming failed. + os.RemoveAll(tmpfile) + } + return err +} + +func (db *DB) needUpdate(url string) (bool, error) { + stat, err := os.Stat(db.file) + if err != nil { + return true, nil // Local db is missing, must be downloaded. + } + + resp, err := http.Head(url) + if err != nil { + return false, err + } + defer resp.Body.Close() + + // Check X-Database-MD5 if it exists + headerMd5 := resp.Header.Get("X-Database-MD5") + if len(headerMd5) > 0 && db.checksum != headerMd5 { + return true, nil + } + + if stat.Size() != resp.ContentLength { + return true, nil + } + return false, nil +} + +func (db *DB) download(url string) (tmpfile string, err error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + tmpfile = filepath.Join(os.TempDir(), + fmt.Sprintf("_freegeoip.%d.db.gz", time.Now().UnixNano())) + f, err := os.Create(tmpfile) + if err != nil { + return "", err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + if err != nil { + return "", err + } + return tmpfile, nil +} + +func (db *DB) makeDir() (dbdir string, err error) { + dbdir = filepath.Dir(db.file) + _, err = os.Stat(dbdir) + if err != nil { + err = os.MkdirAll(dbdir, 0755) + if err != nil { + return "", err + } + } + return dbdir, nil +} + +func (db *DB) renameFile(name string) error { + os.Rename(db.file, db.file+".bak") // Optional, might fail. + _, err := db.makeDir() + if err != nil { + return err + } + return os.Rename(name, db.file) +} + +// Date returns the UTC date the database file was last modified. +// If no database file has been opened the behaviour of Date is undefined. +func (db *DB) Date() time.Time { + db.mu.RLock() + defer db.mu.RUnlock() + return db.lastUpdated +} + +// NotifyClose returns a channel that is closed when the database is closed. +func (db *DB) NotifyClose() <-chan struct{} { + return db.notifyQuit +} + +// NotifyOpen returns a channel that notifies when a new database is +// loaded or reloaded. This can be used to monitor background updates +// when the DB points to a URL. +func (db *DB) NotifyOpen() (filename <-chan string) { + return db.notifyOpen +} + +// NotifyError returns a channel that notifies when an error occurs +// while downloading or reloading a DB that points to a URL. +func (db *DB) NotifyError() (errChan <-chan error) { + return db.notifyError +} + +// NotifyInfo returns a channel that notifies informational messages +// while downloading or reloading. +func (db *DB) NotifyInfo() <-chan string { + return db.notifyInfo +} + +func (db *DB) sendError(err error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return + } + select { + case db.notifyError <- err: + default: + } +} + +func (db *DB) sendInfo(message string) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return + } + select { + case db.notifyInfo <- message: + default: + } +} + +// Lookup performs a database lookup of the given IP address, and stores +// the response into the result value. The result value must be a struct +// with specific fields and tags as described here: +// https://godoc.org/github.com/oschwald/maxminddb-golang#Reader.Lookup +// +// See the DefaultQuery for an example of the result struct. +func (db *DB) Lookup(addr net.IP, result interface{}) error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.reader != nil { + return db.reader.Lookup(addr, result) + } + return ErrUnavailable +} + +// DefaultQuery is the default query used for database lookups. +type DefaultQuery struct { + Continent struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"continent"` + Country struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"country"` + Region []struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"subdivisions"` + City struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"city"` + Location struct { + Latitude float64 `maxminddb:"latitude"` + Longitude float64 `maxminddb:"longitude"` + MetroCode uint `maxminddb:"metro_code"` + TimeZone string `maxminddb:"time_zone"` + } `maxminddb:"location"` + Postal struct { + Code string `maxminddb:"code"` + } `maxminddb:"postal"` +} + +// Close closes the database. +func (db *DB) Close() { + db.mu.Lock() + defer db.mu.Unlock() + if !db.closed { + db.closed = true + close(db.notifyQuit) + close(db.notifyOpen) + close(db.notifyError) + close(db.notifyInfo) + } + if db.reader != nil { + db.reader.Close() + db.reader = nil + } +} diff --git a/vendor/github.com/apilayer/freegeoip/doc.go b/vendor/github.com/apilayer/freegeoip/doc.go new file mode 100644 index 0000000000..65903477df --- /dev/null +++ b/vendor/github.com/apilayer/freegeoip/doc.go @@ -0,0 +1,14 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Package freegeoip provides an API for searching the geolocation of IP +// addresses. It uses a database that can be either a local file or a +// remote resource from a URL. +// +// Local databases are monitored by fsnotify and reloaded when the file is +// either updated or overwritten. +// +// Remote databases are automatically downloaded and updated in background +// so you can focus on using the API and not managing the database. +package freegeoip diff --git a/vendor/github.com/apilayer/freegeoip/freegeo-warning.png b/vendor/github.com/apilayer/freegeoip/freegeo-warning.png new file mode 100644 index 0000000000000000000000000000000000000000..0e142cb4020bb466d0435112a172f4cac2333705 GIT binary patch literal 14752 zcmb`uWmH^C*EI^k-60U%-CY9#8Yg&g_uw=H4Yb7M0PrvYB=rkFITH1C2C1P2R$!3>vD6om*5Q#X73`4EVOc|~!k7!qOo zgA>-V>^ri=_%68zY|NR$N4gFf1a5D-_wTS}BxEGw9$$7(PF7|#b~AK$!H;gcue$En zO+F_A*NwJw<$@Xz9h)Fb2}FvYn3G2}m zK>Tlj=Kl8&OCbIWtQv6sY58x^mH7_~iT?sbCj#@fY%bF(J8UnV#GXZ|+yfD04vUM! zLZ>N;>BEb)JH>^=gNZxmeIra2zk~iEv+vU7GyMK?Ksl{?jJJ=}_GY=dW8?#G3V@6U z1wSSGRhV<{pAO}$uM*Hc!V#qS-63#!IPX{*e(Uf)^|DwMPCNY{6L=+NI0ZG{=v=uiE&si}@J6S+Z8gK);Hd)QmU?}dhjcw>$S35UM)7Hw)GOurmJf$6 zZFcq!VBgp?oMpG_9nS;!o2FGma`)q^rSAof%U0~k_3_dORdh-qN*>M8n5ZqUeFXD2 z=(@)gjtS?qwkLU)hy9(q&5^V9lg+m0vil>;JcNI{V32^}JHG4^?Q!bmrN3O&^K=$G z@#8v?Dd64y9=qS`t;LQ4myLSGAzO=?lF$gV;phX^-{iTMQN~y8eedPxZ2Zo|xokCy z2JE@a!&15WG8}){Lb6>|xA^1!9+#Mv?-bhzjkl{iQ9_>D$8X(as^F%}O0lXPLQVw% zkG~ykl0{35J!9k6D|wd_oc-F|xL;tPH7Yc= z?53+>M?00t$=P@@h>CH8h9Np{9_V*unzP_9LLdu}x3;!g>?Bn9sl^=;C*VR^sQgZ$ zN#thUX9z8j7ia@2VC1pWxKK-jVwM7JeTSj+Qu!GC+>JBgOVPF~O|!wPr>ye3p9u8s zBQ-xSSltd?(z_Nl6RNTEi#faOQ2Pk(qarrjdm7)Hg~FSSaGrETyR-7NZ6iz$TW#y|Pk>728Q+<&S;weWXyC%nK&})~e>|T* z{`+k6@g*dt}yI{<<;YFIJJTn<_g;g-zLd#(y?4Z=zRQ!bzmt%HEe zOccH*4R2q77@8;Cr(+y-;NvV_4ri6mdVzd!j>s(VdOuOisR$IzIw_4{hP$Pc8LFJ zkZA-#6!WxqVkFhy-{j7k9)t9jOq1LI&WmKIcc zMy!xZNgZ|3Jm|f{Wl_=M`I0HN_B7|m<9iTDu*^h3$0YLdtzy+S+b4G(iGrfvp3c=& z^1HToFCm`W!SCt5=;$C>PKz%0%1y5P&$A75nG%XJ+M8c$0>$fcJwIE7jXSdWx4j%@ zoi5d-zRmAng;^fGEmanZ;{kljn2Pg#y>BsjB%n0jV=t%w4*&77VUrVq5^QTWF7;PT z_o7%_8c_jUKRvd??6|j~c%EVSIaWNKM~h>;yxdL@d2W9q;|wO5mE87x*q>-h(_wj7 z_Hm!vfXzPN%+uN(N_JlfWlOby8xJ4LcF6K#^>phBqR&m`s?Nuu_`@I!t|ZWd3+clO z*^-G%u($b{040YJ$Z(kObY@D&cKo>1L0GPa(B@8d7fwOaI1r9R(Be7F3aB2C=6z3mV3~nk01HL zr;n%g)fM~aQ~N?Np(9vf{pYe9-wQtzDLp)=4N{;o1dfkmNPBV)GS*OYoPim56@DXX_RKF%2<_5EgdIGbg;J8FmAMS?8z0J9tv zP1F$;H=`NZR`PrNb3Qa!e}3GiFVh6`Peln`AxgyYpW}2IXqmV?Xs$#I(&X+rlxx5ar0GnXUXcv6iE~xD8^_Mu4=&pD%~XJt0F1m0Q#XJZ_n03943$NEF_U_Ctxs zNGgh7&gB@~DZPcuSH66`#mBB%4%OQOLW14W9OLePJYq#nx2iG7Ce^=Up&984v zVwU^y&IF;{rkhong|aXlH5ng-AZDFSZ-YjL7lhlT-{-LZ4Akrr(R*K|i7ED!V9~JV z=NBE*awcInT{Mmmj8P%->^fF$J2%@KiQ?mSwYPXVb}Jmt%^k3QGS*w%V_~ROmcYH4 zxZ$L!wxLYb)4T0{w&)=6oXSNNH>Oo?IzEqs;fsN#`tBy8Rt!ub0?xCH-!ajUe~>Dq zV%;5WCm^rX4#^b0+@@=$SGB;befn^-Et@HRE}>?8xjmSTgI9d!Vh0hvWeJFhnc2E9 z)Rg&=S-gOI*^@mzeJ=eg*dcU+wF$Jp9+PZ38p5&ccxxFlb7#A^tW!3u5acijjir<> zSCV|@*`4@h1x#B(B|jN;M4W#&eJlTE=Bs$%&U8pp$xiOK>ut3mH>beqTG2#K=Wr5l zLA$|OYVCZQ?YmgqE|K4Dqc#n}D4%ALH-fXBQE!dAY?MV8rcg4D2asq^25BS*RpwBSLoLd_p$>s+CS^`NS4`yYg~xUlHm; z2PS_PkZYMM6#cR%Fj7nBf31Y)RCvMeUQDg;5q zIvgr;k%UDC`9v-WPhQj>;_hs(r8M#zIJsqkzE>d8$5*`;I`BPnf}qGl{E z;%Q62U7KqRwMKrxR*91$wZ-g9DqO5!gh~a`)UT7y8(Q)#cLh?|!ku?13#6+Bqw{R` zUOz#6ed_v-^O|Aw2kHG@T*hu=sYYTbp4+TH*8LI)(Xv=+{=rs7M|(qz?<9ZgkHI~E z=yq6k-ZprB_3ToY9^TH(y}ZRJ5T=-zJ>pGscGhL>|Cl8hBi^UcxEznopi1*Nzon#y zdW#*RkV9PFkZZ|hc%<1+DHW@F-EYm%hl0z%z3@3Oyj9PdBh2jJWO&}_IcyZmxwOrt z<$Xg3cb!bFKlk!S+7CM!W?+P8>a{5PVaVyp;`vRagXHVROE{bTpJx%U!DJ~7USP%p ztOq{Oc&HkA;OXAOZQ?%hgpSr$L=OJATc(Oao>D0d#S-EuSV>tpmirNOZei0C*FQhs zBMUa95-4E{)=|6nGdf&?#H2{BpXu=*?;;6|8j{PzkEOlI zGOcmjL3Fi2wZ4J6qxqCTY$9i*0oYYjH$ zV2JJyORD8@O?BOypw$*IhA|?u*q&=@IVq~z+kkrt$V~Dy3rribouu0qh&QM(;193o z02dM6dpT{qF>X(=Ug+B2pZ&*5wI{5n%+c7ZX7dyxw`W^yl>I3 zogS#+nAl2bwF7euruUC|+92&8PSi?r3nN7(`T~Q9zw)IukMB|XJu?1Rqa0ZSBkUaR z63i6rPptVs5CSDvYeQdsr%D#*dYG3H86SoW-Xy#%I!A{JNjx1u`#T|BMYGWrnTOe{ z6}IfyaOwoHFsh;@$FRt)q!>kPf11MN?ib9IxbJ70rS7>u5ypjWvuA)@?z#ce`SH+7 zgyEg_4t$5R*F^SpwdUWvx8*Lk;wO^nve@O5ExtK+Bf7Lhi@Xl*qr$waJ#$7KIo1lR z+*2LJ6;kybbn-UsB8FrA#-jR86uY>smx{qa_v_@4Kud_xMn54Lfnmm!&6ql_HcI32 zAQ}UA@sAk6MD}>ji*omE4DHn(y1)oVl;k8=4|xZZ0xu*X1?m2{%MK|IUh7n^HmGU^ zx%X6&j*mzl{%B!NnZ?TUyZpK(rJgm(jp0PoJ?t0pdI=wpS;^hZ$N@|z0UisJJknqF z`HtcwxJ$wm?WF`UA8%h&_usPGpDo?yYd#~skLKUprf4s<{LDq~WnC@?S}aaJT%5S4 z%f@@Y_#71b&;70@b+r-8n%)kFS2#_Y{u<`vliHOsPP`!0jw_aaFGtSnICDoarG@R# zbq#UD%BMHfAKEK*>z}3xCV>0O5K3Q1yv5=M+#oWrqF|WFFRyI8iEKNInHu8qQTr+* zcXXkQC7_*R97k|{Kzp&{QE4EVb#13`{`{lyaKd1R6>P#y<(#g7H^dYOLgQ-nCuO>6 zn&AwVIIQN-)vP5BQJW)@_l%n4DFOX1CAsktfV~*v)qBraLN3uIf|$xl&>P7GAW1wv zPd4Cuz0Xc{FYw*_WbrmFxw!>CD_1pI&Xx*4FE2OhKgYMZietbed&P~59KB4=EL5A~ zGBvRZL@u-$dU>iejPoc(os}MKFdDq8^p}t0hNh4zROV@(a%CfxDe3z%M9X^yO6S50 z)bVOna!AQr{z*VIOjQ}II{MS$8y#pdxm{Hvsd~0&6**%*VDF)De28}CswD*tfauv$ z%5b216~~rac{~S@XBa29mx=OJuf|Xu_?9IIYTqtYm^ChqODHH&F|+BJ{AKh`(=PRG z4e{rH3xoh>1_9MbK|fyw(^!NlNEz{@uE|u)u!*CIylE*TIzdnvV+g9};5n1W)kKTZ za`SdltD}X-VOb01^I33uXS7%8kjC0(m5%`tsmffPS|P9K_i<65E>Tf6{q>+buevc@ z*HMG(=Z4^h3yrp+8|y9S;T&S0-RumaFYj)k6|fr`PDfnvEskO<(?|rCaRUht-+y_s zISkE_`7T=2Cno!@2A|qi>0Jg+CU^N+?syk~+7Su5Ue->|8ux0`;P<#mbb8{Uv)R|9 zg^w5Uq)3EbaUZ|+Glt!Srhk)U8glxTwK1|NFwe7}&_^w{whzQ|-jetP*!1^ZHmyza zdzWva{Sy-|AL0Fie?K(XEia~x-~^N&!=9ru7&W)mUX)MFP7SnL%_x zf`6Jo9b%KhAlRdLlUgsw&avtIeP}^ZrES`k;+9yS;ldxTy*qPABQHF9@2`lzwXNpc z7I!+1sax5sJn;MXt%iEVJsh3IsIXPK_v(1UK^EEl9DkC+oL`zZ!O4>f`zAQ)rNvX6@0OijjWd zmdGrz!v|_g-zwvi`NkKxMG`If`VW=_cB@=5S3VZ}_kx z9YAXR^d}Y|T@e-~G)nCHxu$*RsCs0hXJjZ+mE(~ z8TwQA<%xDYnA~?^^?h{k9d6qEOmyofs7;-<`)3`w-;} zwR1lvu^!j5jJ4rFueuv12z$7MNp8T$qIr-+zmpV~^~Fli-?>|zoZRjw6l2kyjlXa- zZqJn2t+Si3lfmv3_rzp4&@bo5BVjEv$0H&Iq)GKk)|@e&h{MN{K#u$0YPxtv3?fbf z!G)kEm!A>XpE@y1xiJ|Oa3}6}`%1UZxLA?cukF&}Y$bz#Il}x9+0B4Qk(eH%n#`^m z!kJ-Vlxk+B<(WzNLbRGuUi%b5MX!$VmRkfieY7}d+UVT8lRc3<4=a^p0o6!ahK6(N zzBZ(STrElf)+BN2lIsnEn9(tJC|Jnaj8r}0>1tOXGA|>h-s%RN4x%Yw^#T4Cx^{ef zwWgudKTh%Jkz5ygMDEDKBple>FOmx%YtD_+ChQ`Ef=nnusi+|*Jt)juRQg` zy~t88xlOJl6N^RnkJnIxYQ#SR0Cq!3SsfeMF9|oJ>SDLcn^BwutY4p|w+3o~hn}Ph z%>rq4yZoh1(Zj^lEjC3DQ)l`JVgW}~E*btfV;bJ77LRx9!DPO7>xHkPpX5G3ySk;3 zy*BI`qAGmw^dS|Ptf_|(V2ISrB~5`yOdn5{E>D$xClBv5R9OXm`lnW8*2t?@GR!F7 z>aU($pyBrNmf5e%^OvU{yF6zY*EdTOVMM`vt5$z-Cy{PO_(p(pm6S~Nx!vw;6g1d} zWYW(>i9kv8zv}s2B0(Bpy)o^zXM32oA-?552B~~($1j9j%K%JMOFE`1i7Y=+L077k zdlCw5e_*S(N3^AgnqNJapKmgHtV{S`1#T0yi;Z-ZJRAAs8=dNx18{A$FZXfmm{0l5 zFjsB;T6vka8e}gQ-Zs-tne)#Cz?KT&+%ZM|>KOU>>xtR}95ZvgKZ@VjoB5YHg2|c! zn0H<7)?+K}tDQ`bGhB#L9v@E)KM3(2XH}1t+bvEr&0ind9v_!>cD$wv@>Fe2U$cn? z>~OhQY|W@=7ObPZC<-wBV#A@O>=WGh$Hb|kEVYaU=gI;-%fMtp-qN`qvZr=Ew^Z#m zQ5P$(Q@irUUOh+gitwd+z)PI(IZ7+nKPm_WhUVVM{jQ5_m}^XO$II4stS2vu)9Q_YVnClwCO#xEzQ!(+a^ga6j-nn_18;*KV<>=?ezct0-X41N7aJ9&#dsa@z?$p$u~qC!=h)m1=n<7U*JYhhuQ zNg&qW0uIs1j7vYoj>Ai@_rp{{UKgsh>y26&e&(L*n8;4B7--<1u39zR3Umbnf*t{) zCIt!{r+#er*T*Ag8M=k z@{&(q)O#_mf8>P!=u`g<{uQeJqgee1{Fh=y@o&rjg<|!;k*-vW*IyihGlo^k|7HtX zK)XeK$Pn&!Gy*ohIN%_x%|6%$c@GrstF#QksZ_58^`B!B7-%$R4 zQ``PAH73!?{{#GA*xbyWeYxI#t;}8FEdhLE zEk1B?Ou7{Ejj4F@0y_+-rI@JXRN_A7m-^vabZDFuksP21lRvq10>a0o>}WhxZ+2qy>XD9riq2-xvm$ z8k7A=t@&K;38J!It4AGHuil%7aO3S}FFL(2klg)1rz$R-5VxoZzn)+?s$=u9bt>e6 zCNx?82xrK5`2Jq$gB5wzpEPC3o4$yE=$1c9Nxk;AwbgjLp9`({We;$bJ7j*7ZNX+k zp~y*xe&)~ET+>H&5582gC*vzPZX5G3dlV81#;|4a84>Coxja2iI6aQ1`^Ax0mMda9 z{cT{G1~>2J`0HrNSV+d@_2UXoj zgg=`5$=vDTaMd>`CjmnloUn&0Dq0Em4p55c$&Y!WqrL-qSzU2T$U#Oi;{9Z!Jb%mF zX<0*4*oiw(@HPr>NYf2zJ`@NHD>P&7_W!)!StnF~m=i!tXG#3p>Kv0`uBhjW!{x1?h_9mv4`mOklbX#c{mZ0pTGwV09*5B8>(G%b9^XUy<^IF2AJBI8Q)z_UA zc@~zi#&xu(MaFO=O;UB%>FQ+P4J?XF1j`}BUyEPa{eBv!&w$G``vnya`>ECy@)Om5 zK5R&%kY1WYxi8Ybx_$s_-}aZ|&TDDBRBV8=#oIJoYBb#~7nSGUAe!Lio<-T32tI}U zIf(XqNyiYb-a%Vv(aDVZ;AXdKd(g zPOUusn}mVGKK$U1(@s!BiC0b%gWCNdFNoBf46li>vR%U9v8w8+KhYOChAn5yhINK>XJeya_55I@ zX)XuP>f`O-Q+6_IqnDaDHX^U+{t!HF^;k@JI^d3nf~|vilV}#4H2mTP*Hl^#`bw3Q zW~1F^3faEeEqX-@yPlE=J@mV+)q;6#x_^W6Ro)!4-R*Uw9-qzcVWu)Ki02bSw!r3} zOXcHnmffP4x0#bN^<%3lF2q9 zG!M%$`bbuAuSPe+h=z)uTOYy<$Ex)iF$5Rs@~}tCMRjvy5NZ(pmDjsPSJNgvj;zGiBVvzi$$Q1$!2+gAPCu?$#B6J8=C=_{9yyaTBUfxyB zwjjf~bqeo{b%Rvn}a6!c*lb%otjXQ*Of$CgsnZ~lNSQ2%L$(80UhI~9Gv ztvBgh135GJvJLH%G&M3X^mG2+S+=ev1kU>V8Go2a-6!1G@5_{!)T((ds3{7^i{kV~(?xi2)ieW@2z~qBZuX9pl9%HL{>ANDG z{-=buU0|h$$$B!qDwE@R6^-NSg^E=Pq9{0Xo@Q;LQag*7_1T1A%(W{cKYk8Egzlike3Bxsd~W#y(*RA@R%-UH_vS2rv9Fxp zgIHgu0~l#~h$DQkn?qqjOIaOP3nBfoV!Yp~ph`kHpHu5N4qI9Oi=%ILSD;{KhPC_C zmUzi~0&}Wlb#Z$Rc_IOM4ob8A?^5lO*iTUiPV01vDPQM%GAAiyi53s1*iC-%=f*Jz zJ~5)rB0m&zI_Y-NP>0CI1ZHW zDOcS{^I0rcWHNK}y$A!H%Eg?E61;&V*jPjf>NPk+vez@|oP}1&5hBjzN@KQ)vdmVr z7mJM4$UJvf+Qo=Zz_gU4=&+y5*`%$DKcY5E52lN6>@&w&vugJP<#oP6hNe*-@tdc^ z_iS)!T_Idn3Fz_%J@`<18KUMUzvwUsbC)(bCI)W4aqhxSC0N}VD?5T}t}tinUElmu z#3S_xF*+g96b?9YRl%GP+sSppGc`Fm$y>TVJ9sK&JYA-h@ol0OW99acpjVy!RJ3I^ zlkaOxk^26=%9LtF42HGoo%O^dfd-}%Ntw*+41Caz@e;>|7O9Q`0;DR05)hG3zNsQ4 zvgk^2TbKGHh>p@0L&}wSqs=%$fK)>;4sPPJ38uVOGN&5Kcn<#!EJ;j`pAHESVfIE+NMR2+i$j5BG|c67S2eCbL+%g;FW+ii!a7~1i4%`OeMNUc=9NnW6 z?(!=tgh|X!&c`tz$#|XWlUW#I@B9Se0vTOIj5MU_^tEYNey^Ax1maOk!_*^8(yLwt z5k-L%;XmAraCWg+$cEr3a5x{$h32YISaVjA%$J**x8rXnB#{<9s0-(El+n~;;d#%Dz5_|D+a9C1lnn72Y)uLlS!o+H>xqXxrrc}=KvkCT@ zz!zPadsjT^!!mV9rLX2E?%=Le*B;pSMUw)6UzbSg;a4^VmF8R-oPj6wjr|6gZnkz} z6yvLsY?+B!6RW4^#tL>gt`B%tUC~Cyjt6bjnhF)))|p-pf%lVhlWu#`)V=zt`Eb2; z!^S0fiORJTa93xJ-%HMIWfE$d= zikV*b;g9Cx_=D=pT(%Rl;OI4HjTiG_1|MB}0Of&DCl(a-{<7y9gF_7pBgYc8RDZJD zlAv3a3pk+^Nb+7~?7%Z*)v#bPZCnrZMOeQ zCe5^xCUu$d^e=w`a8{QV`tS)&ex6r8{DNuW9H1s%T4H9}kG3$8&z+i5@ z6ZM?(8CCZtFZgST@oIQ0utbPHum2ieEO572thqw_)l%LR_Wm-5^u+Ey$qn2ROYf?! zJbQM2_0Af@wrMhX`3R1b#9LSEgyH17i3phgQiGGpq1hZ;f1$KbsO+&;!QQn2n=;q! ztCQ0quQ=~5JQ#bSMQGDy5pB`Vvec68b zssYx_tW&q_j-&us^b7lN;0o$<)*SDvJ zzKaX6gU@SW8`ujHTcy2y=(MDkMc{NrvWo6lsMptQIfeJQ$ZX)K z`QZo9((g;_dz5z*5iGEJ+1#5(`s^$hL_*_V_kp9 z&|P-!V5S-(Y&5?atiYiuH%DKm+W?FQ(5~2>#*R%ae6VfvC~zK!j(fm7OD+hNQ`BiJ z5(vyXeRq%o&F0H zmhhnyn6{xwt5D`!e)>Ku>@Spd+ALaq+e#6_u5AX0M~c%t3%(utvi87|smc$04(_uk zJOQBXy}(byw)>kzvnXjc{mxasbl`*dj^E9;Gvb`xPzN%w%6-mF){wiG%FZCK@C6O9 z-BN4yf}G#X8CwVK-tB(?hy*e@v z^66(QBxE8KqmGj|M-eo`ONCauC1Prw&bQCxbOb^$pzWOX*?y&8UqOOS$>J57R<@qP zuUhDbG|NAXPb{F*@2o8`z_9n35;24?GvT8X{q3`DZrY};3;Pxv8z;2?2mpgx>W7t$dJhg zGwb-$xpo5WNjE=2yWd6YtAZf&p(UZ0zGWH!akHi2gAO-NA*W}cxQ?)nR6p|w7%`k?`^M`O0RVkBl|cAY~rQUPhtQNkb|ZWO1V zRTgJgMO1Ad{b91jJ5RUfbgfOUnDU5!nUA3&H}vK6L^llZW+x(2}2g?>4b-P>^a)NRd{O0EMRhNz{`0 zuo!r^gnk8{Z9AwDlzmGD5j0zFoCK3(7*n4g$x4gy$MTT2vgrQg+#_E>`N+z*I^ z*=*l+iBzrV>XN=3FG@wX%uj03K;HJgExpmgw>ZJQL?_YAhA80Y$dM)K$i3abj~8+@ zW_7=YSg-27b91ld^n4}LSm&^?nKzA3_ncIo@~ohaoinRMx`dduuz0VP7+w&mx4Pw` zugox1O&yEtW(suOwa#ZSTOnEbZL;-}=zfV8JUq?x)juU{xIFYX*{LpB3Q&yY8DGD0 za_x0S^}cK|*DSw-iL9G+V;djVxuC%>w!c@bs+7Aj(8-i5rl)j5W_XJn1fPM^+n(No zk}9H7-r8GM-#54bQId!Yar7F5huR&q1JH(p?Y+}{fI$$1zd|87=|!V(Mpmc5puzZ9 z$Y+_6X~DN;Zyw@cQT6cUdjG{5C=J4+d!%IlW}tp>4&MHjNXXF)TrR4s^tmJCsn9rBV*y_9KFYcc?&D`5O?)9~zGM33g6;WixbWnEa( z`KmVsNv_(ziF%C3*%2xje_jzl?wF6$1k$58Q^F{58>XG5U=S ztE)?awrcNB6g#8wpVx=h44Q$Dqw98MwX`xA2MQ%F2DilsQF6{T=`OBo=BufQf{-2e zF;00bQd3Vd>(l;948bU=DE8l8Q>U)kn?=3ltqKjLtuNi0&o3urBneA!&K4`5BvkwU z2$8D=M^xGfN^Z(usqddl(aAH7Hql^|Xyk@pz>JUIN( z%J7(s$q_D9Vm-T{X6pVmxm63&v}YpKvYYY&e3)pOV@RLU`WtuBc`zyPeUcNeR?DUTeBI7w3%X+$f)rg=N`1$qy_5Ccm zcu?3$O%&=}c=GKDn>yzVth=wGy8t=37pSN-RWcPu&7#J(JT%O2lIubnssraX75@xV zLUz=u+o|=MW4@qeuBeKu2UI{?nRk_9>*!MTSK=j9A4JuOw-!!iqV-~(fGSA6YDvyErBTk9WdlUu@R z*n2@iEEeWlYdb#!u2T$a>fbJRYbaPHslh7dy`4_ZYyb(FxyQFOjfO}vYGEp1+fF?B z*VgClNMOa6i`DULV7&cDx7!VP5n?av=ThRz67$TaCjIzLx=(rm6q!{>{Ii>3t-eyN zqk`*3B0GxD5TR4=SkifyJ3N}-C6Hu^6HNJz0B3~*FGnX4WB3c3)Q=qUlbu!L_%pEQ zv->eoCzqT{e+sTx)E>t3$uaukYBMHuq$lYv3FQB^iii3X-BDU{&2bt(Ja@ zDmdrPpdQpv{uM=qXsf<*&K6s7$Y}3j=#vc{zb(MTSlL97RS;SCY6l#;G@shC4E@@1d{& zAI^61&Kjscol5d1&pgsid8IbsXIf}J!%G%i4g#_BObIKH(`!CPQzA87`p?Mr&?pHX z*9OS0j;kn6-T`ScaMiu?s>RgCyIfB_jeud8CGJDeZ-2fF*Jh?0Yt_=j7I27H;8|5+ z3S*U>KCf@_l|ljNX;yr3EZ=Vt)o??T3&%uI+-J21jSR992~klLe%bn*C~N%*PyW*< za3vhnBFq3H78D@+W9ru7pKx#jBc-a^(Cvbi4I zc*_YPS7Kc4q)|>=fkrib@PeR#2vxG1@>dGVwQdK}{|Q8iU}GqSHACs|H^K?0xqw3{ zrira2%U)<~ zS8yFV!4WGytLBDbj`gKRwU`+ zq0LvgID;X^8rhc@wJ>_Ul=w-DS0cr$DC?nhI*4st>q}|gtcgpe0lBt@Tx%10G`%cG z!7Hw6sP>0nE2p60@3a~7?}zGTTDWLJ@BL)!akYfU7KxfV1yazhBp zQQ%B|>D_%Ln6~H4@c_;`>ROLf7oA~IXuojYEwh+_spuiY(FA8rpqb z$zJEH&mhHP^lSF(&7L3g5wVELUJM0G&=p0L1etd~*qAX0cCGJlzcCBaiP7C}%~>*r%zc1@dY`2`i@^#XYwq8N zzu>Fam1LwsW=J8nS${nZYI`4(u7*^c@U{=+dR-kI-8HaO`TGRdzXB;W%%?A~|6g+I zzYqOKiTn@v-$;`Gr9}S!EJ^-HiR4CkHT(_@rO>&NN$7ht_;%zHN>)lqvRuOG$NvGM CrpJ*0 literal 0 HcmV?d00001 diff --git a/vendor/github.com/oschwald/maxminddb-golang/LICENSE b/vendor/github.com/oschwald/maxminddb-golang/LICENSE new file mode 100644 index 0000000000..2969677f15 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2015, Gregory J. Oschwald + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/oschwald/maxminddb-golang/README.md b/vendor/github.com/oschwald/maxminddb-golang/README.md new file mode 100644 index 0000000000..cdd6bd1a85 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/README.md @@ -0,0 +1,38 @@ +# MaxMind DB Reader for Go # + +[![Build Status](https://travis-ci.org/oschwald/maxminddb-golang.png?branch=master)](https://travis-ci.org/oschwald/maxminddb-golang) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/4j2f9oep8nnfrmov/branch/master?svg=true)](https://ci.appveyor.com/project/oschwald/maxminddb-golang/branch/master) +[![GoDoc](https://godoc.org/github.com/oschwald/maxminddb-golang?status.png)](https://godoc.org/github.com/oschwald/maxminddb-golang) + +This is a Go reader for the MaxMind DB format. Although this can be used to +read [GeoLite2](http://dev.maxmind.com/geoip/geoip2/geolite2/) and +[GeoIP2](https://www.maxmind.com/en/geoip2-databases) databases, +[geoip2](https://github.com/oschwald/geoip2-golang) provides a higher-level +API for doing so. + +This is not an official MaxMind API. + +## Installation ## + +``` +go get github.com/oschwald/maxminddb-golang +``` + +## Usage ## + +[See GoDoc](http://godoc.org/github.com/oschwald/maxminddb-golang) for +documentation and examples. + +## Examples ## + +See [GoDoc](http://godoc.org/github.com/oschwald/maxminddb-golang) or +`example_test.go` for examples. + +## Contributing ## + +Contributions welcome! Please fork the repository and open a pull request +with your changes. + +## License ## + +This is free software, licensed under the ISC License. diff --git a/vendor/github.com/oschwald/maxminddb-golang/appveyor.yml b/vendor/github.com/oschwald/maxminddb-golang/appveyor.yml new file mode 100644 index 0000000000..e2bb9dd237 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/appveyor.yml @@ -0,0 +1,19 @@ +version: "{build}" + +os: Windows Server 2012 R2 + +clone_folder: c:\gopath\src\github.com\oschwald\maxminddb-golang + +environment: + GOPATH: c:\gopath + +install: + - echo %PATH% + - echo %GOPATH% + - git submodule update --init --recursive + - go version + - go env + - go get -v -t ./... + +build_script: + - go test -v ./... diff --git a/vendor/github.com/oschwald/maxminddb-golang/decoder.go b/vendor/github.com/oschwald/maxminddb-golang/decoder.go new file mode 100644 index 0000000000..6e4d7e5b83 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/decoder.go @@ -0,0 +1,721 @@ +package maxminddb + +import ( + "encoding/binary" + "math" + "math/big" + "reflect" + "sync" +) + +type decoder struct { + buffer []byte +} + +type dataType int + +const ( + _Extended dataType = iota + _Pointer + _String + _Float64 + _Bytes + _Uint16 + _Uint32 + _Map + _Int32 + _Uint64 + _Uint128 + _Slice + _Container + _Marker + _Bool + _Float32 +) + +const ( + // This is the value used in libmaxminddb + maximumDataStructureDepth = 512 +) + +func (d *decoder) decode(offset uint, result reflect.Value, depth int) (uint, error) { + if depth > maximumDataStructureDepth { + return 0, newInvalidDatabaseError("exceeded maximum data structure depth; database is likely corrupt") + } + typeNum, size, newOffset, err := d.decodeCtrlData(offset) + if err != nil { + return 0, err + } + + if typeNum != _Pointer && result.Kind() == reflect.Uintptr { + result.Set(reflect.ValueOf(uintptr(offset))) + return d.nextValueOffset(offset, 1) + } + return d.decodeFromType(typeNum, size, newOffset, result, depth+1) +} + +func (d *decoder) decodeCtrlData(offset uint) (dataType, uint, uint, error) { + newOffset := offset + 1 + if offset >= uint(len(d.buffer)) { + return 0, 0, 0, newOffsetError() + } + ctrlByte := d.buffer[offset] + + typeNum := dataType(ctrlByte >> 5) + if typeNum == _Extended { + if newOffset >= uint(len(d.buffer)) { + return 0, 0, 0, newOffsetError() + } + typeNum = dataType(d.buffer[newOffset] + 7) + newOffset++ + } + + var size uint + size, newOffset, err := d.sizeFromCtrlByte(ctrlByte, newOffset, typeNum) + return typeNum, size, newOffset, err +} + +func (d *decoder) sizeFromCtrlByte(ctrlByte byte, offset uint, typeNum dataType) (uint, uint, error) { + size := uint(ctrlByte & 0x1f) + if typeNum == _Extended { + return size, offset, nil + } + + var bytesToRead uint + if size < 29 { + return size, offset, nil + } + + bytesToRead = size - 28 + newOffset := offset + bytesToRead + if newOffset > uint(len(d.buffer)) { + return 0, 0, newOffsetError() + } + if size == 29 { + return 29 + uint(d.buffer[offset]), offset + 1, nil + } + + sizeBytes := d.buffer[offset:newOffset] + + switch { + case size == 30: + size = 285 + uintFromBytes(0, sizeBytes) + case size > 30: + size = uintFromBytes(0, sizeBytes) + 65821 + } + return size, newOffset, nil +} + +func (d *decoder) decodeFromType( + dtype dataType, + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + result = d.indirect(result) + + // For these types, size has a special meaning + switch dtype { + case _Bool: + return d.unmarshalBool(size, offset, result) + case _Map: + return d.unmarshalMap(size, offset, result, depth) + case _Pointer: + return d.unmarshalPointer(size, offset, result, depth) + case _Slice: + return d.unmarshalSlice(size, offset, result, depth) + } + + // For the remaining types, size is the byte size + if offset+size > uint(len(d.buffer)) { + return 0, newOffsetError() + } + switch dtype { + case _Bytes: + return d.unmarshalBytes(size, offset, result) + case _Float32: + return d.unmarshalFloat32(size, offset, result) + case _Float64: + return d.unmarshalFloat64(size, offset, result) + case _Int32: + return d.unmarshalInt32(size, offset, result) + case _String: + return d.unmarshalString(size, offset, result) + case _Uint16: + return d.unmarshalUint(size, offset, result, 16) + case _Uint32: + return d.unmarshalUint(size, offset, result, 32) + case _Uint64: + return d.unmarshalUint(size, offset, result, 64) + case _Uint128: + return d.unmarshalUint128(size, offset, result) + default: + return 0, newInvalidDatabaseError("unknown type: %d", dtype) + } +} + +func (d *decoder) unmarshalBool(size uint, offset uint, result reflect.Value) (uint, error) { + if size > 1 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (bool size of %v)", size) + } + value, newOffset, err := d.decodeBool(size, offset) + if err != nil { + return 0, err + } + switch result.Kind() { + case reflect.Bool: + result.SetBool(value) + return newOffset, nil + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +// indirect follows pointers and create values as necessary. This is +// heavily based on encoding/json as my original version had a subtle +// bug. This method should be considered to be licensed under +// https://golang.org/LICENSE +func (d *decoder) indirect(result reflect.Value) reflect.Value { + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if result.Kind() == reflect.Interface && !result.IsNil() { + e := result.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() { + result = e + continue + } + } + + if result.Kind() != reflect.Ptr { + break + } + + if result.IsNil() { + result.Set(reflect.New(result.Type().Elem())) + } + result = result.Elem() + } + return result +} + +var sliceType = reflect.TypeOf([]byte{}) + +func (d *decoder) unmarshalBytes(size uint, offset uint, result reflect.Value) (uint, error) { + value, newOffset, err := d.decodeBytes(size, offset) + if err != nil { + return 0, err + } + switch result.Kind() { + case reflect.Slice: + if result.Type() == sliceType { + result.SetBytes(value) + return newOffset, nil + } + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +func (d *decoder) unmarshalFloat32(size uint, offset uint, result reflect.Value) (uint, error) { + if size != 4 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float32 size of %v)", size) + } + value, newOffset, err := d.decodeFloat32(size, offset) + if err != nil { + return 0, err + } + + switch result.Kind() { + case reflect.Float32, reflect.Float64: + result.SetFloat(float64(value)) + return newOffset, nil + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +func (d *decoder) unmarshalFloat64(size uint, offset uint, result reflect.Value) (uint, error) { + + if size != 8 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float 64 size of %v)", size) + } + value, newOffset, err := d.decodeFloat64(size, offset) + if err != nil { + return 0, err + } + switch result.Kind() { + case reflect.Float32, reflect.Float64: + if result.OverflowFloat(value) { + return 0, newUnmarshalTypeError(value, result.Type()) + } + result.SetFloat(value) + return newOffset, nil + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +func (d *decoder) unmarshalInt32(size uint, offset uint, result reflect.Value) (uint, error) { + if size > 4 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (int32 size of %v)", size) + } + value, newOffset, err := d.decodeInt(size, offset) + if err != nil { + return 0, err + } + + switch result.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n := int64(value) + if !result.OverflowInt(n) { + result.SetInt(n) + return newOffset, nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n := uint64(value) + if !result.OverflowUint(n) { + result.SetUint(n) + return newOffset, nil + } + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +func (d *decoder) unmarshalMap( + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + result = d.indirect(result) + switch result.Kind() { + default: + return 0, newUnmarshalTypeError("map", result.Type()) + case reflect.Struct: + return d.decodeStruct(size, offset, result, depth) + case reflect.Map: + return d.decodeMap(size, offset, result, depth) + case reflect.Interface: + if result.NumMethod() == 0 { + rv := reflect.ValueOf(make(map[string]interface{}, size)) + newOffset, err := d.decodeMap(size, offset, rv, depth) + result.Set(rv) + return newOffset, err + } + return 0, newUnmarshalTypeError("map", result.Type()) + } +} + +func (d *decoder) unmarshalPointer(size uint, offset uint, result reflect.Value, depth int) (uint, error) { + pointer, newOffset, err := d.decodePointer(size, offset) + if err != nil { + return 0, err + } + _, err = d.decode(pointer, result, depth) + return newOffset, err +} + +func (d *decoder) unmarshalSlice( + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + switch result.Kind() { + case reflect.Slice: + return d.decodeSlice(size, offset, result, depth) + case reflect.Interface: + if result.NumMethod() == 0 { + a := []interface{}{} + rv := reflect.ValueOf(&a).Elem() + newOffset, err := d.decodeSlice(size, offset, rv, depth) + result.Set(rv) + return newOffset, err + } + } + return 0, newUnmarshalTypeError("array", result.Type()) +} + +func (d *decoder) unmarshalString(size uint, offset uint, result reflect.Value) (uint, error) { + value, newOffset, err := d.decodeString(size, offset) + + if err != nil { + return 0, err + } + switch result.Kind() { + case reflect.String: + result.SetString(value) + return newOffset, nil + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) + +} + +func (d *decoder) unmarshalUint(size uint, offset uint, result reflect.Value, uintType uint) (uint, error) { + if size > uintType/8 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint%v size of %v)", uintType, size) + } + + value, newOffset, err := d.decodeUint(size, offset) + if err != nil { + return 0, err + } + + switch result.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n := int64(value) + if !result.OverflowInt(n) { + result.SetInt(n) + return newOffset, nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if !result.OverflowUint(value) { + result.SetUint(value) + return newOffset, nil + } + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +var bigIntType = reflect.TypeOf(big.Int{}) + +func (d *decoder) unmarshalUint128(size uint, offset uint, result reflect.Value) (uint, error) { + if size > 16 { + return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint128 size of %v)", size) + } + value, newOffset, err := d.decodeUint128(size, offset) + if err != nil { + return 0, err + } + + switch result.Kind() { + case reflect.Struct: + if result.Type() == bigIntType { + result.Set(reflect.ValueOf(*value)) + return newOffset, nil + } + case reflect.Interface: + if result.NumMethod() == 0 { + result.Set(reflect.ValueOf(value)) + return newOffset, nil + } + } + return newOffset, newUnmarshalTypeError(value, result.Type()) +} + +func (d *decoder) decodeBool(size uint, offset uint) (bool, uint, error) { + return size != 0, offset, nil +} + +func (d *decoder) decodeBytes(size uint, offset uint) ([]byte, uint, error) { + newOffset := offset + size + bytes := make([]byte, size) + copy(bytes, d.buffer[offset:newOffset]) + return bytes, newOffset, nil +} + +func (d *decoder) decodeFloat64(size uint, offset uint) (float64, uint, error) { + newOffset := offset + size + bits := binary.BigEndian.Uint64(d.buffer[offset:newOffset]) + return math.Float64frombits(bits), newOffset, nil +} + +func (d *decoder) decodeFloat32(size uint, offset uint) (float32, uint, error) { + newOffset := offset + size + bits := binary.BigEndian.Uint32(d.buffer[offset:newOffset]) + return math.Float32frombits(bits), newOffset, nil +} + +func (d *decoder) decodeInt(size uint, offset uint) (int, uint, error) { + newOffset := offset + size + var val int32 + for _, b := range d.buffer[offset:newOffset] { + val = (val << 8) | int32(b) + } + return int(val), newOffset, nil +} + +func (d *decoder) decodeMap( + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + if result.IsNil() { + result.Set(reflect.MakeMap(result.Type())) + } + + for i := uint(0); i < size; i++ { + var key []byte + var err error + key, offset, err = d.decodeKey(offset) + + if err != nil { + return 0, err + } + + value := reflect.New(result.Type().Elem()) + offset, err = d.decode(offset, value, depth) + if err != nil { + return 0, err + } + result.SetMapIndex(reflect.ValueOf(string(key)), value.Elem()) + } + return offset, nil +} + +func (d *decoder) decodePointer( + size uint, + offset uint, +) (uint, uint, error) { + pointerSize := ((size >> 3) & 0x3) + 1 + newOffset := offset + pointerSize + if newOffset > uint(len(d.buffer)) { + return 0, 0, newOffsetError() + } + pointerBytes := d.buffer[offset:newOffset] + var prefix uint + if pointerSize == 4 { + prefix = 0 + } else { + prefix = uint(size & 0x7) + } + unpacked := uintFromBytes(prefix, pointerBytes) + + var pointerValueOffset uint + switch pointerSize { + case 1: + pointerValueOffset = 0 + case 2: + pointerValueOffset = 2048 + case 3: + pointerValueOffset = 526336 + case 4: + pointerValueOffset = 0 + } + + pointer := unpacked + pointerValueOffset + + return pointer, newOffset, nil +} + +func (d *decoder) decodeSlice( + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + result.Set(reflect.MakeSlice(result.Type(), int(size), int(size))) + for i := 0; i < int(size); i++ { + var err error + offset, err = d.decode(offset, result.Index(i), depth) + if err != nil { + return 0, err + } + } + return offset, nil +} + +func (d *decoder) decodeString(size uint, offset uint) (string, uint, error) { + newOffset := offset + size + return string(d.buffer[offset:newOffset]), newOffset, nil +} + +type fieldsType struct { + namedFields map[string]int + anonymousFields []int +} + +var ( + fieldMap = map[reflect.Type]*fieldsType{} + fieldMapMu sync.RWMutex +) + +func (d *decoder) decodeStruct( + size uint, + offset uint, + result reflect.Value, + depth int, +) (uint, error) { + resultType := result.Type() + + fieldMapMu.RLock() + fields, ok := fieldMap[resultType] + fieldMapMu.RUnlock() + if !ok { + numFields := resultType.NumField() + namedFields := make(map[string]int, numFields) + var anonymous []int + for i := 0; i < numFields; i++ { + field := resultType.Field(i) + + fieldName := field.Name + if tag := field.Tag.Get("maxminddb"); tag != "" { + if tag == "-" { + continue + } + fieldName = tag + } + if field.Anonymous { + anonymous = append(anonymous, i) + continue + } + namedFields[fieldName] = i + } + fieldMapMu.Lock() + fields = &fieldsType{namedFields, anonymous} + fieldMap[resultType] = fields + fieldMapMu.Unlock() + } + + // This fills in embedded structs + for _, i := range fields.anonymousFields { + _, err := d.unmarshalMap(size, offset, result.Field(i), depth) + if err != nil { + return 0, err + } + } + + // This handles named fields + for i := uint(0); i < size; i++ { + var ( + err error + key []byte + ) + key, offset, err = d.decodeKey(offset) + if err != nil { + return 0, err + } + // The string() does not create a copy due to this compiler + // optimization: https://github.com/golang/go/issues/3512 + j, ok := fields.namedFields[string(key)] + if !ok { + offset, err = d.nextValueOffset(offset, 1) + if err != nil { + return 0, err + } + continue + } + + offset, err = d.decode(offset, result.Field(j), depth) + if err != nil { + return 0, err + } + } + return offset, nil +} + +func (d *decoder) decodeUint(size uint, offset uint) (uint64, uint, error) { + newOffset := offset + size + bytes := d.buffer[offset:newOffset] + + var val uint64 + for _, b := range bytes { + val = (val << 8) | uint64(b) + } + return val, newOffset, nil +} + +func (d *decoder) decodeUint128(size uint, offset uint) (*big.Int, uint, error) { + newOffset := offset + size + val := new(big.Int) + val.SetBytes(d.buffer[offset:newOffset]) + + return val, newOffset, nil +} + +func uintFromBytes(prefix uint, uintBytes []byte) uint { + val := prefix + for _, b := range uintBytes { + val = (val << 8) | uint(b) + } + return val +} + +// decodeKey decodes a map key into []byte slice. We use a []byte so that we +// can take advantage of https://github.com/golang/go/issues/3512 to avoid +// copying the bytes when decoding a struct. Previously, we achieved this by +// using unsafe. +func (d *decoder) decodeKey(offset uint) ([]byte, uint, error) { + typeNum, size, dataOffset, err := d.decodeCtrlData(offset) + if err != nil { + return nil, 0, err + } + if typeNum == _Pointer { + pointer, ptrOffset, err := d.decodePointer(size, dataOffset) + if err != nil { + return nil, 0, err + } + key, _, err := d.decodeKey(pointer) + return key, ptrOffset, err + } + if typeNum != _String { + return nil, 0, newInvalidDatabaseError("unexpected type when decoding string: %v", typeNum) + } + newOffset := dataOffset + size + if newOffset > uint(len(d.buffer)) { + return nil, 0, newOffsetError() + } + return d.buffer[dataOffset:newOffset], newOffset, nil +} + +// This function is used to skip ahead to the next value without decoding +// the one at the offset passed in. The size bits have different meanings for +// different data types +func (d *decoder) nextValueOffset(offset uint, numberToSkip uint) (uint, error) { + if numberToSkip == 0 { + return offset, nil + } + typeNum, size, offset, err := d.decodeCtrlData(offset) + if err != nil { + return 0, err + } + switch typeNum { + case _Pointer: + _, offset, err = d.decodePointer(size, offset) + if err != nil { + return 0, err + } + case _Map: + numberToSkip += 2 * size + case _Slice: + numberToSkip += size + case _Bool: + default: + offset += size + } + return d.nextValueOffset(offset, numberToSkip-1) +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/errors.go b/vendor/github.com/oschwald/maxminddb-golang/errors.go new file mode 100644 index 0000000000..132780019b --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/errors.go @@ -0,0 +1,42 @@ +package maxminddb + +import ( + "fmt" + "reflect" +) + +// InvalidDatabaseError is returned when the database contains invalid data +// and cannot be parsed. +type InvalidDatabaseError struct { + message string +} + +func newOffsetError() InvalidDatabaseError { + return InvalidDatabaseError{"unexpected end of database"} +} + +func newInvalidDatabaseError(format string, args ...interface{}) InvalidDatabaseError { + return InvalidDatabaseError{fmt.Sprintf(format, args...)} +} + +func (e InvalidDatabaseError) Error() string { + return e.message +} + +// UnmarshalTypeError is returned when the value in the database cannot be +// assigned to the specified data type. +type UnmarshalTypeError struct { + Value string // stringified copy of the database value that caused the error + Type reflect.Type // type of the value that could not be assign to +} + +func newUnmarshalTypeError(value interface{}, rType reflect.Type) UnmarshalTypeError { + return UnmarshalTypeError{ + Value: fmt.Sprintf("%v", value), + Type: rType, + } +} + +func (e UnmarshalTypeError) Error() string { + return fmt.Sprintf("maxminddb: cannot unmarshal %s into type %s", e.Value, e.Type.String()) +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/mmap_unix.go b/vendor/github.com/oschwald/maxminddb-golang/mmap_unix.go new file mode 100644 index 0000000000..d898d25704 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/mmap_unix.go @@ -0,0 +1,15 @@ +// +build !windows,!appengine + +package maxminddb + +import ( + "golang.org/x/sys/unix" +) + +func mmap(fd int, length int) (data []byte, err error) { + return unix.Mmap(fd, 0, length, unix.PROT_READ, unix.MAP_SHARED) +} + +func munmap(b []byte) (err error) { + return unix.Munmap(b) +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/mmap_windows.go b/vendor/github.com/oschwald/maxminddb-golang/mmap_windows.go new file mode 100644 index 0000000000..661250eca0 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/mmap_windows.go @@ -0,0 +1,85 @@ +// +build windows,!appengine + +package maxminddb + +// Windows support largely borrowed from mmap-go. +// +// Copyright 2011 Evan Shaw. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import ( + "errors" + "os" + "reflect" + "sync" + "unsafe" + + "golang.org/x/sys/windows" +) + +type memoryMap []byte + +// Windows +var handleLock sync.Mutex +var handleMap = map[uintptr]windows.Handle{} + +func mmap(fd int, length int) (data []byte, err error) { + h, errno := windows.CreateFileMapping(windows.Handle(fd), nil, + uint32(windows.PAGE_READONLY), 0, uint32(length), nil) + if h == 0 { + return nil, os.NewSyscallError("CreateFileMapping", errno) + } + + addr, errno := windows.MapViewOfFile(h, uint32(windows.FILE_MAP_READ), 0, + 0, uintptr(length)) + if addr == 0 { + return nil, os.NewSyscallError("MapViewOfFile", errno) + } + handleLock.Lock() + handleMap[addr] = h + handleLock.Unlock() + + m := memoryMap{} + dh := m.header() + dh.Data = addr + dh.Len = length + dh.Cap = dh.Len + + return m, nil +} + +func (m *memoryMap) header() *reflect.SliceHeader { + return (*reflect.SliceHeader)(unsafe.Pointer(m)) +} + +func flush(addr, len uintptr) error { + errno := windows.FlushViewOfFile(addr, len) + return os.NewSyscallError("FlushViewOfFile", errno) +} + +func munmap(b []byte) (err error) { + m := memoryMap(b) + dh := m.header() + + addr := dh.Data + length := uintptr(dh.Len) + + flush(addr, length) + err = windows.UnmapViewOfFile(addr) + if err != nil { + return err + } + + handleLock.Lock() + defer handleLock.Unlock() + handle, ok := handleMap[addr] + if !ok { + // should be impossible; we would've errored above + return errors.New("unknown base address") + } + delete(handleMap, addr) + + e := windows.CloseHandle(windows.Handle(handle)) + return os.NewSyscallError("CloseHandle", e) +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/reader.go b/vendor/github.com/oschwald/maxminddb-golang/reader.go new file mode 100644 index 0000000000..97b96070fc --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/reader.go @@ -0,0 +1,259 @@ +package maxminddb + +import ( + "bytes" + "errors" + "fmt" + "net" + "reflect" +) + +const ( + // NotFound is returned by LookupOffset when a matched root record offset + // cannot be found. + NotFound = ^uintptr(0) + + dataSectionSeparatorSize = 16 +) + +var metadataStartMarker = []byte("\xAB\xCD\xEFMaxMind.com") + +// Reader holds the data corresponding to the MaxMind DB file. Its only public +// field is Metadata, which contains the metadata from the MaxMind DB file. +type Reader struct { + hasMappedFile bool + buffer []byte + decoder decoder + Metadata Metadata + ipv4Start uint +} + +// Metadata holds the metadata decoded from the MaxMind DB file. In particular +// in has the format version, the build time as Unix epoch time, the database +// type and description, the IP version supported, and a slice of the natural +// languages included. +type Metadata struct { + BinaryFormatMajorVersion uint `maxminddb:"binary_format_major_version"` + BinaryFormatMinorVersion uint `maxminddb:"binary_format_minor_version"` + BuildEpoch uint `maxminddb:"build_epoch"` + DatabaseType string `maxminddb:"database_type"` + Description map[string]string `maxminddb:"description"` + IPVersion uint `maxminddb:"ip_version"` + Languages []string `maxminddb:"languages"` + NodeCount uint `maxminddb:"node_count"` + RecordSize uint `maxminddb:"record_size"` +} + +// FromBytes takes a byte slice corresponding to a MaxMind DB file and returns +// a Reader structure or an error. +func FromBytes(buffer []byte) (*Reader, error) { + metadataStart := bytes.LastIndex(buffer, metadataStartMarker) + + if metadataStart == -1 { + return nil, newInvalidDatabaseError("error opening database: invalid MaxMind DB file") + } + + metadataStart += len(metadataStartMarker) + metadataDecoder := decoder{buffer[metadataStart:]} + + var metadata Metadata + + rvMetdata := reflect.ValueOf(&metadata) + _, err := metadataDecoder.decode(0, rvMetdata, 0) + if err != nil { + return nil, err + } + + searchTreeSize := metadata.NodeCount * metadata.RecordSize / 4 + dataSectionStart := searchTreeSize + dataSectionSeparatorSize + dataSectionEnd := uint(metadataStart - len(metadataStartMarker)) + if dataSectionStart > dataSectionEnd { + return nil, newInvalidDatabaseError("the MaxMind DB contains invalid metadata") + } + d := decoder{ + buffer[searchTreeSize+dataSectionSeparatorSize : metadataStart-len(metadataStartMarker)], + } + + reader := &Reader{ + buffer: buffer, + decoder: d, + Metadata: metadata, + ipv4Start: 0, + } + + reader.ipv4Start, err = reader.startNode() + + return reader, err +} + +func (r *Reader) startNode() (uint, error) { + if r.Metadata.IPVersion != 6 { + return 0, nil + } + + nodeCount := r.Metadata.NodeCount + + node := uint(0) + var err error + for i := 0; i < 96 && node < nodeCount; i++ { + node, err = r.readNode(node, 0) + if err != nil { + return 0, err + } + } + return node, err +} + +// Lookup takes an IP address as a net.IP structure and a pointer to the +// result value to Decode into. +func (r *Reader) Lookup(ipAddress net.IP, result interface{}) error { + if r.buffer == nil { + return errors.New("cannot call Lookup on a closed database") + } + pointer, err := r.lookupPointer(ipAddress) + if pointer == 0 || err != nil { + return err + } + return r.retrieveData(pointer, result) +} + +// LookupOffset maps an argument net.IP to a corresponding record offset in the +// database. NotFound is returned if no such record is found, and a record may +// otherwise be extracted by passing the returned offset to Decode. LookupOffset +// is an advanced API, which exists to provide clients with a means to cache +// previously-decoded records. +func (r *Reader) LookupOffset(ipAddress net.IP) (uintptr, error) { + if r.buffer == nil { + return 0, errors.New("cannot call LookupOffset on a closed database") + } + pointer, err := r.lookupPointer(ipAddress) + if pointer == 0 || err != nil { + return NotFound, err + } + return r.resolveDataPointer(pointer) +} + +// Decode the record at |offset| into |result|. The result value pointed to +// must be a data value that corresponds to a record in the database. This may +// include a struct representation of the data, a map capable of holding the +// data or an empty interface{} value. +// +// If result is a pointer to a struct, the struct need not include a field +// for every value that may be in the database. If a field is not present in +// the structure, the decoder will not decode that field, reducing the time +// required to decode the record. +// +// As a special case, a struct field of type uintptr will be used to capture +// the offset of the value. Decode may later be used to extract the stored +// value from the offset. MaxMind DBs are highly normalized: for example in +// the City database, all records of the same country will reference a +// single representative record for that country. This uintptr behavior allows +// clients to leverage this normalization in their own sub-record caching. +func (r *Reader) Decode(offset uintptr, result interface{}) error { + if r.buffer == nil { + return errors.New("cannot call Decode on a closed database") + } + return r.decode(offset, result) +} + +func (r *Reader) decode(offset uintptr, result interface{}) error { + rv := reflect.ValueOf(result) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return errors.New("result param must be a pointer") + } + + _, err := r.decoder.decode(uint(offset), reflect.ValueOf(result), 0) + return err +} + +func (r *Reader) lookupPointer(ipAddress net.IP) (uint, error) { + if ipAddress == nil { + return 0, errors.New("ipAddress passed to Lookup cannot be nil") + } + + ipV4Address := ipAddress.To4() + if ipV4Address != nil { + ipAddress = ipV4Address + } + if len(ipAddress) == 16 && r.Metadata.IPVersion == 4 { + return 0, fmt.Errorf("error looking up '%s': you attempted to look up an IPv6 address in an IPv4-only database", ipAddress.String()) + } + + return r.findAddressInTree(ipAddress) +} + +func (r *Reader) findAddressInTree(ipAddress net.IP) (uint, error) { + + bitCount := uint(len(ipAddress) * 8) + + var node uint + if bitCount == 32 { + node = r.ipv4Start + } + + nodeCount := r.Metadata.NodeCount + + for i := uint(0); i < bitCount && node < nodeCount; i++ { + bit := uint(1) & (uint(ipAddress[i>>3]) >> (7 - (i % 8))) + + var err error + node, err = r.readNode(node, bit) + if err != nil { + return 0, err + } + } + if node == nodeCount { + // Record is empty + return 0, nil + } else if node > nodeCount { + return node, nil + } + + return 0, newInvalidDatabaseError("invalid node in search tree") +} + +func (r *Reader) readNode(nodeNumber uint, index uint) (uint, error) { + RecordSize := r.Metadata.RecordSize + + baseOffset := nodeNumber * RecordSize / 4 + + var nodeBytes []byte + var prefix uint + switch RecordSize { + case 24: + offset := baseOffset + index*3 + nodeBytes = r.buffer[offset : offset+3] + case 28: + prefix = uint(r.buffer[baseOffset+3]) + if index != 0 { + prefix &= 0x0F + } else { + prefix = (0xF0 & prefix) >> 4 + } + offset := baseOffset + index*4 + nodeBytes = r.buffer[offset : offset+3] + case 32: + offset := baseOffset + index*4 + nodeBytes = r.buffer[offset : offset+4] + default: + return 0, newInvalidDatabaseError("unknown record size: %d", RecordSize) + } + return uintFromBytes(prefix, nodeBytes), nil +} + +func (r *Reader) retrieveData(pointer uint, result interface{}) error { + offset, err := r.resolveDataPointer(pointer) + if err != nil { + return err + } + return r.decode(offset, result) +} + +func (r *Reader) resolveDataPointer(pointer uint) (uintptr, error) { + var resolved = uintptr(pointer - r.Metadata.NodeCount - dataSectionSeparatorSize) + + if resolved > uintptr(len(r.buffer)) { + return 0, newInvalidDatabaseError("the MaxMind DB file's search tree is corrupt") + } + return resolved, nil +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/reader_appengine.go b/vendor/github.com/oschwald/maxminddb-golang/reader_appengine.go new file mode 100644 index 0000000000..d200f9fe05 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/reader_appengine.go @@ -0,0 +1,28 @@ +// +build appengine + +package maxminddb + +import "io/ioutil" + +// Open takes a string path to a MaxMind DB file and returns a Reader +// structure or an error. The database file is opened using a memory map, +// except on Google App Engine where mmap is not supported; there the database +// is loaded into memory. Use the Close method on the Reader object to return +// the resources to the system. +func Open(file string) (*Reader, error) { + bytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + return FromBytes(bytes) +} + +// Close unmaps the database file from virtual memory and returns the +// resources to the system. If called on a Reader opened using FromBytes +// or Open on Google App Engine, this method sets the underlying buffer +// to nil, returning the resources to the system. +func (r *Reader) Close() error { + r.buffer = nil + return nil +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/reader_other.go b/vendor/github.com/oschwald/maxminddb-golang/reader_other.go new file mode 100644 index 0000000000..2a89fa676e --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/reader_other.go @@ -0,0 +1,63 @@ +// +build !appengine + +package maxminddb + +import ( + "os" + "runtime" +) + +// Open takes a string path to a MaxMind DB file and returns a Reader +// structure or an error. The database file is opened using a memory map, +// except on Google App Engine where mmap is not supported; there the database +// is loaded into memory. Use the Close method on the Reader object to return +// the resources to the system. +func Open(file string) (*Reader, error) { + mapFile, err := os.Open(file) + if err != nil { + return nil, err + } + defer func() { + if rerr := mapFile.Close(); rerr != nil { + err = rerr + } + }() + + stats, err := mapFile.Stat() + if err != nil { + return nil, err + } + + fileSize := int(stats.Size()) + mmap, err := mmap(int(mapFile.Fd()), fileSize) + if err != nil { + return nil, err + } + + reader, err := FromBytes(mmap) + if err != nil { + if err2 := munmap(mmap); err2 != nil { + // failing to unmap the file is probably the more severe error + return nil, err2 + } + return nil, err + } + + reader.hasMappedFile = true + runtime.SetFinalizer(reader, (*Reader).Close) + return reader, err +} + +// Close unmaps the database file from virtual memory and returns the +// resources to the system. If called on a Reader opened using FromBytes +// or Open on Google App Engine, this method does nothing. +func (r *Reader) Close() error { + var err error + if r.hasMappedFile { + runtime.SetFinalizer(r, nil) + r.hasMappedFile = false + err = munmap(r.buffer) + } + r.buffer = nil + return err +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/traverse.go b/vendor/github.com/oschwald/maxminddb-golang/traverse.go new file mode 100644 index 0000000000..f9b443c0df --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/traverse.go @@ -0,0 +1,108 @@ +package maxminddb + +import "net" + +// Internal structure used to keep track of nodes we still need to visit. +type netNode struct { + ip net.IP + bit uint + pointer uint +} + +// Networks represents a set of subnets that we are iterating over. +type Networks struct { + reader *Reader + nodes []netNode // Nodes we still have to visit. + lastNode netNode + err error +} + +// Networks returns an iterator that can be used to traverse all networks in +// the database. +// +// Please note that a MaxMind DB may map IPv4 networks into several locations +// in in an IPv6 database. This iterator will iterate over all of these +// locations separately. +func (r *Reader) Networks() *Networks { + s := 4 + if r.Metadata.IPVersion == 6 { + s = 16 + } + return &Networks{ + reader: r, + nodes: []netNode{ + { + ip: make(net.IP, s), + }, + }, + } +} + +// Next prepares the next network for reading with the Network method. It +// returns true if there is another network to be processed and false if there +// are no more networks or if there is an error. +func (n *Networks) Next() bool { + for len(n.nodes) > 0 { + node := n.nodes[len(n.nodes)-1] + n.nodes = n.nodes[:len(n.nodes)-1] + + for { + if node.pointer < n.reader.Metadata.NodeCount { + ipRight := make(net.IP, len(node.ip)) + copy(ipRight, node.ip) + if len(ipRight) <= int(node.bit>>3) { + n.err = newInvalidDatabaseError( + "invalid search tree at %v/%v", ipRight, node.bit) + return false + } + ipRight[node.bit>>3] |= 1 << (7 - (node.bit % 8)) + + rightPointer, err := n.reader.readNode(node.pointer, 1) + if err != nil { + n.err = err + return false + } + + node.bit++ + n.nodes = append(n.nodes, netNode{ + pointer: rightPointer, + ip: ipRight, + bit: node.bit, + }) + + node.pointer, err = n.reader.readNode(node.pointer, 0) + if err != nil { + n.err = err + return false + } + + } else if node.pointer > n.reader.Metadata.NodeCount { + n.lastNode = node + return true + } else { + break + } + } + } + + return false +} + +// Network returns the current network or an error if there is a problem +// decoding the data for the network. It takes a pointer to a result value to +// decode the network's data into. +func (n *Networks) Network(result interface{}) (*net.IPNet, error) { + if err := n.reader.retrieveData(n.lastNode.pointer, result); err != nil { + return nil, err + } + + return &net.IPNet{ + IP: n.lastNode.ip, + Mask: net.CIDRMask(int(n.lastNode.bit), len(n.lastNode.ip)*8), + }, nil +} + +// Err returns an error, if any, that was encountered during iteration. +func (n *Networks) Err() error { + return n.err +} diff --git a/vendor/github.com/oschwald/maxminddb-golang/verifier.go b/vendor/github.com/oschwald/maxminddb-golang/verifier.go new file mode 100644 index 0000000000..ace9d35c40 --- /dev/null +++ b/vendor/github.com/oschwald/maxminddb-golang/verifier.go @@ -0,0 +1,185 @@ +package maxminddb + +import "reflect" + +type verifier struct { + reader *Reader +} + +// Verify checks that the database is valid. It validates the search tree, +// the data section, and the metadata section. This verifier is stricter than +// the specification and may return errors on databases that are readable. +func (r *Reader) Verify() error { + v := verifier{r} + if err := v.verifyMetadata(); err != nil { + return err + } + + return v.verifyDatabase() +} + +func (v *verifier) verifyMetadata() error { + metadata := v.reader.Metadata + + if metadata.BinaryFormatMajorVersion != 2 { + return testError( + "binary_format_major_version", + 2, + metadata.BinaryFormatMajorVersion, + ) + } + + if metadata.BinaryFormatMinorVersion != 0 { + return testError( + "binary_format_minor_version", + 0, + metadata.BinaryFormatMinorVersion, + ) + } + + if metadata.DatabaseType == "" { + return testError( + "database_type", + "non-empty string", + metadata.DatabaseType, + ) + } + + if len(metadata.Description) == 0 { + return testError( + "description", + "non-empty slice", + metadata.Description, + ) + } + + if metadata.IPVersion != 4 && metadata.IPVersion != 6 { + return testError( + "ip_version", + "4 or 6", + metadata.IPVersion, + ) + } + + if metadata.RecordSize != 24 && + metadata.RecordSize != 28 && + metadata.RecordSize != 32 { + return testError( + "record_size", + "24, 28, or 32", + metadata.RecordSize, + ) + } + + if metadata.NodeCount == 0 { + return testError( + "node_count", + "positive integer", + metadata.NodeCount, + ) + } + return nil +} + +func (v *verifier) verifyDatabase() error { + offsets, err := v.verifySearchTree() + if err != nil { + return err + } + + if err := v.verifyDataSectionSeparator(); err != nil { + return err + } + + return v.verifyDataSection(offsets) +} + +func (v *verifier) verifySearchTree() (map[uint]bool, error) { + offsets := make(map[uint]bool) + + it := v.reader.Networks() + for it.Next() { + offset, err := v.reader.resolveDataPointer(it.lastNode.pointer) + if err != nil { + return nil, err + } + offsets[uint(offset)] = true + } + if err := it.Err(); err != nil { + return nil, err + } + return offsets, nil +} + +func (v *verifier) verifyDataSectionSeparator() error { + separatorStart := v.reader.Metadata.NodeCount * v.reader.Metadata.RecordSize / 4 + + separator := v.reader.buffer[separatorStart : separatorStart+dataSectionSeparatorSize] + + for _, b := range separator { + if b != 0 { + return newInvalidDatabaseError("unexpected byte in data separator: %v", separator) + } + } + return nil +} + +func (v *verifier) verifyDataSection(offsets map[uint]bool) error { + pointerCount := len(offsets) + + decoder := v.reader.decoder + + var offset uint + bufferLen := uint(len(decoder.buffer)) + for offset < bufferLen { + var data interface{} + rv := reflect.ValueOf(&data) + newOffset, err := decoder.decode(offset, rv, 0) + if err != nil { + return newInvalidDatabaseError("received decoding error (%v) at offset of %v", err, offset) + } + if newOffset <= offset { + return newInvalidDatabaseError("data section offset unexpectedly went from %v to %v", offset, newOffset) + } + + pointer := offset + + if _, ok := offsets[pointer]; ok { + delete(offsets, pointer) + } else { + return newInvalidDatabaseError("found data (%v) at %v that the search tree does not point to", data, pointer) + } + + offset = newOffset + } + + if offset != bufferLen { + return newInvalidDatabaseError( + "unexpected data at the end of the data section (last offset: %v, end: %v)", + offset, + bufferLen, + ) + } + + if len(offsets) != 0 { + return newInvalidDatabaseError( + "found %v pointers (of %v) in the search tree that we did not see in the data section", + len(offsets), + pointerCount, + ) + } + return nil +} + +func testError( + field string, + expected interface{}, + actual interface{}, +) error { + return newInvalidDatabaseError( + "%v - Expected: %v Actual: %v", + field, + expected, + actual, + ) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index fe6a6dc5ec..2239b0a8bd 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -38,6 +38,12 @@ "revision": "5d049714c4a64225c3c79a7cf7d02f7fb5b96338", "revisionTime": "2018-01-16T20:38:02Z" }, + { + "checksumSHA1": "hp2pna9yEn9hemIjc7asalxL2Qs=", + "path": "github.com/apilayer/freegeoip", + "revision": "3f942d1392f6439bda0f67b3c650ce468ebdba8e", + "revisionTime": "2018-07-02T11:14:01Z" + }, { "checksumSHA1": "USkefO0g1U9mr+8hagv3fpSkrxg=", "path": "github.com/aristanetworks/goarista/monotime", @@ -339,6 +345,12 @@ "revision": "bd9c3193394760d98b2fa6ebb2291f0cd1d06a7d", "revisionTime": "2018-06-06T20:41:48Z" }, + { + "checksumSHA1": "a1WxG0wMDGFnjojQghwu1i1SDhk=", + "path": "github.com/oschwald/maxminddb-golang", + "revision": "c5bec84d1963260297932a1b7a1753c8420717a7", + "revisionTime": "2018-02-25T17:45:17Z" + }, { "checksumSHA1": "Se195FlZ160eaEk/uVx4KdTPSxU=", "path": "github.com/pborman/uuid", From b784aa949c7de14d3dbd41e77fa451fa8e49996d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurk=C3=B3=20Mih=C3=A1ly?= Date: Wed, 8 Aug 2018 16:43:20 +0300 Subject: [PATCH 7/7] dashboard: go generate --- dashboard/assets.go | 8601 +++++++++++++++++++++++++++++++------------ 1 file changed, 6238 insertions(+), 2363 deletions(-) diff --git a/dashboard/assets.go b/dashboard/assets.go index f3e7cf9815..649cd03a6b 100644 --- a/dashboard/assets.go +++ b/dashboard/assets.go @@ -119,11 +119,11 @@ var _bundleJs = []byte((((((((((`!function(modules) { return __webpack_require__.d(getter, "a", getter), getter; }, __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); - }, __webpack_require__.p = "", __webpack_require__(__webpack_require__.s = 375); + }, __webpack_require__.p = "", __webpack_require__(__webpack_require__.s = 401); }([ function(module, exports, __webpack_require__) { "use strict"; (function(process) { - "production" === process.env.NODE_ENV ? module.exports = __webpack_require__(376) : module.exports = __webpack_require__(377); + "production" === process.env.NODE_ENV ? module.exports = __webpack_require__(402) : module.exports = __webpack_require__(403); }).call(exports, __webpack_require__(2)); }, function(module, exports, __webpack_require__) { (function(process) { @@ -131,8 +131,8 @@ var _bundleJs = []byte((((((((((`!function(modules) { var REACT_ELEMENT_TYPE = "function" == typeof Symbol && Symbol.for && Symbol.for("react.element") || 60103, isValidElement = function(object) { return "object" == typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE; }; - module.exports = __webpack_require__(418)(isValidElement, !0); - } else module.exports = __webpack_require__(419)(); + module.exports = __webpack_require__(444)(isValidElement, !0); + } else module.exports = __webpack_require__(445)(); }).call(exports, __webpack_require__(2)); }, function(module, exports) { function defaultSetTimout() { @@ -243,6 +243,180 @@ var _bundleJs = []byte((((((((((`!function(modules) { return classNames; }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); }(); +}, function(module, exports, __webpack_require__) { + "use strict"; + exports.__esModule = !0, exports.default = function(obj, keys) { + var target = {}; + for (var i in obj) keys.indexOf(i) >= 0 || Object.prototype.hasOwnProperty.call(obj, i) && (target[i] = obj[i]); + return target; + }; +}, function(module, exports, __webpack_require__) { + "use strict"; + exports.__esModule = !0; + var _assign = __webpack_require__(238), _assign2 = function(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + }(_assign); + exports.default = _assign2.default || function(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); + } + return target; + }; +}, function(module, exports, __webpack_require__) { + "use strict"; + (function(process) { + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + function getDefaultTheme() { + return defaultTheme || (defaultTheme = (0, _createMuiTheme2.default)()); + } + Object.defineProperty(exports, "__esModule", { + value: !0 + }), exports.sheetsManager = void 0; + var _keys = __webpack_require__(43), _keys2 = _interopRequireDefault(_keys), _extends2 = __webpack_require__(5), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(11), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(10), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(12), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(13), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(14), _inherits3 = _interopRequireDefault(_inherits2), _objectWithoutProperties2 = __webpack_require__(4), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _map = __webpack_require__(464), _map2 = _interopRequireDefault(_map), _minSafeInteger = __webpack_require__(480), _minSafeInteger2 = _interopRequireDefault(_minSafeInteger), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _propTypes = __webpack_require__(1), _propTypes2 = _interopRequireDefault(_propTypes), _warning = __webpack_require__(17), _warning2 = _interopRequireDefault(_warning), _hoistNonReactStatics = __webpack_require__(170), _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics), _getDisplayName = __webpack_require__(261), _getDisplayName2 = _interopRequireDefault(_getDisplayName), _wrapDisplayName = __webpack_require__(83), _wrapDisplayName2 = _interopRequireDefault(_wrapDisplayName), _contextTypes = __webpack_require__(483), _contextTypes2 = _interopRequireDefault(_contextTypes), _jss = __webpack_require__(263), _ns = __webpack_require__(262), ns = function(obj) { + if (obj && obj.__esModule) return obj; + var newObj = {}; + if (null != obj) for (var key in obj) Object.prototype.hasOwnProperty.call(obj, key) && (newObj[key] = obj[key]); + return newObj.default = obj, newObj; + }(_ns), _jssPreset = __webpack_require__(505), _jssPreset2 = _interopRequireDefault(_jssPreset), _createMuiTheme = __webpack_require__(169), _createMuiTheme2 = _interopRequireDefault(_createMuiTheme), _themeListener = __webpack_require__(168), _themeListener2 = _interopRequireDefault(_themeListener), _createGenerateClassName = __webpack_require__(518), _createGenerateClassName2 = _interopRequireDefault(_createGenerateClassName), _getStylesCreator = __webpack_require__(519), _getStylesCreator2 = _interopRequireDefault(_getStylesCreator), jss = (0, + _jss.create)((0, _jssPreset2.default)()), generateClassName = (0, _createGenerateClassName2.default)(), indexCounter = _minSafeInteger2.default, sheetsManager = exports.sheetsManager = new _map2.default(), noopTheme = {}, defaultTheme = void 0, withStyles = function(stylesOrCreator) { + var options = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; + return function(Component) { + var _options$withTheme = options.withTheme, withTheme = void 0 !== _options$withTheme && _options$withTheme, _options$flip = options.flip, flip = void 0 === _options$flip ? null : _options$flip, name = options.name, styleSheetOptions = (0, + _objectWithoutProperties3.default)(options, [ "withTheme", "flip", "name" ]), stylesCreator = (0, + _getStylesCreator2.default)(stylesOrCreator), listenToTheme = stylesCreator.themingEnabled || withTheme || "string" == typeof name; + indexCounter += 1, stylesCreator.options.index = indexCounter, "production" !== process.env.NODE_ENV && (0, + _warning2.default)(indexCounter < 0, [ "Material-UI: you might have a memory leak.", "The indexCounter is not supposed to grow that much." ].join(" ")); + var WithStyles = function(_React$Component) { + function WithStyles(props, context) { + (0, _classCallCheck3.default)(this, WithStyles); + var _this = (0, _possibleConstructorReturn3.default)(this, (WithStyles.__proto__ || (0, + _getPrototypeOf2.default)(WithStyles)).call(this, props, context)); + _this.state = {}, _this.disableStylesGeneration = !1, _this.jss = null, _this.sheetOptions = null, + _this.sheetsManager = sheetsManager, _this.stylesCreatorSaved = null, _this.theme = null, + _this.unsubscribeId = null, _this.jss = _this.context[ns.jss] || jss; + var muiThemeProviderOptions = _this.context.muiThemeProviderOptions; + return muiThemeProviderOptions && (muiThemeProviderOptions.sheetsManager && (_this.sheetsManager = muiThemeProviderOptions.sheetsManager), + _this.disableStylesGeneration = muiThemeProviderOptions.disableStylesGeneration), + _this.stylesCreatorSaved = stylesCreator, _this.sheetOptions = (0, _extends3.default)({ + generateClassName: generateClassName + }, _this.context[ns.sheetOptions]), _this.theme = listenToTheme ? _themeListener2.default.initial(context) || getDefaultTheme() : noopTheme, + _this; + } + return (0, _inherits3.default)(WithStyles, _React$Component), (0, _createClass3.default)(WithStyles, [ { + key: "componentWillMount", + value: function() { + this.attach(this.theme); + } + }, { + key: "componentDidMount", + value: function() { + var _this2 = this; + listenToTheme && (this.unsubscribeId = _themeListener2.default.subscribe(this.context, function(theme) { + var oldTheme = _this2.theme; + _this2.theme = theme, _this2.attach(_this2.theme), _this2.setState({}, function() { + _this2.detach(oldTheme); + }); + })); + } + }, { + key: "componentWillReceiveProps", + value: function() { + this.stylesCreatorSaved !== stylesCreator && "production" !== process.env.NODE_ENV && (this.detach(this.theme), + this.stylesCreatorSaved = stylesCreator, this.attach(this.theme)); + } + }, { + key: "componentWillUnmount", + value: function() { + this.detach(this.theme), null !== this.unsubscribeId && _themeListener2.default.unsubscribe(this.context, this.unsubscribeId); + } + }, { + key: "attach", + value: function(theme) { + if (!this.disableStylesGeneration) { + var stylesCreatorSaved = this.stylesCreatorSaved, sheetManager = this.sheetsManager.get(stylesCreatorSaved); + sheetManager || (sheetManager = new _map2.default(), this.sheetsManager.set(stylesCreatorSaved, sheetManager)); + var sheetManagerTheme = sheetManager.get(theme); + if (sheetManagerTheme || (sheetManagerTheme = { + refs: 0, + sheet: null + }, sheetManager.set(theme, sheetManagerTheme)), 0 === sheetManagerTheme.refs) { + var styles = stylesCreatorSaved.create(theme, name), meta = name; + "production" === process.env.NODE_ENV || meta || (meta = (0, _getDisplayName2.default)(Component)); + var sheet = this.jss.createStyleSheet(styles, (0, _extends3.default)({ + meta: meta, + classNamePrefix: meta, + flip: "boolean" == typeof flip ? flip : "rtl" === theme.direction, + link: !1 + }, this.sheetOptions, stylesCreatorSaved.options, { + name: name + }, styleSheetOptions)); + sheetManagerTheme.sheet = sheet, sheet.attach(); + var sheetsRegistry = this.context[ns.sheetsRegistry]; + sheetsRegistry && sheetsRegistry.add(sheet); + } + sheetManagerTheme.refs += 1; + } + } + }, { + key: "detach", + value: function(theme) { + if (!this.disableStylesGeneration) { + var stylesCreatorSaved = this.stylesCreatorSaved, sheetManager = this.sheetsManager.get(stylesCreatorSaved), sheetManagerTheme = sheetManager.get(theme); + if (sheetManagerTheme.refs -= 1, 0 === sheetManagerTheme.refs) { + sheetManager.delete(theme), this.jss.removeStyleSheet(sheetManagerTheme.sheet); + var sheetsRegistry = this.context[ns.sheetsRegistry]; + sheetsRegistry && sheetsRegistry.remove(sheetManagerTheme.sheet); + } + } + } + }, { + key: "render", + value: function() { + var _this3 = this, _props = this.props, classesProp = _props.classes, innerRef = _props.innerRef, other = (0, + _objectWithoutProperties3.default)(_props, [ "classes", "innerRef" ]), classes = void 0, renderedClasses = {}; + if (!this.disableStylesGeneration) { + var sheetManager = this.sheetsManager.get(this.stylesCreatorSaved), sheetsManagerTheme = sheetManager.get(this.theme); + renderedClasses = sheetsManagerTheme.sheet.classes; + } + classes = classesProp ? (0, _extends3.default)({}, renderedClasses, (0, _keys2.default)(classesProp).reduce(function(accumulator, key) { + return "production" !== process.env.NODE_ENV && (0, _warning2.default)(renderedClasses[key] || _this3.disableStylesGeneration, [ "Material-UI: the key ` + ("`" + `" + key + "`)) + ("`" + (` provided to the classes property is not implemented in " + (0, + _getDisplayName2.default)(Component) + ".", "You can only override one of the following: " + (0, + _keys2.default)(renderedClasses).join(",") ].join("\n")), "production" !== process.env.NODE_ENV && (0, + _warning2.default)(!classesProp[key] || "string" == typeof classesProp[key], [ "Material-UI: the key ` + "`"))) + ((`" + key + "` + ("`" + ` provided to the classes property is not valid for " + (0, + _getDisplayName2.default)(Component) + ".", "You need to provide a non empty string instead of: " + classesProp[key] + "." ].join("\n")), + classesProp[key] && (accumulator[key] = renderedClasses[key] + " " + classesProp[key]), + accumulator; + }, {})) : renderedClasses; + var more = {}; + return withTheme && (more.theme = this.theme), _react2.default.createElement(Component, (0, + _extends3.default)({ + classes: classes + }, more, other, { + ref: innerRef + })); + } + } ]), WithStyles; + }(_react2.default.Component); + return WithStyles.propTypes = "production" !== process.env.NODE_ENV ? { + classes: _propTypes2.default.object, + innerRef: _propTypes2.default.func + } : {}, WithStyles.contextTypes = (0, _extends3.default)({ + muiThemeProviderOptions: _propTypes2.default.object + }, _contextTypes2.default, listenToTheme ? _themeListener2.default.contextTypes : {}), + "production" !== process.env.NODE_ENV && (WithStyles.displayName = (0, _wrapDisplayName2.default)(Component, "WithStyles")), + (0, _hoistNonReactStatics2.default)(WithStyles, Component), "production" !== process.env.NODE_ENV && (WithStyles.Naked = Component, + WithStyles.options = options), WithStyles; + }; + }; + exports.default = withStyles; + }).call(exports, __webpack_require__(2)); }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; function _toConsumableArray(arr) { @@ -292,7 +466,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "o", function() { return parseChildIndex; }); - var __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1_lodash_isString__ = __webpack_require__(173), __WEBPACK_IMPORTED_MODULE_1_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isString__), __WEBPACK_IMPORTED_MODULE_2_lodash_isObject__ = __webpack_require__(32), __WEBPACK_IMPORTED_MODULE_2_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_3_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_3_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray__ = __webpack_require__(13), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7__DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_8__PureRender__ = __webpack_require__(5), PRESENTATION_ATTRIBUTES = { + var __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1_lodash_isString__ = __webpack_require__(189), __WEBPACK_IMPORTED_MODULE_1_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isString__), __WEBPACK_IMPORTED_MODULE_2_lodash_isObject__ = __webpack_require__(35), __WEBPACK_IMPORTED_MODULE_2_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_3_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_3_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray__ = __webpack_require__(18), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7__DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_8__PureRender__ = __webpack_require__(9), PRESENTATION_ATTRIBUTES = { alignmentBaseline: __WEBPACK_IMPORTED_MODULE_6_prop_types___default.a.string, angle: __WEBPACK_IMPORTED_MODULE_6_prop_types___default.a.number, baselineShift: __WEBPACK_IMPORTED_MODULE_6_prop_types___default.a.string, @@ -488,6 +662,22 @@ var _bundleJs = []byte((((((((((`!function(modules) { entry === child && (result = index); }), result; }; +}, function(module, exports, __webpack_require__) { + "use strict"; + exports.__esModule = !0; + var _defineProperty = __webpack_require__(162), _defineProperty2 = function(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + }(_defineProperty); + exports.default = function(obj, key, value) { + return key in obj ? (0, _defineProperty2.default)(obj, key, { + value: value, + enumerable: !0, + configurable: !0, + writable: !0 + }) : obj[key] = value, obj; + }; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; function shallowEqual(a, b) { @@ -504,25 +694,67 @@ var _bundleJs = []byte((((((((((`!function(modules) { __webpack_exports__.b = shallowEqual, __webpack_exports__.a = pureRenderDecorator; }, function(module, exports, __webpack_require__) { "use strict"; - exports.__esModule = !0; - var _assign = __webpack_require__(222), _assign2 = function(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - }(_assign); - exports.default = _assign2.default || function(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); - } - return target; + exports.__esModule = !0, exports.default = function(instance, Constructor) { + if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); + }; +}, function(module, exports, __webpack_require__) { + module.exports = { + default: __webpack_require__(420), + __esModule: !0 }; }, function(module, exports, __webpack_require__) { "use strict"; - exports.__esModule = !0, exports.default = function(obj, keys) { - var target = {}; - for (var i in obj) keys.indexOf(i) >= 0 || Object.prototype.hasOwnProperty.call(obj, i) && (target[i] = obj[i]); - return target; + exports.__esModule = !0; + var _defineProperty = __webpack_require__(162), _defineProperty2 = function(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + }(_defineProperty); + exports.default = function() { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0, + "value" in descriptor && (descriptor.writable = !0), (0, _defineProperty2.default)(target, descriptor.key, descriptor); + } + } + return function(Constructor, protoProps, staticProps) { + return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), + Constructor; + }; + }(); +}, function(module, exports, __webpack_require__) { + "use strict"; + exports.__esModule = !0; + var _typeof2 = __webpack_require__(111), _typeof3 = function(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + }(_typeof2); + exports.default = function(self, call) { + if (!self) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !call || "object" !== (void 0 === call ? "undefined" : (0, _typeof3.default)(call)) && "function" != typeof call ? self : call; + }; +}, function(module, exports, __webpack_require__) { + "use strict"; + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + exports.__esModule = !0; + var _setPrototypeOf = __webpack_require__(437), _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf), _create = __webpack_require__(441), _create2 = _interopRequireDefault(_create), _typeof2 = __webpack_require__(111), _typeof3 = _interopRequireDefault(_typeof2); + exports.default = function(subClass, superClass) { + if ("function" != typeof superClass && null !== superClass) throw new TypeError("Super expression must either be null or a function, not " + (void 0 === superClass ? "undefined" : (0, + _typeof3.default)(superClass))); + subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: !1, + writable: !0, + configurable: !0 + } + }), superClass && (_setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass); }; }, function(module, exports, __webpack_require__) { function isFunction(value) { @@ -530,7 +762,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { var tag = baseGetTag(value); return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; } - var baseGetTag = __webpack_require__(41), isObject = __webpack_require__(32), asyncTag = "[object AsyncFunction]", funcTag = "[object Function]", genTag = "[object GeneratorFunction]", proxyTag = "[object Proxy]"; + var baseGetTag = __webpack_require__(45), isObject = __webpack_require__(35), asyncTag = "[object AsyncFunction]", funcTag = "[object Function]", genTag = "[object GeneratorFunction]", proxyTag = "[object Proxy]"; module.exports = isFunction; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -557,7 +789,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "c", function() { return getLinearRegression; }); - var __WEBPACK_IMPORTED_MODULE_0_lodash_get__ = __webpack_require__(174), __WEBPACK_IMPORTED_MODULE_0_lodash_get___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_get__), __WEBPACK_IMPORTED_MODULE_1_lodash_isArray__ = __webpack_require__(13), __WEBPACK_IMPORTED_MODULE_1_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__ = __webpack_require__(120), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__), __WEBPACK_IMPORTED_MODULE_3_lodash_isNumber__ = __webpack_require__(272), __WEBPACK_IMPORTED_MODULE_3_lodash_isNumber___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isNumber__), __WEBPACK_IMPORTED_MODULE_4_lodash_isString__ = __webpack_require__(173), __WEBPACK_IMPORTED_MODULE_4_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isString__), mathSign = function(value) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_get__ = __webpack_require__(190), __WEBPACK_IMPORTED_MODULE_0_lodash_get___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_get__), __WEBPACK_IMPORTED_MODULE_1_lodash_isArray__ = __webpack_require__(18), __WEBPACK_IMPORTED_MODULE_1_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__ = __webpack_require__(128), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__), __WEBPACK_IMPORTED_MODULE_3_lodash_isNumber__ = __webpack_require__(298), __WEBPACK_IMPORTED_MODULE_3_lodash_isNumber___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isNumber__), __WEBPACK_IMPORTED_MODULE_4_lodash_isString__ = __webpack_require__(189), __WEBPACK_IMPORTED_MODULE_4_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isString__), mathSign = function(value) { return 0 === value ? 0 : value > 0 ? 1 : -1; }, isPercent = function(value) { return __WEBPACK_IMPORTED_MODULE_4_lodash_isString___default()(value) && value.indexOf("%") === value.length - 1; @@ -612,158 +844,6 @@ var _bundleJs = []byte((((((((((`!function(modules) { b: (ysum - a * xsum) / len }; }; -}, function(module, exports, __webpack_require__) { - "use strict"; - (function(process) { - function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - } - function getDefaultTheme() { - return defaultTheme || (defaultTheme = (0, _createMuiTheme2.default)()); - } - Object.defineProperty(exports, "__esModule", { - value: !0 - }), exports.sheetsManager = void 0; - var _keys = __webpack_require__(55), _keys2 = _interopRequireDefault(_keys), _extends2 = __webpack_require__(6), _extends3 = _interopRequireDefault(_extends2), _getPrototypeOf = __webpack_require__(26), _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf), _classCallCheck2 = __webpack_require__(27), _classCallCheck3 = _interopRequireDefault(_classCallCheck2), _createClass2 = __webpack_require__(28), _createClass3 = _interopRequireDefault(_createClass2), _possibleConstructorReturn2 = __webpack_require__(29), _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2), _inherits2 = __webpack_require__(30), _inherits3 = _interopRequireDefault(_inherits2), _objectWithoutProperties2 = __webpack_require__(7), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2), _map = __webpack_require__(440), _map2 = _interopRequireDefault(_map), _minSafeInteger = __webpack_require__(456), _minSafeInteger2 = _interopRequireDefault(_minSafeInteger), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _propTypes = __webpack_require__(1), _propTypes2 = _interopRequireDefault(_propTypes), _warning = __webpack_require__(11), _warning2 = _interopRequireDefault(_warning), _hoistNonReactStatics = __webpack_require__(162), _hoistNonReactStatics2 = _interopRequireDefault(_hoistNonReactStatics), _getDisplayName = __webpack_require__(244), _getDisplayName2 = _interopRequireDefault(_getDisplayName), _wrapDisplayName = __webpack_require__(79), _wrapDisplayName2 = _interopRequireDefault(_wrapDisplayName), _contextTypes = __webpack_require__(459), _contextTypes2 = _interopRequireDefault(_contextTypes), _jss = __webpack_require__(246), _ns = __webpack_require__(245), ns = function(obj) { - if (obj && obj.__esModule) return obj; - var newObj = {}; - if (null != obj) for (var key in obj) Object.prototype.hasOwnProperty.call(obj, key) && (newObj[key] = obj[key]); - return newObj.default = obj, newObj; - }(_ns), _jssPreset = __webpack_require__(481), _jssPreset2 = _interopRequireDefault(_jssPreset), _createMuiTheme = __webpack_require__(161), _createMuiTheme2 = _interopRequireDefault(_createMuiTheme), _themeListener = __webpack_require__(160), _themeListener2 = _interopRequireDefault(_themeListener), _createGenerateClassName = __webpack_require__(494), _createGenerateClassName2 = _interopRequireDefault(_createGenerateClassName), _getStylesCreator = __webpack_require__(495), _getStylesCreator2 = _interopRequireDefault(_getStylesCreator), jss = (0, - _jss.create)((0, _jssPreset2.default)()), generateClassName = (0, _createGenerateClassName2.default)(), indexCounter = _minSafeInteger2.default, sheetsManager = exports.sheetsManager = new _map2.default(), noopTheme = {}, defaultTheme = void 0, withStyles = function(stylesOrCreator) { - var options = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - return function(Component) { - var _options$withTheme = options.withTheme, withTheme = void 0 !== _options$withTheme && _options$withTheme, _options$flip = options.flip, flip = void 0 === _options$flip ? null : _options$flip, name = options.name, styleSheetOptions = (0, - _objectWithoutProperties3.default)(options, [ "withTheme", "flip", "name" ]), stylesCreator = (0, - _getStylesCreator2.default)(stylesOrCreator), listenToTheme = stylesCreator.themingEnabled || withTheme || "string" == typeof name; - indexCounter += 1, stylesCreator.options.index = indexCounter, "production" !== process.env.NODE_ENV && (0, - _warning2.default)(indexCounter < 0, [ "Material-UI: you might have a memory leak.", "The indexCounter is not supposed to grow that much." ].join(" ")); - var WithStyles = function(_React$Component) { - function WithStyles(props, context) { - (0, _classCallCheck3.default)(this, WithStyles); - var _this = (0, _possibleConstructorReturn3.default)(this, (WithStyles.__proto__ || (0, - _getPrototypeOf2.default)(WithStyles)).call(this, props, context)); - _this.state = {}, _this.disableStylesGeneration = !1, _this.jss = null, _this.sheetOptions = null, - _this.sheetsManager = sheetsManager, _this.stylesCreatorSaved = null, _this.theme = null, - _this.unsubscribeId = null, _this.jss = _this.context[ns.jss] || jss; - var muiThemeProviderOptions = _this.context.muiThemeProviderOptions; - return muiThemeProviderOptions && (muiThemeProviderOptions.sheetsManager && (_this.sheetsManager = muiThemeProviderOptions.sheetsManager), - _this.disableStylesGeneration = muiThemeProviderOptions.disableStylesGeneration), - _this.stylesCreatorSaved = stylesCreator, _this.sheetOptions = (0, _extends3.default)({ - generateClassName: generateClassName - }, _this.context[ns.sheetOptions]), _this.theme = listenToTheme ? _themeListener2.default.initial(context) || getDefaultTheme() : noopTheme, - _this; - } - return (0, _inherits3.default)(WithStyles, _React$Component), (0, _createClass3.default)(WithStyles, [ { - key: "componentWillMount", - value: function() { - this.attach(this.theme); - } - }, { - key: "componentDidMount", - value: function() { - var _this2 = this; - listenToTheme && (this.unsubscribeId = _themeListener2.default.subscribe(this.context, function(theme) { - var oldTheme = _this2.theme; - _this2.theme = theme, _this2.attach(_this2.theme), _this2.setState({}, function() { - _this2.detach(oldTheme); - }); - })); - } - }, { - key: "componentWillReceiveProps", - value: function() { - this.stylesCreatorSaved !== stylesCreator && "production" !== process.env.NODE_ENV && (this.detach(this.theme), - this.stylesCreatorSaved = stylesCreator, this.attach(this.theme)); - } - }, { - key: "componentWillUnmount", - value: function() { - this.detach(this.theme), null !== this.unsubscribeId && _themeListener2.default.unsubscribe(this.context, this.unsubscribeId); - } - }, { - key: "attach", - value: function(theme) { - if (!this.disableStylesGeneration) { - var stylesCreatorSaved = this.stylesCreatorSaved, sheetManager = this.sheetsManager.get(stylesCreatorSaved); - sheetManager || (sheetManager = new _map2.default(), this.sheetsManager.set(stylesCreatorSaved, sheetManager)); - var sheetManagerTheme = sheetManager.get(theme); - if (sheetManagerTheme || (sheetManagerTheme = { - refs: 0, - sheet: null - }, sheetManager.set(theme, sheetManagerTheme)), 0 === sheetManagerTheme.refs) { - var styles = stylesCreatorSaved.create(theme, name), meta = name; - "production" === process.env.NODE_ENV || meta || (meta = (0, _getDisplayName2.default)(Component)); - var sheet = this.jss.createStyleSheet(styles, (0, _extends3.default)({ - meta: meta, - classNamePrefix: meta, - flip: "boolean" == typeof flip ? flip : "rtl" === theme.direction, - link: !1 - }, this.sheetOptions, stylesCreatorSaved.options, { - name: name - }, styleSheetOptions)); - sheetManagerTheme.sheet = sheet, sheet.attach(); - var sheetsRegistry = this.context[ns.sheetsRegistry]; - sheetsRegistry && sheetsRegistry.add(sheet); - } - sheetManagerTheme.refs += 1; - } - } - }, { - key: "detach", - value: function(theme) { - if (!this.disableStylesGeneration) { - var stylesCreatorSaved = this.stylesCreatorSaved, sheetManager = this.sheetsManager.get(stylesCreatorSaved), sheetManagerTheme = sheetManager.get(theme); - if (sheetManagerTheme.refs -= 1, 0 === sheetManagerTheme.refs) { - sheetManager.delete(theme), this.jss.removeStyleSheet(sheetManagerTheme.sheet); - var sheetsRegistry = this.context[ns.sheetsRegistry]; - sheetsRegistry && sheetsRegistry.remove(sheetManagerTheme.sheet); - } - } - } - }, { - key: "render", - value: function() { - var _this3 = this, _props = this.props, classesProp = _props.classes, innerRef = _props.innerRef, other = (0, - _objectWithoutProperties3.default)(_props, [ "classes", "innerRef" ]), classes = void 0, renderedClasses = {}; - if (!this.disableStylesGeneration) { - var sheetManager = this.sheetsManager.get(this.stylesCreatorSaved), sheetsManagerTheme = sheetManager.get(this.theme); - renderedClasses = sheetsManagerTheme.sheet.classes; - } - classes = classesProp ? (0, _extends3.default)({}, renderedClasses, (0, _keys2.default)(classesProp).reduce(function(accumulator, key) { - return "production" !== process.env.NODE_ENV && (0, _warning2.default)(renderedClasses[key] || _this3.disableStylesGeneration, [ "Material-UI: the key ` + ("`" + `" + key + "`)) + ("`" + (` provided to the classes property is not implemented in " + (0, - _getDisplayName2.default)(Component) + ".", "You can only override one of the following: " + (0, - _keys2.default)(renderedClasses).join(",") ].join("\n")), "production" !== process.env.NODE_ENV && (0, - _warning2.default)(!classesProp[key] || "string" == typeof classesProp[key], [ "Material-UI: the key ` + "`"))) + ((`" + key + "` + ("`" + ` provided to the classes property is not valid for " + (0, - _getDisplayName2.default)(Component) + ".", "You need to provide a non empty string instead of: " + classesProp[key] + "." ].join("\n")), - classesProp[key] && (accumulator[key] = renderedClasses[key] + " " + classesProp[key]), - accumulator; - }, {})) : renderedClasses; - var more = {}; - return withTheme && (more.theme = this.theme), _react2.default.createElement(Component, (0, - _extends3.default)({ - classes: classes - }, more, other, { - ref: innerRef - })); - } - } ]), WithStyles; - }(_react2.default.Component); - return WithStyles.propTypes = "production" !== process.env.NODE_ENV ? { - classes: _propTypes2.default.object, - innerRef: _propTypes2.default.func - } : {}, WithStyles.contextTypes = (0, _extends3.default)({ - muiThemeProviderOptions: _propTypes2.default.object - }, _contextTypes2.default, listenToTheme ? _themeListener2.default.contextTypes : {}), - "production" !== process.env.NODE_ENV && (WithStyles.displayName = (0, _wrapDisplayName2.default)(Component, "WithStyles")), - (0, _hoistNonReactStatics2.default)(WithStyles, Component), "production" !== process.env.NODE_ENV && (WithStyles.Naked = Component, - WithStyles.options = options), WithStyles; - }; - }; - exports.default = withStyles; - }).call(exports, __webpack_require__(2)); }, function(module, exports, __webpack_require__) { "use strict"; (function(process) { @@ -772,7 +852,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { var len = arguments.length; args = new Array(len > 2 ? len - 2 : 0); for (var key = 2; key < len; key++) args[key - 2] = arguments[key]; - if (void 0 === format) throw new Error("`)) + ("`" + (`warning(condition, format, ...args)` + "`")))) + (((` requires a warning message argument"); + if (void 0 === format) throw new Error("`)) + (("`" + `warning(condition, format, ...args)`) + ("`" + ` requires a warning message argument"); if (format.length < 10 || /^[s\W]*$/.test(format)) throw new Error("The warning format should be able to uniquely identify this warning. Please, use a more descriptive format than: " + format); if (!condition) { var argIndex = 0, message = "Warning: " + format.replace(/%s/g, function() { @@ -785,22 +865,6 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), module.exports = warning; }).call(exports, __webpack_require__(2)); -}, function(module, exports, __webpack_require__) { - "use strict"; - exports.__esModule = !0; - var _defineProperty = __webpack_require__(154), _defineProperty2 = function(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - }(_defineProperty); - exports.default = function(obj, key, value) { - return key in obj ? (0, _defineProperty2.default)(obj, key, { - value: value, - enumerable: !0, - configurable: !0, - writable: !0 - }) : obj[key] = value, obj; - }; }, function(module, exports) { var isArray = Array.isArray; module.exports = isArray; @@ -829,7 +893,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }; Layer.propTypes = propTypes, __webpack_exports__.a = Layer; }, function(module, exports, __webpack_require__) { - var global = __webpack_require__(167), core = __webpack_require__(168), hide = __webpack_require__(266), redefine = __webpack_require__(581), ctx = __webpack_require__(584), $export = function(type, name, source) { + var global = __webpack_require__(183), core = __webpack_require__(184), hide = __webpack_require__(292), redefine = __webpack_require__(642), ctx = __webpack_require__(645), $export = function(type, name, source) { var key, own, out, exp, IS_FORCED = type & $export.F, IS_GLOBAL = type & $export.G, IS_STATIC = type & $export.S, IS_PROTO = type & $export.P, IS_BIND = type & $export.B, target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {}).prototype, exports = IS_GLOBAL ? core : core[name] || (core[name] = {}), expProto = exports.prototype || (exports.prototype = {}); IS_GLOBAL && (source = name); for (key in source) own = !IS_FORCED && target && void 0 !== target[key], out = (own ? target : source)[key], @@ -917,8 +981,8 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "y", function() { return parseDomainOfCategoryAxis; }); - var __WEBPACK_IMPORTED_MODULE_0_lodash_isEqual__ = __webpack_require__(45), __WEBPACK_IMPORTED_MODULE_0_lodash_isEqual___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isEqual__), __WEBPACK_IMPORTED_MODULE_1_lodash_sortBy__ = __webpack_require__(321), __WEBPACK_IMPORTED_MODULE_1_lodash_sortBy___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_sortBy__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__ = __webpack_require__(120), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__), __WEBPACK_IMPORTED_MODULE_3_lodash_isString__ = __webpack_require__(173), __WEBPACK_IMPORTED_MODULE_3_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isString__), __WEBPACK_IMPORTED_MODULE_4_lodash_max__ = __webpack_require__(841), __WEBPACK_IMPORTED_MODULE_4_lodash_max___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_max__), __WEBPACK_IMPORTED_MODULE_5_lodash_min__ = __webpack_require__(328), __WEBPACK_IMPORTED_MODULE_5_lodash_min___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_lodash_min__), __WEBPACK_IMPORTED_MODULE_6_lodash_isArray__ = __webpack_require__(13), __WEBPACK_IMPORTED_MODULE_6_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_7_lodash_flatMap__ = __webpack_require__(842), __WEBPACK_IMPORTED_MODULE_7_lodash_flatMap___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_7_lodash_flatMap__), __WEBPACK_IMPORTED_MODULE_8_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_8_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_8_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_9_lodash_get__ = __webpack_require__(174), __WEBPACK_IMPORTED_MODULE_9_lodash_get___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_9_lodash_get__), __WEBPACK_IMPORTED_MODULE_10_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_10_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_10_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_11_recharts_scale__ = __webpack_require__(844), __WEBPACK_IMPORTED_MODULE_12_d3_scale__ = (__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_11_recharts_scale__), - __webpack_require__(331)), __WEBPACK_IMPORTED_MODULE_13_d3_shape__ = __webpack_require__(182), __WEBPACK_IMPORTED_MODULE_14__DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_15__cartesian_ReferenceDot__ = __webpack_require__(364), __WEBPACK_IMPORTED_MODULE_16__cartesian_ReferenceLine__ = __webpack_require__(365), __WEBPACK_IMPORTED_MODULE_17__cartesian_ReferenceArea__ = __webpack_require__(366), __WEBPACK_IMPORTED_MODULE_18__cartesian_ErrorBar__ = __webpack_require__(95), __WEBPACK_IMPORTED_MODULE_19__component_Legend__ = __webpack_require__(180), __WEBPACK_IMPORTED_MODULE_20__ReactUtils__ = __webpack_require__(4), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_isEqual__ = __webpack_require__(49), __WEBPACK_IMPORTED_MODULE_0_lodash_isEqual___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isEqual__), __WEBPACK_IMPORTED_MODULE_1_lodash_sortBy__ = __webpack_require__(347), __WEBPACK_IMPORTED_MODULE_1_lodash_sortBy___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_sortBy__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__ = __webpack_require__(128), __WEBPACK_IMPORTED_MODULE_2_lodash_isNaN___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNaN__), __WEBPACK_IMPORTED_MODULE_3_lodash_isString__ = __webpack_require__(189), __WEBPACK_IMPORTED_MODULE_3_lodash_isString___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_isString__), __WEBPACK_IMPORTED_MODULE_4_lodash_max__ = __webpack_require__(902), __WEBPACK_IMPORTED_MODULE_4_lodash_max___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_max__), __WEBPACK_IMPORTED_MODULE_5_lodash_min__ = __webpack_require__(354), __WEBPACK_IMPORTED_MODULE_5_lodash_min___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_lodash_min__), __WEBPACK_IMPORTED_MODULE_6_lodash_isArray__ = __webpack_require__(18), __WEBPACK_IMPORTED_MODULE_6_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_7_lodash_flatMap__ = __webpack_require__(903), __WEBPACK_IMPORTED_MODULE_7_lodash_flatMap___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_7_lodash_flatMap__), __WEBPACK_IMPORTED_MODULE_8_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_8_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_8_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_9_lodash_get__ = __webpack_require__(190), __WEBPACK_IMPORTED_MODULE_9_lodash_get___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_9_lodash_get__), __WEBPACK_IMPORTED_MODULE_10_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_10_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_10_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_11_recharts_scale__ = __webpack_require__(905), __WEBPACK_IMPORTED_MODULE_12_d3_scale__ = (__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_11_recharts_scale__), + __webpack_require__(357)), __WEBPACK_IMPORTED_MODULE_13_d3_shape__ = __webpack_require__(198), __WEBPACK_IMPORTED_MODULE_14__DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_15__cartesian_ReferenceDot__ = __webpack_require__(390), __WEBPACK_IMPORTED_MODULE_16__cartesian_ReferenceLine__ = __webpack_require__(391), __WEBPACK_IMPORTED_MODULE_17__cartesian_ReferenceArea__ = __webpack_require__(392), __WEBPACK_IMPORTED_MODULE_18__cartesian_ErrorBar__ = __webpack_require__(102), __WEBPACK_IMPORTED_MODULE_19__component_Legend__ = __webpack_require__(196), __WEBPACK_IMPORTED_MODULE_20__ReactUtils__ = __webpack_require__(7), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -1452,7 +1516,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { __webpack_exports__.a = newInterval; var t0 = new Date(), t1 = new Date(); }, function(module, exports, __webpack_require__) { - var global = __webpack_require__(24), core = __webpack_require__(17), ctx = __webpack_require__(51), hide = __webpack_require__(39), has = __webpack_require__(54), $export = function(type, name, source) { + var global = __webpack_require__(30), core = __webpack_require__(22), ctx = __webpack_require__(55), hide = __webpack_require__(42), has = __webpack_require__(58), $export = function(type, name, source) { var key, own, out, IS_FORCED = type & $export.F, IS_GLOBAL = type & $export.G, IS_STATIC = type & $export.S, IS_PROTO = type & $export.P, IS_BIND = type & $export.B, IS_WRAP = type & $export.W, exports = IS_GLOBAL ? core : core[name] || (core[name] = {}), expProto = exports.prototype, target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {}).prototype; IS_GLOBAL && (source = name); for (key in source) (own = !IS_FORCED && target && void 0 !== target[key]) && has(exports, key) || (out = own ? target[key] : source[key], @@ -1485,13 +1549,28 @@ var _bundleJs = []byte((((((((((`!function(modules) { } module.exports = isNil; }, function(module, exports, __webpack_require__) { - var store = __webpack_require__(151)("wks"), uid = __webpack_require__(103), Symbol = __webpack_require__(24).Symbol, USE_SYMBOL = "function" == typeof Symbol; + var store = __webpack_require__(159)("wks"), uid = __webpack_require__(109), Symbol = __webpack_require__(30).Symbol, USE_SYMBOL = "function" == typeof Symbol; (module.exports = function(name) { return store[name] || (store[name] = USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)("Symbol." + name)); }).store = store; }, function(module, exports, __webpack_require__) { - var anObject = __webpack_require__(52), IE8_DOM_DEFINE = __webpack_require__(224), toPrimitive = __webpack_require__(145), dP = Object.defineProperty; - exports.f = __webpack_require__(25) ? Object.defineProperty : function(O, P, Attributes) { + "use strict"; + (function(process) { + function checkDCE() { + if ("undefined" != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" == typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE) { + if ("production" !== process.env.NODE_ENV) throw new Error("^_^"); + try { + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); + } catch (err) { + console.error(err); + } + } + } + "production" === process.env.NODE_ENV ? (checkDCE(), module.exports = __webpack_require__(404)) : module.exports = __webpack_require__(407); + }).call(exports, __webpack_require__(2)); +}, function(module, exports, __webpack_require__) { + var anObject = __webpack_require__(56), IE8_DOM_DEFINE = __webpack_require__(240), toPrimitive = __webpack_require__(153), dP = Object.defineProperty; + exports.f = __webpack_require__(31) ? Object.defineProperty : function(O, P, Attributes) { if (anObject(O), P = toPrimitive(P, !0), anObject(Attributes), IE8_DOM_DEFINE) try { return dP(O, P, Attributes); } catch (e) {} @@ -1519,7 +1598,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "d", function() { return inRangeOfSector; }); - var __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1__DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_2__ChartUtils__ = __webpack_require__(16), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1__DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_2__ChartUtils__ = __webpack_require__(21), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -1620,79 +1699,72 @@ var _bundleJs = []byte((((((((((`!function(modules) { var global = module.exports = "undefined" != typeof window && window.Math == Math ? window : "undefined" != typeof self && self.Math == Math ? self : Function("return this")(); "number" == typeof __g && (__g = global); }, function(module, exports, __webpack_require__) { - module.exports = !__webpack_require__(53)(function() { + module.exports = !__webpack_require__(57)(function() { return 7 != Object.defineProperty({}, "a", { get: function() { return 7; } }).a; }); -}, function(module, exports, __webpack_require__) { - module.exports = { - default: __webpack_require__(394), - __esModule: !0 - }; }, function(module, exports, __webpack_require__) { "use strict"; - exports.__esModule = !0, exports.default = function(instance, Constructor) { - if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); - }; -}, function(module, exports, __webpack_require__) { - "use strict"; - exports.__esModule = !0; - var _defineProperty = __webpack_require__(154), _defineProperty2 = function(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - }(_defineProperty); - exports.default = function() { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0, - "value" in descriptor && (descriptor.writable = !0), (0, _defineProperty2.default)(target, descriptor.key, descriptor); - } + (function(process) { + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; } - return function(Constructor, protoProps, staticProps) { - return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), - Constructor; - }; - }(); -}, function(module, exports, __webpack_require__) { - "use strict"; - exports.__esModule = !0; - var _typeof2 = __webpack_require__(105), _typeof3 = function(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - }(_typeof2); - exports.default = function(self, call) { - if (!self) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - return !call || "object" !== (void 0 === call ? "undefined" : (0, _typeof3.default)(call)) && "function" != typeof call ? self : call; - }; -}, function(module, exports, __webpack_require__) { - "use strict"; - function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - } - exports.__esModule = !0; - var _setPrototypeOf = __webpack_require__(411), _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf), _create = __webpack_require__(415), _create2 = _interopRequireDefault(_create), _typeof2 = __webpack_require__(105), _typeof3 = _interopRequireDefault(_typeof2); - exports.default = function(subClass, superClass) { - if ("function" != typeof superClass && null !== superClass) throw new TypeError("Super expression must either be null or a function, not " + (void 0 === superClass ? "undefined" : (0, - _typeof3.default)(superClass))); - subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, { - constructor: { - value: subClass, - enumerable: !1, - writable: !0, - configurable: !0 + function capitalize(string) { + if ("production" !== process.env.NODE_ENV && "string" != typeof string) throw new Error("Material-UI: capitalize(string) expects a string argument."); + return string.charAt(0).toUpperCase() + string.slice(1); + } + function contains(obj, pred) { + return (0, _keys2.default)(pred).every(function(key) { + return obj.hasOwnProperty(key) && obj[key] === pred[key]; + }); + } + function findIndex(arr, pred) { + for (var predType = void 0 === pred ? "undefined" : (0, _typeof3.default)(pred), i = 0; i < arr.length; i += 1) { + if ("function" === predType && !0 == !!pred(arr[i], i, arr)) return i; + if ("object" === predType && contains(arr[i], pred)) return i; + if (-1 !== [ "string", "number", "boolean" ].indexOf(predType)) return arr.indexOf(pred); } - }), superClass && (_setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass); - }; + return -1; + } + function find(arr, pred) { + var index = findIndex(arr, pred); + return index > -1 ? arr[index] : void 0; + } + function createChainedFunction() { + for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) funcs[_key] = arguments[_key]; + return funcs.filter(function(func) { + return null != func; + }).reduce(function(acc, func) { + return "production" !== process.env.NODE_ENV && (0, _warning2.default)("function" == typeof func, "Material-UI: invalid Argument Type, must only provide functions, undefined, or null."), + function() { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) args[_key2] = arguments[_key2]; + acc.apply(this, args), func.apply(this, args); + }; + }, function() {}); + } + Object.defineProperty(exports, "__esModule", { + value: !0 + }); + var _typeof2 = __webpack_require__(111), _typeof3 = _interopRequireDefault(_typeof2), _keys = __webpack_require__(43), _keys2 = _interopRequireDefault(_keys); + exports.capitalize = capitalize, exports.contains = contains, exports.findIndex = findIndex, + exports.find = find, exports.createChainedFunction = createChainedFunction; + var _warning = __webpack_require__(17), _warning2 = _interopRequireDefault(_warning); + }).call(exports, __webpack_require__(2)); }, function(module, exports, __webpack_require__) { - var freeGlobal = __webpack_require__(268), freeSelf = "object" == typeof self && self && self.Object === Object && self, root = freeGlobal || freeSelf || Function("return this")(); + "use strict"; + function ownerDocument(node) { + return node && node.ownerDocument || document; + } + Object.defineProperty(exports, "__esModule", { + value: !0 + }), exports.default = ownerDocument, module.exports = exports.default; +}, function(module, exports, __webpack_require__) { + var freeGlobal = __webpack_require__(294), freeSelf = "object" == typeof self && self && self.Object === Object && self, root = freeGlobal || freeSelf || Function("return this")(); module.exports = root; }, function(module, exports) { function isObject(value) { @@ -1710,7 +1782,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { Object.defineProperty(exports, "__esModule", { value: !0 }), exports.translateStyle = exports.AnimateGroup = exports.configBezier = exports.configSpring = void 0; - var _Animate = __webpack_require__(287), _Animate2 = _interopRequireDefault(_Animate), _easing = __webpack_require__(305), _util = __webpack_require__(132), _AnimateGroup = __webpack_require__(762), _AnimateGroup2 = _interopRequireDefault(_AnimateGroup); + var _Animate = __webpack_require__(313), _Animate2 = _interopRequireDefault(_Animate), _easing = __webpack_require__(331), _util = __webpack_require__(140), _AnimateGroup = __webpack_require__(823), _AnimateGroup2 = _interopRequireDefault(_AnimateGroup); exports.configSpring = _easing.configSpring, exports.configBezier = _easing.configBezier, exports.AnimateGroup = _AnimateGroup2.default, exports.translateStyle = _util.translateStyle, exports.default = _Animate2.default; @@ -1722,36 +1794,36 @@ var _bundleJs = []byte((((((((((`!function(modules) { return "object" == typeof it ? null !== it : "function" == typeof it; }; }, function(module, exports, __webpack_require__) { - var freeGlobal = __webpack_require__(292), freeSelf = "object" == typeof self && self && self.Object === Object && self, root = freeGlobal || freeSelf || Function("return this")(); + var freeGlobal = __webpack_require__(318), freeSelf = "object" == typeof self && self && self.Object === Object && self, root = freeGlobal || freeSelf || Function("return this")(); module.exports = root; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; - var __WEBPACK_IMPORTED_MODULE_0__src_bisect__ = __webpack_require__(332); + var __WEBPACK_IMPORTED_MODULE_0__src_bisect__ = __webpack_require__(358); __webpack_require__.d(__webpack_exports__, "b", function() { return __WEBPACK_IMPORTED_MODULE_0__src_bisect__.a; }); - var __WEBPACK_IMPORTED_MODULE_1__src_ascending__ = __webpack_require__(69); + var __WEBPACK_IMPORTED_MODULE_1__src_ascending__ = __webpack_require__(73); __webpack_require__.d(__webpack_exports__, "a", function() { return __WEBPACK_IMPORTED_MODULE_1__src_ascending__.a; }); - var __WEBPACK_IMPORTED_MODULE_2__src_bisector__ = __webpack_require__(333); + var __WEBPACK_IMPORTED_MODULE_2__src_bisector__ = __webpack_require__(359); __webpack_require__.d(__webpack_exports__, "c", function() { return __WEBPACK_IMPORTED_MODULE_2__src_bisector__.a; }); - var __WEBPACK_IMPORTED_MODULE_18__src_quantile__ = (__webpack_require__(848), __webpack_require__(849), - __webpack_require__(335), __webpack_require__(337), __webpack_require__(850), __webpack_require__(853), - __webpack_require__(854), __webpack_require__(341), __webpack_require__(855), __webpack_require__(856), - __webpack_require__(857), __webpack_require__(858), __webpack_require__(342), __webpack_require__(334), - __webpack_require__(859), __webpack_require__(204)); + var __WEBPACK_IMPORTED_MODULE_18__src_quantile__ = (__webpack_require__(909), __webpack_require__(910), + __webpack_require__(361), __webpack_require__(363), __webpack_require__(911), __webpack_require__(914), + __webpack_require__(915), __webpack_require__(367), __webpack_require__(916), __webpack_require__(917), + __webpack_require__(918), __webpack_require__(919), __webpack_require__(368), __webpack_require__(360), + __webpack_require__(920), __webpack_require__(220)); __webpack_require__.d(__webpack_exports__, "d", function() { return __WEBPACK_IMPORTED_MODULE_18__src_quantile__.a; }); - var __WEBPACK_IMPORTED_MODULE_19__src_range__ = __webpack_require__(339); + var __WEBPACK_IMPORTED_MODULE_19__src_range__ = __webpack_require__(365); __webpack_require__.d(__webpack_exports__, "e", function() { return __WEBPACK_IMPORTED_MODULE_19__src_range__.a; }); - var __WEBPACK_IMPORTED_MODULE_23__src_ticks__ = (__webpack_require__(860), __webpack_require__(861), - __webpack_require__(862), __webpack_require__(340)); + var __WEBPACK_IMPORTED_MODULE_23__src_ticks__ = (__webpack_require__(921), __webpack_require__(922), + __webpack_require__(923), __webpack_require__(366)); __webpack_require__.d(__webpack_exports__, "h", function() { return __WEBPACK_IMPORTED_MODULE_23__src_ticks__.a; }), __webpack_require__.d(__webpack_exports__, "f", function() { @@ -1759,7 +1831,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "g", function() { return __WEBPACK_IMPORTED_MODULE_23__src_ticks__.c; }); - __webpack_require__(343), __webpack_require__(336), __webpack_require__(863); + __webpack_require__(369), __webpack_require__(362), __webpack_require__(924); }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.d(__webpack_exports__, "d", function() { @@ -1775,12 +1847,17 @@ var _bundleJs = []byte((((((((((`!function(modules) { }); var durationSecond = 1e3, durationMinute = 6e4, durationHour = 36e5, durationDay = 864e5, durationWeek = 6048e5; }, function(module, exports, __webpack_require__) { - var dP = __webpack_require__(22), createDesc = __webpack_require__(75); - module.exports = __webpack_require__(25) ? function(object, key, value) { + var dP = __webpack_require__(28), createDesc = __webpack_require__(79); + module.exports = __webpack_require__(31) ? function(object, key, value) { return dP.f(object, key, createDesc(1, value)); } : function(object, key, value) { return object[key] = value, object; }; +}, function(module, exports, __webpack_require__) { + module.exports = { + default: __webpack_require__(447), + __esModule: !0 + }; }, function(module, exports) { var g; g = function() { @@ -1796,7 +1873,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { function baseGetTag(value) { return null == value ? void 0 === value ? undefinedTag : nullTag : symToStringTag && symToStringTag in Object(value) ? getRawTag(value) : objectToString(value); } - var Symbol = __webpack_require__(83), getRawTag = __webpack_require__(603), objectToString = __webpack_require__(604), nullTag = "[object Null]", undefinedTag = "[object Undefined]", symToStringTag = Symbol ? Symbol.toStringTag : void 0; + var Symbol = __webpack_require__(90), getRawTag = __webpack_require__(664), objectToString = __webpack_require__(665), nullTag = "[object Null]", undefinedTag = "[object Undefined]", symToStringTag = Symbol ? Symbol.toStringTag : void 0; module.exports = baseGetTag; }, function(module, exports) { function isObjectLike(value) { @@ -1832,7 +1909,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { className: __WEBPACK_IMPORTED_MODULE_5_classnames___default()("recharts-label", className) }, attrs, positionAttrs), label); } - var __WEBPACK_IMPORTED_MODULE_0_lodash_isObject__ = __webpack_require__(32), __WEBPACK_IMPORTED_MODULE_0_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_3_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_3_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_react__), __WEBPACK_IMPORTED_MODULE_4_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_4_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_prop_types__), __WEBPACK_IMPORTED_MODULE_5_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_5_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_classnames__), __WEBPACK_IMPORTED_MODULE_6__Text__ = __webpack_require__(61), __WEBPACK_IMPORTED_MODULE_7__util_ReactUtils__ = __webpack_require__(4), __WEBPACK_IMPORTED_MODULE_8__util_DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_9__util_PolarUtils__ = __webpack_require__(23), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_isObject__ = __webpack_require__(35), __WEBPACK_IMPORTED_MODULE_0_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_3_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_3_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_react__), __WEBPACK_IMPORTED_MODULE_4_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_4_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_prop_types__), __WEBPACK_IMPORTED_MODULE_5_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_5_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_classnames__), __WEBPACK_IMPORTED_MODULE_6__Text__ = __webpack_require__(64), __WEBPACK_IMPORTED_MODULE_7__util_ReactUtils__ = __webpack_require__(7), __WEBPACK_IMPORTED_MODULE_8__util_DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_9__util_PolarUtils__ = __webpack_require__(29), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -2046,11 +2123,11 @@ var _bundleJs = []byte((((((((((`!function(modules) { function isEqual(value, other) { return baseIsEqual(value, other); } - var baseIsEqual = __webpack_require__(199); + var baseIsEqual = __webpack_require__(215); module.exports = isEqual; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; - var __WEBPACK_IMPORTED_MODULE_0__src_color__ = __webpack_require__(207); + var __WEBPACK_IMPORTED_MODULE_0__src_color__ = __webpack_require__(223); __webpack_require__.d(__webpack_exports__, "a", function() { return __WEBPACK_IMPORTED_MODULE_0__src_color__.e; }), __webpack_require__.d(__webpack_exports__, "f", function() { @@ -2058,13 +2135,13 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), __webpack_require__.d(__webpack_exports__, "d", function() { return __WEBPACK_IMPORTED_MODULE_0__src_color__.f; }); - var __WEBPACK_IMPORTED_MODULE_1__src_lab__ = __webpack_require__(871); + var __WEBPACK_IMPORTED_MODULE_1__src_lab__ = __webpack_require__(932); __webpack_require__.d(__webpack_exports__, "e", function() { return __WEBPACK_IMPORTED_MODULE_1__src_lab__.a; }), __webpack_require__.d(__webpack_exports__, "c", function() { return __WEBPACK_IMPORTED_MODULE_1__src_lab__.b; }); - var __WEBPACK_IMPORTED_MODULE_2__src_cubehelix__ = __webpack_require__(872); + var __WEBPACK_IMPORTED_MODULE_2__src_cubehelix__ = __webpack_require__(933); __webpack_require__.d(__webpack_exports__, "b", function() { return __WEBPACK_IMPORTED_MODULE_2__src_cubehelix__.a; }); @@ -2100,7 +2177,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { })); })) : null; } - var __WEBPACK_IMPORTED_MODULE_0_lodash_isObject__ = __webpack_require__(32), __WEBPACK_IMPORTED_MODULE_0_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_3_lodash_last__ = __webpack_require__(922), __WEBPACK_IMPORTED_MODULE_3_lodash_last___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_last__), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray__ = __webpack_require__(13), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7__Label__ = __webpack_require__(44), __WEBPACK_IMPORTED_MODULE_8__container_Layer__ = __webpack_require__(14), __WEBPACK_IMPORTED_MODULE_9__util_ReactUtils__ = __webpack_require__(4), __WEBPACK_IMPORTED_MODULE_10__util_ChartUtils__ = __webpack_require__(16), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_isObject__ = __webpack_require__(35), __WEBPACK_IMPORTED_MODULE_0_lodash_isObject___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isObject__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_2_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_3_lodash_last__ = __webpack_require__(983), __WEBPACK_IMPORTED_MODULE_3_lodash_last___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_last__), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray__ = __webpack_require__(18), __WEBPACK_IMPORTED_MODULE_4_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7__Label__ = __webpack_require__(48), __WEBPACK_IMPORTED_MODULE_8__container_Layer__ = __webpack_require__(19), __WEBPACK_IMPORTED_MODULE_9__util_ReactUtils__ = __webpack_require__(7), __WEBPACK_IMPORTED_MODULE_10__util_ChartUtils__ = __webpack_require__(21), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -2184,7 +2261,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var __WEBPACK_IMPORTED_MODULE_0_lodash_sortBy__ = __webpack_require__(321), __WEBPACK_IMPORTED_MODULE_0_lodash_sortBy___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_sortBy__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_range__ = __webpack_require__(373), __WEBPACK_IMPORTED_MODULE_2_lodash_range___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_range__), __WEBPACK_IMPORTED_MODULE_3_lodash_throttle__ = __webpack_require__(933), __WEBPACK_IMPORTED_MODULE_3_lodash_throttle___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_throttle__), __WEBPACK_IMPORTED_MODULE_4_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_4_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_7_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_7_classnames__), __WEBPACK_IMPORTED_MODULE_8__container_Surface__ = __webpack_require__(82), __WEBPACK_IMPORTED_MODULE_9__container_Layer__ = __webpack_require__(14), __WEBPACK_IMPORTED_MODULE_10__component_Tooltip__ = __webpack_require__(125), __WEBPACK_IMPORTED_MODULE_11__component_Legend__ = __webpack_require__(180), __WEBPACK_IMPORTED_MODULE_12__shape_Curve__ = __webpack_require__(71), __WEBPACK_IMPORTED_MODULE_13__shape_Cross__ = __webpack_require__(367), __WEBPACK_IMPORTED_MODULE_14__shape_Sector__ = __webpack_require__(139), __WEBPACK_IMPORTED_MODULE_15__shape_Dot__ = __webpack_require__(63), __WEBPACK_IMPORTED_MODULE_16__shape_Rectangle__ = __webpack_require__(70), __WEBPACK_IMPORTED_MODULE_17__util_ReactUtils__ = __webpack_require__(4), __WEBPACK_IMPORTED_MODULE_18__cartesian_CartesianAxis__ = __webpack_require__(374), __WEBPACK_IMPORTED_MODULE_19__cartesian_Brush__ = __webpack_require__(372), __WEBPACK_IMPORTED_MODULE_20__util_DOMUtils__ = __webpack_require__(198), __WEBPACK_IMPORTED_MODULE_21__util_DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_22__util_ChartUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_23__util_PolarUtils__ = __webpack_require__(23), __WEBPACK_IMPORTED_MODULE_24__util_PureRender__ = __webpack_require__(5), __WEBPACK_IMPORTED_MODULE_25__util_Events__ = __webpack_require__(934), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_lodash_sortBy__ = __webpack_require__(347), __WEBPACK_IMPORTED_MODULE_0_lodash_sortBy___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_sortBy__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_lodash_range__ = __webpack_require__(399), __WEBPACK_IMPORTED_MODULE_2_lodash_range___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_lodash_range__), __WEBPACK_IMPORTED_MODULE_3_lodash_throttle__ = __webpack_require__(994), __WEBPACK_IMPORTED_MODULE_3_lodash_throttle___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_lodash_throttle__), __WEBPACK_IMPORTED_MODULE_4_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_4_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_5_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_5_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_react__), __WEBPACK_IMPORTED_MODULE_6_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_6_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6_prop_types__), __WEBPACK_IMPORTED_MODULE_7_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_7_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_7_classnames__), __WEBPACK_IMPORTED_MODULE_8__container_Surface__ = __webpack_require__(89), __WEBPACK_IMPORTED_MODULE_9__container_Layer__ = __webpack_require__(19), __WEBPACK_IMPORTED_MODULE_10__component_Tooltip__ = __webpack_require__(133), __WEBPACK_IMPORTED_MODULE_11__component_Legend__ = __webpack_require__(196), __WEBPACK_IMPORTED_MODULE_12__shape_Curve__ = __webpack_require__(75), __WEBPACK_IMPORTED_MODULE_13__shape_Cross__ = __webpack_require__(393), __WEBPACK_IMPORTED_MODULE_14__shape_Sector__ = __webpack_require__(147), __WEBPACK_IMPORTED_MODULE_15__shape_Dot__ = __webpack_require__(66), __WEBPACK_IMPORTED_MODULE_16__shape_Rectangle__ = __webpack_require__(74), __WEBPACK_IMPORTED_MODULE_17__util_ReactUtils__ = __webpack_require__(7), __WEBPACK_IMPORTED_MODULE_18__cartesian_CartesianAxis__ = __webpack_require__(400), __WEBPACK_IMPORTED_MODULE_19__cartesian_Brush__ = __webpack_require__(398), __WEBPACK_IMPORTED_MODULE_20__util_DOMUtils__ = __webpack_require__(214), __WEBPACK_IMPORTED_MODULE_21__util_DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_22__util_ChartUtils__ = __webpack_require__(21), __WEBPACK_IMPORTED_MODULE_23__util_PolarUtils__ = __webpack_require__(29), __WEBPACK_IMPORTED_MODULE_24__util_PureRender__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_25__util_Events__ = __webpack_require__(995), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -3239,7 +3316,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { return arg; }, module.exports = emptyFunction; }, function(module, exports, __webpack_require__) { - var aFunction = __webpack_require__(223); + var aFunction = __webpack_require__(239); module.exports = function(fn, that, length) { if (aFunction(fn), void 0 === that) return fn; switch (length) { @@ -3263,7 +3340,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }; }; }, function(module, exports, __webpack_require__) { - var isObject = __webpack_require__(35); + var isObject = __webpack_require__(38); module.exports = function(it) { if (!isObject(it)) throw TypeError(it + " is not an object!"); return it; @@ -3281,66 +3358,29 @@ var _bundleJs = []byte((((((((((`!function(modules) { module.exports = function(it, key) { return hasOwnProperty.call(it, key); }; -}, function(module, exports, __webpack_require__) { - module.exports = { - default: __webpack_require__(421), - __esModule: !0 - }; }, function(module, exports, __webpack_require__) { "use strict"; - (function(process) { - function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + Object.defineProperty(exports, "__esModule", { + value: !0 + }); + var _Typography = __webpack_require__(544); + Object.defineProperty(exports, "default", { + enumerable: !0, + get: function() { + return _interopRequireDefault(_Typography).default; } - function capitalize(string) { - if ("production" !== process.env.NODE_ENV && "string" != typeof string) throw new Error("Material-UI: capitalize(string) expects a string argument."); - return string.charAt(0).toUpperCase() + string.slice(1); - } - function contains(obj, pred) { - return (0, _keys2.default)(pred).every(function(key) { - return obj.hasOwnProperty(key) && obj[key] === pred[key]; - }); - } - function findIndex(arr, pred) { - for (var predType = void 0 === pred ? "undefined" : (0, _typeof3.default)(pred), i = 0; i < arr.length; i += 1) { - if ("function" === predType && !0 == !!pred(arr[i], i, arr)) return i; - if ("object" === predType && contains(arr[i], pred)) return i; - if (-1 !== [ "string", "number", "boolean" ].indexOf(predType)) return arr.indexOf(pred); - } - return -1; - } - function find(arr, pred) { - var index = findIndex(arr, pred); - return index > -1 ? arr[index] : void 0; - } - function createChainedFunction() { - for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) funcs[_key] = arguments[_key]; - return funcs.filter(function(func) { - return null != func; - }).reduce(function(acc, func) { - return "production" !== process.env.NODE_ENV && (0, _warning2.default)("function" == typeof func, "Material-UI: invalid Argument Type, must only provide functions, undefined, or null."), - function() { - for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) args[_key2] = arguments[_key2]; - acc.apply(this, args), func.apply(this, args); - }; - }, function() {}); - } - Object.defineProperty(exports, "__esModule", { - value: !0 - }); - var _typeof2 = __webpack_require__(105), _typeof3 = _interopRequireDefault(_typeof2), _keys = __webpack_require__(55), _keys2 = _interopRequireDefault(_keys); - exports.capitalize = capitalize, exports.contains = contains, exports.findIndex = findIndex, - exports.find = find, exports.createChainedFunction = createChainedFunction; - var _warning = __webpack_require__(11), _warning2 = _interopRequireDefault(_warning); - }).call(exports, __webpack_require__(2)); + }); }, function(module, exports, __webpack_require__) { function getNative(object, key) { var value = getValue(object, key); return baseIsNative(value) ? value : void 0; } - var baseIsNative = __webpack_require__(611), getValue = __webpack_require__(614); + var baseIsNative = __webpack_require__(672), getValue = __webpack_require__(675); module.exports = getNative; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -3354,13 +3394,13 @@ var _bundleJs = []byte((((((((((`!function(modules) { var value = getValue(object, key); return baseIsNative(value) ? value : void 0; } - var baseIsNative = __webpack_require__(668), getValue = __webpack_require__(673); + var baseIsNative = __webpack_require__(729), getValue = __webpack_require__(734); module.exports = getNative; }, function(module, exports, __webpack_require__) { function baseGetTag(value) { return null == value ? void 0 === value ? undefinedTag : nullTag : symToStringTag && symToStringTag in Object(value) ? getRawTag(value) : objectToString(value); } - var Symbol = __webpack_require__(128), getRawTag = __webpack_require__(669), objectToString = __webpack_require__(670), nullTag = "[object Null]", undefinedTag = "[object Undefined]", symToStringTag = Symbol ? Symbol.toStringTag : void 0; + var Symbol = __webpack_require__(136), getRawTag = __webpack_require__(730), objectToString = __webpack_require__(731), nullTag = "[object Null]", undefinedTag = "[object Undefined]", symToStringTag = Symbol ? Symbol.toStringTag : void 0; module.exports = baseGetTag; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -3387,7 +3427,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var _class, _temp2, __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(20), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_1_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_react__), __WEBPACK_IMPORTED_MODULE_2_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_2_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_prop_types__), __WEBPACK_IMPORTED_MODULE_3_reduce_css_calc__ = __webpack_require__(771), __WEBPACK_IMPORTED_MODULE_3_reduce_css_calc___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_reduce_css_calc__), __WEBPACK_IMPORTED_MODULE_4_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_4_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_classnames__), __WEBPACK_IMPORTED_MODULE_5__util_DataUtils__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_6__util_ReactUtils__ = __webpack_require__(4), __WEBPACK_IMPORTED_MODULE_7__util_DOMUtils__ = __webpack_require__(198), _extends = Object.assign || function(target) { + var _class, _temp2, __WEBPACK_IMPORTED_MODULE_0_lodash_isNil__ = __webpack_require__(25), __WEBPACK_IMPORTED_MODULE_0_lodash_isNil___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isNil__), __WEBPACK_IMPORTED_MODULE_1_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_1_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_react__), __WEBPACK_IMPORTED_MODULE_2_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_2_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_prop_types__), __WEBPACK_IMPORTED_MODULE_3_reduce_css_calc__ = __webpack_require__(832), __WEBPACK_IMPORTED_MODULE_3_reduce_css_calc___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_reduce_css_calc__), __WEBPACK_IMPORTED_MODULE_4_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_4_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_classnames__), __WEBPACK_IMPORTED_MODULE_5__util_DataUtils__ = __webpack_require__(16), __WEBPACK_IMPORTED_MODULE_6__util_ReactUtils__ = __webpack_require__(7), __WEBPACK_IMPORTED_MODULE_7__util_DOMUtils__ = __webpack_require__(214), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -3565,7 +3605,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3__util_PureRender__ = __webpack_require__(5), __WEBPACK_IMPORTED_MODULE_4__util_ReactUtils__ = __webpack_require__(4), _extends = Object.assign || function(target) { + var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3__util_PureRender__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_4__util_ReactUtils__ = __webpack_require__(7), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -3607,12 +3647,12 @@ var _bundleJs = []byte((((((((((`!function(modules) { }, _class = _temp)) || _class; __webpack_exports__.a = Dot; }, function(module, exports, __webpack_require__) { - var IObject = __webpack_require__(146), defined = __webpack_require__(148); + var IObject = __webpack_require__(154), defined = __webpack_require__(156); module.exports = function(it) { return IObject(defined(it)); }; }, function(module, exports, __webpack_require__) { - var defined = __webpack_require__(148); + var defined = __webpack_require__(156); module.exports = function(it) { return Object(defined(it)); }; @@ -3651,7 +3691,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), Constructor; }; - }(), _warning = __webpack_require__(11), _warning2 = _interopRequireDefault(_warning), _toCss = __webpack_require__(163), _toCss2 = _interopRequireDefault(_toCss), _toCssValue = __webpack_require__(110), _toCssValue2 = _interopRequireDefault(_toCssValue), StyleRule = function() { + }(), _warning = __webpack_require__(17), _warning2 = _interopRequireDefault(_warning), _toCss = __webpack_require__(171), _toCss2 = _interopRequireDefault(_toCss), _toCssValue = __webpack_require__(117), _toCssValue2 = _interopRequireDefault(_toCssValue), StyleRule = function() { function StyleRule(key, style, options) { _classCallCheck(this, StyleRule), this.type = "style", this.isProcessed = !1; var sheet = options.sheet, Renderer = options.Renderer, selector = options.selector; @@ -3715,11 +3755,28 @@ var _bundleJs = []byte((((((((((`!function(modules) { } ]), StyleRule; }(); exports.default = StyleRule; +}, function(module, exports, __webpack_require__) { + "use strict"; + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + Object.defineProperty(exports, "__esModule", { + value: !0 + }); + var _SvgIcon = __webpack_require__(538); + Object.defineProperty(exports, "default", { + enumerable: !0, + get: function() { + return _interopRequireDefault(_SvgIcon).default; + } + }); }, function(module, exports, __webpack_require__) { function isSymbol(value) { return "symbol" == typeof value || isObjectLike(value) && baseGetTag(value) == symbolTag; } - var baseGetTag = __webpack_require__(41), isObjectLike = __webpack_require__(42), symbolTag = "[object Symbol]"; + var baseGetTag = __webpack_require__(45), isObjectLike = __webpack_require__(46), symbolTag = "[object Symbol]"; module.exports = isSymbol; }, function(module, exports) { function identity(value) { @@ -3751,7 +3808,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var _class, _class2, _temp2, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3_react_smooth__ = __webpack_require__(33), __WEBPACK_IMPORTED_MODULE_3_react_smooth___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_react_smooth__), __WEBPACK_IMPORTED_MODULE_4__util_PureRender__ = __webpack_require__(5), __WEBPACK_IMPORTED_MODULE_5__util_ReactUtils__ = __webpack_require__(4), _extends = Object.assign || function(target) { + var _class, _class2, _temp2, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3_react_smooth__ = __webpack_require__(36), __WEBPACK_IMPORTED_MODULE_3_react_smooth___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_react_smooth__), __WEBPACK_IMPORTED_MODULE_4__util_PureRender__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_5__util_ReactUtils__ = __webpack_require__(7), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -3896,7 +3953,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_lodash_isArray__ = __webpack_require__(13), __WEBPACK_IMPORTED_MODULE_0_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(8), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_2_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_react__), __WEBPACK_IMPORTED_MODULE_3_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_3_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_prop_types__), __WEBPACK_IMPORTED_MODULE_4_d3_shape__ = __webpack_require__(182), __WEBPACK_IMPORTED_MODULE_5_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_5_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_classnames__), __WEBPACK_IMPORTED_MODULE_6__util_PureRender__ = __webpack_require__(5), __WEBPACK_IMPORTED_MODULE_7__util_ReactUtils__ = __webpack_require__(4), __WEBPACK_IMPORTED_MODULE_8__util_DataUtils__ = __webpack_require__(9), _extends = Object.assign || function(target) { + var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_lodash_isArray__ = __webpack_require__(18), __WEBPACK_IMPORTED_MODULE_0_lodash_isArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash_isArray__), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__ = __webpack_require__(15), __WEBPACK_IMPORTED_MODULE_1_lodash_isFunction___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash_isFunction__), __WEBPACK_IMPORTED_MODULE_2_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_2_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_react__), __WEBPACK_IMPORTED_MODULE_3_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_3_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3_prop_types__), __WEBPACK_IMPORTED_MODULE_4_d3_shape__ = __webpack_require__(198), __WEBPACK_IMPORTED_MODULE_5_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_5_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5_classnames__), __WEBPACK_IMPORTED_MODULE_6__util_PureRender__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_7__util_ReactUtils__ = __webpack_require__(7), __WEBPACK_IMPORTED_MODULE_8__util_DataUtils__ = __webpack_require__(16), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -4011,7 +4068,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = (__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), - __webpack_require__(1)), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__util_PureRender__ = __webpack_require__(5), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(4), _createClass = function() { + __webpack_require__(1)), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__util_PureRender__ = __webpack_require__(9), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(7), _createClass = function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; @@ -4106,7 +4163,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } var _class, _class2, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = (__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), - __webpack_require__(1)), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__util_PureRender__ = __webpack_require__(5), _createClass = function() { + __webpack_require__(1)), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__util_PureRender__ = __webpack_require__(9), _createClass = function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; @@ -4224,7 +4281,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }; }; }, function(module, exports, __webpack_require__) { - var $keys = __webpack_require__(226), enumBugKeys = __webpack_require__(152); + var $keys = __webpack_require__(242), enumBugKeys = __webpack_require__(160); module.exports = Object.keys || function(O) { return $keys(O, enumBugKeys); }; @@ -4276,13 +4333,13 @@ var _bundleJs = []byte((((((((((`!function(modules) { Object.defineProperty(exports, "__esModule", { value: !0 }), exports.keys = void 0; - var _extends2 = __webpack_require__(6), _extends3 = _interopRequireDefault(_extends2), _objectWithoutProperties2 = __webpack_require__(7), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2); + var _extends2 = __webpack_require__(5), _extends3 = _interopRequireDefault(_extends2), _objectWithoutProperties2 = __webpack_require__(4), _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2); exports.default = createBreakpoints; var keys = exports.keys = [ "xs", "sm", "md", "lg", "xl" ]; }, function(module, exports, __webpack_require__) { "use strict"; exports.__esModule = !0; - var _getDisplayName = __webpack_require__(244), _getDisplayName2 = function(obj) { + var _getDisplayName = __webpack_require__(261), _getDisplayName2 = function(obj) { return obj && obj.__esModule ? obj : { default: obj }; @@ -4321,7 +4378,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), Constructor; }; - }(), _createRule = __webpack_require__(111), _createRule2 = _interopRequireDefault(_createRule), _linkRule = __webpack_require__(249), _linkRule2 = _interopRequireDefault(_linkRule), _StyleRule = __webpack_require__(66), _StyleRule2 = _interopRequireDefault(_StyleRule), _escape = __webpack_require__(467), _escape2 = _interopRequireDefault(_escape), RuleList = function() { + }(), _createRule = __webpack_require__(118), _createRule2 = _interopRequireDefault(_createRule), _linkRule = __webpack_require__(266), _linkRule2 = _interopRequireDefault(_linkRule), _StyleRule = __webpack_require__(69), _StyleRule2 = _interopRequireDefault(_StyleRule), _escape = __webpack_require__(491), _escape2 = _interopRequireDefault(_escape), RuleList = function() { function RuleList(options) { _classCallCheck(this, RuleList), this.map = {}, this.raw = {}, this.index = [], this.options = options, this.classes = options.classes; @@ -4407,6 +4464,293 @@ var _bundleJs = []byte((((((((((`!function(modules) { } ]), RuleList; }(); exports.default = RuleList; +}, function(module, exports) { + exports = module.exports = function(searchInput) { + if (searchInput && "object" == typeof searchInput) { + var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; + hasKeyCode && (searchInput = hasKeyCode); + } + if ("number" == typeof searchInput) return names[searchInput]; + var search = String(searchInput), foundNamedKey = codes[search.toLowerCase()]; + if (foundNamedKey) return foundNamedKey; + var foundNamedKey = aliases[search.toLowerCase()]; + return foundNamedKey || (1 === search.length ? search.charCodeAt(0) : void 0); + }; + var codes = exports.code = exports.codes = { + backspace: 8, + tab: 9, + enter: 13, + shift: 16, + ctrl: 17, + alt: 18, + "pause/break": 19, + "caps lock": 20, + esc: 27, + space: 32, + "page up": 33, + "page down": 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + insert: 45, + delete: 46, + command: 91, + "left command": 91, + "right command": 93, + "numpad *": 106, + "numpad +": 107, + "numpad -": 109, + "numpad .": 110, + "numpad /": 111, + "num lock": 144, + "scroll lock": 145, + "my computer": 182, + "my calculator": 183, + ";": 186, + "=": 187, + ",": 188, + "-": 189, + ".": 190, + "/": 191, + "`)))) + ((("`" + (`": 192, + "[": 219, + "\\": 220, + "]": 221, + "'": 222 + }, aliases = exports.aliases = { + windows: 91, + "⇧": 16, + "⌥": 18, + "⌃": 17, + "⌘": 91, + ctl: 17, + control: 17, + option: 18, + pause: 19, + break: 19, + caps: 20, + return: 13, + escape: 27, + spc: 32, + pgup: 33, + pgdn: 34, + ins: 45, + del: 46, + cmd: 91 + }; + for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32; + for (var i = 48; i < 58; i++) codes[i - 48] = i; + for (i = 1; i < 13; i++) codes["f" + i] = i + 111; + for (i = 0; i < 10; i++) codes["numpad " + i] = i + 96; + var names = exports.names = exports.title = {}; + for (i in codes) names[codes[i]] = i; + for (var alias in aliases) codes[alias] = aliases[alias]; +}, function(module, exports, __webpack_require__) { + "use strict"; + (function(process) { + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + function _objectWithoutProperties(obj, keys) { + var target = {}; + for (var i in obj) keys.indexOf(i) >= 0 || Object.prototype.hasOwnProperty.call(obj, i) && (target[i] = obj[i]); + return target; + } + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function"); + } + function _possibleConstructorReturn(self, call) { + if (!self) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !call || "object" != typeof call && "function" != typeof call ? self : call; + } + function _inherits(subClass, superClass) { + if ("function" != typeof superClass && null !== superClass) throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: !1, + writable: !0, + configurable: !0 + } + }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); + } + function noop() {} + exports.__esModule = !0, exports.EXITING = exports.ENTERED = exports.ENTERING = exports.EXITED = exports.UNMOUNTED = void 0; + var _propTypes = __webpack_require__(1), PropTypes = function(obj) { + if (obj && obj.__esModule) return obj; + var newObj = {}; + if (null != obj) for (var key in obj) Object.prototype.hasOwnProperty.call(obj, key) && (newObj[key] = obj[key]); + return newObj.default = obj, newObj; + }(_propTypes), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _reactDom = __webpack_require__(27), _reactDom2 = _interopRequireDefault(_reactDom), _PropTypes = __webpack_require__(536), UNMOUNTED = exports.UNMOUNTED = "unmounted", EXITED = exports.EXITED = "exited", ENTERING = exports.ENTERING = "entering", ENTERED = exports.ENTERED = "entered", EXITING = exports.EXITING = "exiting", Transition = function(_React$Component) { + function Transition(props, context) { + _classCallCheck(this, Transition); + var _this = _possibleConstructorReturn(this, _React$Component.call(this, props, context)), parentGroup = context.transitionGroup, appear = parentGroup && !parentGroup.isMounting ? props.enter : props.appear, initialStatus = void 0; + return _this.nextStatus = null, props.in ? appear ? (initialStatus = EXITED, _this.nextStatus = ENTERING) : initialStatus = ENTERED : initialStatus = props.unmountOnExit || props.mountOnEnter ? UNMOUNTED : EXITED, + _this.state = { + status: initialStatus + }, _this.nextCallback = null, _this; + } + return _inherits(Transition, _React$Component), Transition.prototype.getChildContext = function() { + return { + transitionGroup: null + }; + }, Transition.prototype.componentDidMount = function() { + this.updateStatus(!0); + }, Transition.prototype.componentWillReceiveProps = function(nextProps) { + var _ref = this.pendingState || this.state, status = _ref.status; + nextProps.in ? (status === UNMOUNTED && this.setState({ + status: EXITED + }), status !== ENTERING && status !== ENTERED && (this.nextStatus = ENTERING)) : status !== ENTERING && status !== ENTERED || (this.nextStatus = EXITING); + }, Transition.prototype.componentDidUpdate = function() { + this.updateStatus(); + }, Transition.prototype.componentWillUnmount = function() { + this.cancelNextCallback(); + }, Transition.prototype.getTimeouts = function() { + var timeout = this.props.timeout, exit = void 0, enter = void 0, appear = void 0; + return exit = enter = appear = timeout, null != timeout && "number" != typeof timeout && (exit = timeout.exit, + enter = timeout.enter, appear = timeout.appear), { + exit: exit, + enter: enter, + appear: appear + }; + }, Transition.prototype.updateStatus = function() { + var mounting = arguments.length > 0 && void 0 !== arguments[0] && arguments[0], nextStatus = this.nextStatus; + if (null !== nextStatus) { + this.nextStatus = null, this.cancelNextCallback(); + var node = _reactDom2.default.findDOMNode(this); + nextStatus === ENTERING ? this.performEnter(node, mounting) : this.performExit(node); + } else this.props.unmountOnExit && this.state.status === EXITED && this.setState({ + status: UNMOUNTED + }); + }, Transition.prototype.performEnter = function(node, mounting) { + var _this2 = this, enter = this.props.enter, appearing = this.context.transitionGroup ? this.context.transitionGroup.isMounting : mounting, timeouts = this.getTimeouts(); + if (!mounting && !enter) return void this.safeSetState({ + status: ENTERED + }, function() { + _this2.props.onEntered(node); + }); + this.props.onEnter(node, appearing), this.safeSetState({ + status: ENTERING + }, function() { + _this2.props.onEntering(node, appearing), _this2.onTransitionEnd(node, timeouts.enter, function() { + _this2.safeSetState({ + status: ENTERED + }, function() { + _this2.props.onEntered(node, appearing); + }); + }); + }); + }, Transition.prototype.performExit = function(node) { + var _this3 = this, exit = this.props.exit, timeouts = this.getTimeouts(); + if (!exit) return void this.safeSetState({ + status: EXITED + }, function() { + _this3.props.onExited(node); + }); + this.props.onExit(node), this.safeSetState({ + status: EXITING + }, function() { + _this3.props.onExiting(node), _this3.onTransitionEnd(node, timeouts.exit, function() { + _this3.safeSetState({ + status: EXITED + }, function() { + _this3.props.onExited(node); + }); + }); + }); + }, Transition.prototype.cancelNextCallback = function() { + null !== this.nextCallback && (this.nextCallback.cancel(), this.nextCallback = null); + }, Transition.prototype.safeSetState = function(nextState, callback) { + var _this4 = this; + this.pendingState = nextState, callback = this.setNextCallback(callback), this.setState(nextState, function() { + _this4.pendingState = null, callback(); + }); + }, Transition.prototype.setNextCallback = function(callback) { + var _this5 = this, active = !0; + return this.nextCallback = function(event) { + active && (active = !1, _this5.nextCallback = null, callback(event)); + }, this.nextCallback.cancel = function() { + active = !1; + }, this.nextCallback; + }, Transition.prototype.onTransitionEnd = function(node, timeout, handler) { + this.setNextCallback(handler), node ? (this.props.addEndListener && this.props.addEndListener(node, this.nextCallback), + null != timeout && setTimeout(this.nextCallback, timeout)) : setTimeout(this.nextCallback, 0); + }, Transition.prototype.render = function() { + var status = this.state.status; + if (status === UNMOUNTED) return null; + var _props = this.props, children = _props.children, childProps = _objectWithoutProperties(_props, [ "children" ]); + if (delete childProps.in, delete childProps.mountOnEnter, delete childProps.unmountOnExit, + delete childProps.appear, delete childProps.enter, delete childProps.exit, delete childProps.timeout, + delete childProps.addEndListener, delete childProps.onEnter, delete childProps.onEntering, + delete childProps.onEntered, delete childProps.onExit, delete childProps.onExiting, + delete childProps.onExited, "function" == typeof children) return children(status, childProps); + var child = _react2.default.Children.only(children); + return _react2.default.cloneElement(child, childProps); + }, Transition; + }(_react2.default.Component); + Transition.contextTypes = { + transitionGroup: PropTypes.object + }, Transition.childContextTypes = { + transitionGroup: function() {} + }, Transition.propTypes = "production" !== process.env.NODE_ENV ? { + children: PropTypes.oneOfType([ PropTypes.func.isRequired, PropTypes.element.isRequired ]).isRequired, + in: PropTypes.bool, + mountOnEnter: PropTypes.bool, + unmountOnExit: PropTypes.bool, + appear: PropTypes.bool, + enter: PropTypes.bool, + exit: PropTypes.bool, + timeout: function(props) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) args[_key - 1] = arguments[_key]; + var pt = _PropTypes.timeoutsShape; + return props.addEndListener || (pt = pt.isRequired), pt.apply(void 0, [ props ].concat(args)); + }, + addEndListener: PropTypes.func, + onEnter: PropTypes.func, + onEntering: PropTypes.func, + onEntered: PropTypes.func, + onExit: PropTypes.func, + onExiting: PropTypes.func, + onExited: PropTypes.func + } : {}, Transition.defaultProps = { + in: !1, + mountOnEnter: !1, + unmountOnExit: !1, + appear: !1, + enter: !0, + exit: !0, + onEnter: noop, + onEntering: noop, + onEntered: noop, + onExit: noop, + onExiting: noop, + onExited: noop + }, Transition.UNMOUNTED = 0, Transition.EXITED = 1, Transition.ENTERING = 2, Transition.ENTERED = 3, + Transition.EXITING = 4, exports.default = Transition; + }).call(exports, __webpack_require__(2)); +}, function(module, exports, __webpack_require__) { + "use strict"; + (function(process) { + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; + } + exports.__esModule = !0; + var _shouldUpdate = __webpack_require__(541), _shouldUpdate2 = _interopRequireDefault(_shouldUpdate), _shallowEqual = __webpack_require__(543), _shallowEqual2 = _interopRequireDefault(_shallowEqual), _setDisplayName = __webpack_require__(274), _setDisplayName2 = _interopRequireDefault(_setDisplayName), _wrapDisplayName = __webpack_require__(83), _wrapDisplayName2 = _interopRequireDefault(_wrapDisplayName), pure = function(BaseComponent) { + var hoc = (0, _shouldUpdate2.default)(function(props, nextProps) { + return !(0, _shallowEqual2.default)(props, nextProps); + }); + return "production" !== process.env.NODE_ENV ? (0, _setDisplayName2.default)((0, + _wrapDisplayName2.default)(BaseComponent, "pure"))(hoc(BaseComponent)) : hoc(BaseComponent); + }; + exports.default = pure; + }).call(exports, __webpack_require__(2)); }, function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4488,7 +4832,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { version: "1.1" }), children); } - var __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(4), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2_classnames__ = __webpack_require__(3), __WEBPACK_IMPORTED_MODULE_2_classnames___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_classnames__), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(7), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -4509,11 +4853,11 @@ var _bundleJs = []byte((((((((((`!function(modules) { }; Surface.propTypes = propTypes, __webpack_exports__.a = Surface; }, function(module, exports, __webpack_require__) { - var root = __webpack_require__(31), Symbol = root.Symbol; + var root = __webpack_require__(34), Symbol = root.Symbol; module.exports = Symbol; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; - var __WEBPACK_IMPORTED_MODULE_0__src_path__ = __webpack_require__(633); + var __WEBPACK_IMPORTED_MODULE_0__src_path__ = __webpack_require__(694); __webpack_require__.d(__webpack_exports__, "a", function() { return __WEBPACK_IMPORTED_MODULE_0__src_path__.a; }); @@ -4567,7 +4911,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { return null; } var __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_1__util_ReactUtils__ = (__webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), - __webpack_require__(4)), _extends = Object.assign || function(target) { + __webpack_require__(7)), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -4580,7 +4924,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { function baseIteratee(value) { return "function" == typeof value ? value : null == value ? identity : "object" == typeof value ? isArray(value) ? baseMatchesProperty(value[0], value[1]) : baseMatches(value) : property(value); } - var baseMatches = __webpack_require__(815), baseMatchesProperty = __webpack_require__(818), identity = __webpack_require__(68), isArray = __webpack_require__(13), property = __webpack_require__(822); + var baseMatches = __webpack_require__(876), baseMatchesProperty = __webpack_require__(879), identity = __webpack_require__(72), isArray = __webpack_require__(18), property = __webpack_require__(883); module.exports = baseIteratee; }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -4616,29 +4960,29 @@ var _bundleJs = []byte((((((((((`!function(modules) { }, linearish(scale); } __webpack_exports__.b = linearish, __webpack_exports__.a = linear; - var __WEBPACK_IMPORTED_MODULE_0_d3_array__ = __webpack_require__(37), __WEBPACK_IMPORTED_MODULE_1_d3_interpolate__ = __webpack_require__(92), __WEBPACK_IMPORTED_MODULE_2__continuous__ = __webpack_require__(137), __WEBPACK_IMPORTED_MODULE_3__tickFormat__ = __webpack_require__(883); + var __WEBPACK_IMPORTED_MODULE_0_d3_array__ = __webpack_require__(40), __WEBPACK_IMPORTED_MODULE_1_d3_interpolate__ = __webpack_require__(99), __WEBPACK_IMPORTED_MODULE_2__continuous__ = __webpack_require__(145), __WEBPACK_IMPORTED_MODULE_3__tickFormat__ = __webpack_require__(944); }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; - var __WEBPACK_IMPORTED_MODULE_0__src_value__ = __webpack_require__(206); + var __WEBPACK_IMPORTED_MODULE_0__src_value__ = __webpack_require__(222); __webpack_require__.d(__webpack_exports__, "a", function() { return __WEBPACK_IMPORTED_MODULE_0__src_value__.a; }); - var __WEBPACK_IMPORTED_MODULE_5__src_number__ = (__webpack_require__(349), __webpack_require__(209), - __webpack_require__(347), __webpack_require__(350), __webpack_require__(136)); + var __WEBPACK_IMPORTED_MODULE_5__src_number__ = (__webpack_require__(375), __webpack_require__(225), + __webpack_require__(373), __webpack_require__(376), __webpack_require__(144)); __webpack_require__.d(__webpack_exports__, "c", function() { return __WEBPACK_IMPORTED_MODULE_5__src_number__.a; }); - var __WEBPACK_IMPORTED_MODULE_7__src_round__ = (__webpack_require__(351), __webpack_require__(873)); + var __WEBPACK_IMPORTED_MODULE_7__src_round__ = (__webpack_require__(377), __webpack_require__(934)); __webpack_require__.d(__webpack_exports__, "d", function() { return __WEBPACK_IMPORTED_MODULE_7__src_round__.a; }); - var __WEBPACK_IMPORTED_MODULE_15__src_cubehelix__ = (__webpack_require__(352), __webpack_require__(874), - __webpack_require__(877), __webpack_require__(346), __webpack_require__(878), __webpack_require__(879), - __webpack_require__(880), __webpack_require__(881)); + var __WEBPACK_IMPORTED_MODULE_15__src_cubehelix__ = (__webpack_require__(378), __webpack_require__(935), + __webpack_require__(938), __webpack_require__(372), __webpack_require__(939), __webpack_require__(940), + __webpack_require__(941), __webpack_require__(942)); __webpack_require__.d(__webpack_exports__, "b", function() { return __WEBPACK_IMPORTED_MODULE_15__src_cubehelix__.a; }); - __webpack_require__(882); + __webpack_require__(943); }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; function linear(a, d) { @@ -4665,7 +5009,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { return d ? linear(a, d) : Object(__WEBPACK_IMPORTED_MODULE_0__constant__.a)(isNaN(a) ? b : a); } __webpack_exports__.c = hue, __webpack_exports__.b = gamma, __webpack_exports__.a = nogamma; - var __WEBPACK_IMPORTED_MODULE_0__constant__ = __webpack_require__(348); + var __WEBPACK_IMPORTED_MODULE_0__constant__ = __webpack_require__(374); }, function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_exports__.a = function(s) { @@ -4698,7 +5042,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } }), superClass && (Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass); } - var _class, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__container_Layer__ = __webpack_require__(14), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(4), _extends = Object.assign || function(target) { + var _class, _temp, __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(0), __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__), __WEBPACK_IMPORTED_MODULE_1_prop_types__ = __webpack_require__(1), __WEBPACK_IMPORTED_MODULE_1_prop_types___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_prop_types__), __WEBPACK_IMPORTED_MODULE_2__container_Layer__ = __webpack_require__(19), __WEBPACK_IMPORTED_MODULE_3__util_ReactUtils__ = __webpack_require__(7), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -4809,7 +5153,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { __webpack_require__.d(__webpack_exports__, "a", function() { return formatAxisMap; }); - var __WEBPACK_IMPORTED_MODULE_0__ChartUtils__ = __webpack_require__(16), _extends = Object.assign || function(target) { + var __WEBPACK_IMPORTED_MODULE_0__ChartUtils__ = __webpack_require__(21), _extends = Object.assign || function(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]); @@ -4860,7 +5204,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }, function(module, exports, __webpack_require__) { "use strict"; (function(process) { - var emptyFunction = __webpack_require__(50), warning = emptyFunction; + var emptyFunction = __webpack_require__(54), warning = emptyFunction; if ("production" !== process.env.NODE_ENV) { var printWarning = function(format) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) args[_key - 1] = arguments[_key]; @@ -4873,7 +5217,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { } catch (x) {} }; warning = function(condition, format) { - if (void 0 === format) throw new Error("` + ("`" + `warning(condition, format, ...args)`)) + ("`" + (` requires a warning message argument"); + if (void 0 === format) throw new Error("` + "`")) + (`warning(condition, format, ...args)` + ("`" + ` requires a warning message argument"); if (0 !== format.indexOf("Failed Composite propType: ") && !condition) { for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) args[_key2 - 2] = arguments[_key2]; printWarning.apply(void 0, [ format ].concat(args)); @@ -4882,21 +5226,6 @@ var _bundleJs = []byte((((((((((`!function(modules) { } module.exports = warning; }).call(exports, __webpack_require__(2)); -}, function(module, exports, __webpack_require__) { - "use strict"; - (function(process) { - function checkDCE() { - if ("undefined" != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" == typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE) { - if ("production" !== process.env.NODE_ENV) throw new Error("^_^"); - try { - __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); - } catch (err) { - console.error(err); - } - } - } - "production" === process.env.NODE_ENV ? (checkDCE(), module.exports = __webpack_require__(378)) : module.exports = __webpack_require__(381); - }).call(exports, __webpack_require__(2)); }, function(module, exports, __webpack_require__) { "use strict"; function is(x, y) { @@ -4913,7 +5242,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { var hasOwnProperty = Object.prototype.hasOwnProperty; module.exports = shallowEqual; }, function(module, exports, __webpack_require__) { - var toInteger = __webpack_require__(149), min = Math.min; + var toInteger = __webpack_require__(157), min = Math.min; module.exports = function(it) { return it > 0 ? min(toInteger(it), 9007199254740991) : 0; }; @@ -4934,7 +5263,7 @@ var _bundleJs = []byte((((((((((`!function(modules) { }; } exports.__esModule = !0; - var _iterator = __webpack_require__(396), _iterator2 = _interopRequireDefault(_iterator), _symbol = __webpack_require__(404), _symbol2 = _interopRequireDefault(_symbol), _typeof = "function" == typeof _symbol2.default && "symbol" == typeof _iterator2.default ? function(obj) { + var _iterator = __webpack_require__(422), _iterator2 = _interopRequireDefault(_iterator), _symbol = __webpack_require__(430), _symbol2 = _interopRequireDefault(_symbol), _typeof = "function" == typeof _symbol2.default && "symbol" == typeof _iterator2.default ? function(obj) { return typeof obj; } : function(obj) { return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : typeof obj; @@ -4945,9 +5274,9 @@ var _bundleJs = []byte((((((((((`!function(modules) { return obj && "function" == typeof _symbol2.default && obj.constructor === _symbol2.default && obj !== _symbol2.default.prototype ? "symbol" : void 0 === obj ? "undefined" : _typeof(obj); }; }, function(module, exports, __webpack_require__) { - var anObject = __webpack_require__(52), dPs = __webpack_require__(400), enumBugKeys = __webpack_require__(152), IE_PROTO = __webpack_require__(150)("IE_PROTO"), Empty = function() {}, createDict = function() { - var iframeDocument, iframe = __webpack_require__(225)("iframe"), i = enumBugKeys.length; - for (iframe.style.display = "none", __webpack_require__(401).appendChild(iframe), + var anObject = __webpack_require__(56), dPs = __webpack_require__(426), enumBugKeys = __webpack_require__(160), IE_PROTO = __webpack_require__(158)("IE_PROTO"), Empty = function() {}, createDict = function() { + var iframeDocument, iframe = __webpack_require__(241)("iframe"), i = enumBugKeys.length; + for (iframe.style.display = "none", __webpack_require__(427).appendChild(iframe), iframe.src = "javascript:", iframeDocument = iframe.contentWindow.document, iframeDocument.open(), iframeDocument.write("