浏览代码

dashboard 备注 客户端管理优化 多客户端支持 流量显示支持 热更新支持 404错误页支持

刘河 6 年之前
父节点
当前提交
c34e5e1a7d

+ 43 - 44
bridge/bridge.go

@@ -5,6 +5,7 @@ import (
 	"github.com/cnlh/easyProxy/utils"
 	"log"
 	"net"
+	"strconv"
 	"sync"
 	"time"
 )
@@ -31,20 +32,20 @@ func newList() *list {
 }
 
 type Tunnel struct {
-	TunnelPort int                    //通信隧道端口
-	listener   *net.TCPListener       //server端监听
-	SignalList map[string]*list       //通信
-	TunnelList map[string]*list       //隧道
-	RunList    map[string]interface{} //运行中的任务
+	TunnelPort int                 //通信隧道端口
+	listener   *net.TCPListener    //server端监听
+	SignalList map[int]*list       //通信
+	TunnelList map[int]*list       //隧道
+	RunList    map[int]interface{} //运行中的任务
 	lock       sync.Mutex
 	tunnelLock sync.Mutex
 }
 
-func NewTunnel(tunnelPort int, runList map[string]interface{}) *Tunnel {
+func NewTunnel(tunnelPort int, runList map[int]interface{}) *Tunnel {
 	t := new(Tunnel)
 	t.TunnelPort = tunnelPort
-	t.SignalList = make(map[string]*list)
-	t.TunnelList = make(map[string]*list)
+	t.SignalList = make(map[int]*list)
+	t.TunnelList = make(map[int]*list)
 	t.RunList = runList
 	return t
 }
@@ -87,7 +88,8 @@ func (s *Tunnel) cliProcess(c *utils.Conn) error {
 		c.Conn.Close()
 		return err
 	}
-	if !s.verify(string(vval)) {
+	id, err := utils.GetCsvDb().GetIdByVerifyKey(string(vval),c.Conn.RemoteAddr().String())
+	if err != nil {
 		log.Println("当前客户端连接校验错误,关闭此客户端:", c.Conn.RemoteAddr())
 		s.verifyError(c)
 		return errors.New("验证错误")
@@ -97,18 +99,18 @@ func (s *Tunnel) cliProcess(c *utils.Conn) error {
 	if flag, err := c.ReadFlag(); err != nil {
 		return err
 	} else {
-		return s.typeDeal(flag, c, string(vval))
+		return s.typeDeal(flag, c, id)
 	}
 }
 
 //tcp连接类型区分
-func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, cFlag string) error {
+func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, id int) error {
 	switch typeVal {
 	case utils.WORK_MAIN:
 		log.Println("客户端连接成功", c.Conn.RemoteAddr())
-		s.addList(s.SignalList, c, cFlag)
+		s.addList(s.SignalList, c, id)
 	case utils.WORK_CHAN:
-		s.addList(s.TunnelList, c, cFlag)
+		s.addList(s.TunnelList, c, id)
 	default:
 		return errors.New("无法识别")
 	}
@@ -117,41 +119,38 @@ func (s *Tunnel) typeDeal(typeVal string, c *utils.Conn, cFlag string) error {
 }
 
 //加到对应的list中
-func (s *Tunnel) addList(m map[string]*list, c *utils.Conn, cFlag string) {
+func (s *Tunnel) addList(m map[int]*list, c *utils.Conn, id int) {
 	s.lock.Lock()
-	if v, ok := m[cFlag]; ok {
+	if v, ok := m[id]; ok {
 		v.Add(c)
 	} else {
 		l := newList()
 		l.Add(c)
-		m[cFlag] = l
+		m[id] = l
 	}
 	s.lock.Unlock()
 }
 
 //新建隧道
-func (s *Tunnel) newChan(cFlag string) error {
+func (s *Tunnel) newChan(id int) error {
 	var connPass *utils.Conn
 	var err error
 retry:
-	if connPass, err = s.waitAndPop(s.SignalList, cFlag); err != nil {
+	if connPass, err = s.waitAndPop(s.SignalList, id); err != nil {
 		return err
 	}
 	if _, err = connPass.Conn.Write([]byte("chan")); err != nil {
 		goto retry
 	}
-	s.SignalList[cFlag].Add(connPass)
+	s.SignalList[id].Add(connPass)
 	return nil
 }
 
 //得到一个tcp隧道
 //TODO 超时问题 锁机制问题 对单个客户端加锁
-func (s *Tunnel) GetTunnel(cFlag string, en, de int, crypt, mux bool) (c *utils.Conn, err error) {
-	if v, ok := s.TunnelList[cFlag]; !ok || v.Len() < 3 { //新建通道
-		go s.newChan(cFlag)
-	}
+func (s *Tunnel) GetTunnel(id int, en, de int, crypt, mux bool) (c *utils.Conn, err error) {
 retry:
-	if c, err = s.waitAndPop(s.TunnelList, cFlag); err != nil {
+	if c, err = s.waitAndPop(s.TunnelList, id); err != nil {
 		return
 	}
 	if _, err = c.WriteTest(); err != nil {
@@ -163,61 +162,61 @@ retry:
 }
 
 //得到一个通信通道
-func (s *Tunnel) GetSignal(cFlag string) (err error, conn *utils.Conn) {
-	if v, ok := s.SignalList[cFlag]; !ok || v.Len() == 0 {
+func (s *Tunnel) GetSignal(id int) (err error, conn *utils.Conn) {
+	if v, ok := s.SignalList[id]; !ok || v.Len() == 0 {
 		err = errors.New("客户端未连接")
 		return
 	}
-	conn = s.SignalList[cFlag].Pop()
+	conn = s.SignalList[id].Pop()
 	return
 }
 
 //重回slice 复用
-func (s *Tunnel) ReturnSignal(conn *utils.Conn, cFlag string) {
-	if v, ok := s.SignalList[cFlag]; ok {
+func (s *Tunnel) ReturnSignal(conn *utils.Conn, id int) {
+	if v, ok := s.SignalList[id]; ok {
 		v.Add(conn)
 	}
 }
 
 //重回slice 复用
-func (s *Tunnel) ReturnTunnel(conn *utils.Conn, cFlag string) {
-	if v, ok := s.TunnelList[cFlag]; ok {
+func (s *Tunnel) ReturnTunnel(conn *utils.Conn, id int) {
+	if v, ok := s.TunnelList[id]; ok {
 		utils.FlushConn(conn.Conn)
 		v.Add(conn)
 	}
 }
 
 //删除通信通道
-func (s *Tunnel) DelClientSignal(cFlag string) {
-	s.delClient(cFlag, s.SignalList)
+func (s *Tunnel) DelClientSignal(id int) {
+	s.delClient(id, s.SignalList)
 }
 
 //删除隧道
-func (s *Tunnel) DelClientTunnel(cFlag string) {
-	s.delClient(cFlag, s.TunnelList)
+func (s *Tunnel) DelClientTunnel(id int) {
+	s.delClient(id, s.TunnelList)
 }
 
-func (s *Tunnel) delClient(cFlag string, l map[string]*list) {
-	if t := l[utils.Getverifyval(cFlag)]; t != nil {
+func (s *Tunnel) delClient(id int, l map[int]*list) {
+	if t := l[id]; t != nil {
 		for {
 			if t.Len() <= 0 {
 				break
 			}
 			t.Pop().Close()
 		}
-		delete(l, utils.Getverifyval(cFlag))
+		delete(l, id)
 	}
 }
 
 //等待
-func (s *Tunnel) waitAndPop(m map[string]*list, cFlag string) (c *utils.Conn, err error) {
+func (s *Tunnel) waitAndPop(m map[int]*list, id int) (c *utils.Conn, err error) {
 	ticker := time.NewTicker(time.Millisecond * 100)
-	stop := time.After(time.Second * 10)
+	stop := time.After(time.Second * 3)
 	for {
 		select {
 		case <-ticker.C:
 			s.lock.Lock()
-			if v, ok := m[cFlag]; ok && v.Len() > 0 {
+			if v, ok := m[id]; ok && v.Len() > 0 {
 				c = v.Pop()
 				ticker.Stop()
 				s.lock.Unlock()
@@ -225,16 +224,16 @@ func (s *Tunnel) waitAndPop(m map[string]*list, cFlag string) (c *utils.Conn, er
 			}
 			s.lock.Unlock()
 		case <-stop:
-			err = errors.New("client key: " + cFlag + ",err: get client conn timeout")
+			err = errors.New("client id: " + strconv.Itoa(id) + ",err: get client conn timeout")
 			return
 		}
 	}
 	return
 }
 
-func (s *Tunnel) verify(vKeyMd5 string) bool {
+func (s *Tunnel) verify(id int) bool {
 	for k := range s.RunList {
-		if utils.Getverifyval(k) == vKeyMd5 {
+		if k == id {
 			return true
 		}
 	}

+ 33 - 10
client/client.go

@@ -5,12 +5,16 @@ import (
 	"log"
 	"net"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
 type TRPClient struct {
-	svrAddr string
-	tcpNum  int
+	svrAddr      string
+	tcpNum       int
+	tunnelNum    int64
+	tunnel       chan bool
+	serverStatus bool
 	sync.Mutex
 	vKey string
 }
@@ -21,6 +25,7 @@ func NewRPClient(svraddr string, tcpNum int, vKey string) *TRPClient {
 	c.svrAddr = svraddr
 	c.tcpNum = tcpNum
 	c.vKey = vKey
+	c.tunnel = make(chan bool)
 	return c
 }
 
@@ -29,12 +34,17 @@ func (s *TRPClient) Start() error {
 	for i := 0; i < s.tcpNum; i++ {
 		go s.NewConn()
 	}
+	for i := 0; i < 5; i++ {
+		go s.dealChan()
+	}
+	go s.session()
 	return nil
 }
 
 //新建
 func (s *TRPClient) NewConn() error {
 	s.Lock()
+	s.serverStatus = false
 	conn, err := net.Dial("tcp", s.svrAddr)
 	if err != nil {
 		log.Println("连接服务端失败,五秒后将重连")
@@ -44,11 +54,12 @@ func (s *TRPClient) NewConn() error {
 		return err
 	}
 	s.Unlock()
-	return s.process(utils.NewConn(conn))
+	return s.processor(utils.NewConn(conn))
 }
 
 //处理
-func (s *TRPClient) process(c *utils.Conn) error {
+func (s *TRPClient) processor(c *utils.Conn) error {
+	s.serverStatus = true
 	c.SetAlive()
 	if _, err := c.Write([]byte(utils.Getverifyval(s.vKey))); err != nil {
 		return err
@@ -58,7 +69,6 @@ func (s *TRPClient) process(c *utils.Conn) error {
 		flags, err := c.ReadFlag()
 		if err != nil {
 			log.Println("服务端断开,五秒后将重连", err)
-			time.Sleep(5 * time.Second)
 			go s.NewConn()
 			break
 		}
@@ -66,9 +76,6 @@ func (s *TRPClient) process(c *utils.Conn) error {
 		case utils.VERIFY_EER:
 			log.Fatalln("vkey:", s.vKey, "不正确,服务端拒绝连接,请检查")
 		case utils.WORK_CHAN: //隧道模式,每次开启10个,加快连接速度
-			for i := 0; i < 5; i++ {
-				go s.dealChan()
-			}
 		case utils.RES_MSG:
 			log.Println("服务端返回错误。")
 		default:
@@ -98,13 +105,16 @@ func (s *TRPClient) dealChan() {
 	//写标志
 	c.WriteChan()
 re:
+	atomic.AddInt64(&s.tunnelNum, 1)
 	//获取连接的host type(tcp or udp)
 	typeStr, host, en, de, crypt, mux, err := c.GetHostFromConn()
+	s.tunnel <- true
+	atomic.AddInt64(&s.tunnelNum, -1)
 	if err != nil {
 		c.Close()
 		return
 	}
-	Process(c, typeStr, host, en, de, crypt, mux)
+	s.ConnectAndCopy(c, typeStr, host, en, de, crypt, mux)
 	if mux {
 		utils.FlushConn(conn)
 		goto re
@@ -113,7 +123,20 @@ re:
 	}
 }
 
-func Process(c *utils.Conn, typeStr, host string, en, de int, crypt, mux bool) {
+func (s *TRPClient) session() {
+	t := time.NewTicker(time.Millisecond * 1000)
+	for {
+		select {
+		case <-s.tunnel:
+		case <-t.C:
+		}
+		if s.serverStatus && s.tunnelNum < 5 {
+			go s.dealChan()
+		}
+	}
+}
+
+func (s *TRPClient) ConnectAndCopy(c *utils.Conn, typeStr, host string, en, de int, crypt, mux bool) {
 	//与目标建立连接,超时时间为3
 	server, err := net.DialTimeout(typeStr, host, time.Second*3)
 	if err != nil {

+ 2 - 2
cmd/proxy_server/proxy_server.go

@@ -25,7 +25,7 @@ var (
 func main() {
 	flag.Parse()
 	server.VerifyKey = *VerifyKey
-	cnf := server.ServerConfig{
+	cnf := &utils.ServerConfig{
 		TcpPort:        *httpPort,
 		Mode:           *rpMode,
 		Target:         *tunnelTarget,
@@ -51,5 +51,5 @@ func main() {
 	}
 	log.Println("服务端启动,监听tcp服务端端口:", *TcpPort)
 	cnf.CompressDecode, cnf.CompressEncode = utils.GetCompressType(cnf.Compress)
-	server.StartNewServer(*TcpPort, &cnf)
+	server.StartNewServer(*TcpPort, cnf)
 }

+ 2 - 0
conf/clients.csv

@@ -0,0 +1,2 @@
+2,zl4p3da659qa9rh3,127.0.0.1:58000,测试2,true,,,0,0,
+1,rfd0tl1anega0d0g,127.0.0.1:53603,测试,true,1,1,1,1,snappy

+ 2 - 2
conf/hosts.csv

@@ -1,2 +1,2 @@
-a.o.com,127.0.0.1:8082,7hiixust68kbz33a,,www.baidu.com
-b.o.com,,7hiixust68kbz33a,,ab
+a.o.com,127.0.0.1:8080,1,,,测试2
+b.o.com,127.0.0.1:8082,2,,,测试

+ 4 - 5
conf/tasks.csv

@@ -1,5 +1,4 @@
-8001,tunnelServer,127.0.0.1:88,jq5i7n0sjs1h0jje,aaa,bbb,,1,0,0,0,1
-0,hostServer,,7n7bxc2bm1fyjfab,ab,b,,1,1,1,0,1
-0,hostServer,,ts08z6vk5nc72fs8,aaa,bbb,snappy,1,0,1,2,3
-8002,tunnelServer,127.0.0.1:88,2nxo93wvotb9g75s,,,,1,0,0,0,1
-8025,socks5Server,,2p3qs71oym3zx52w,,,,1,0,0,0,1
+9001,tunnelServer,127.0.0.1:8080,,,,1,0,0,0,0,1,1,true,test
+53,udpServer,114.114.114.114:53,,,,1,0,0,0,0,2,2,true,udp测试
+8024,socks5Server,,,,,1,0,0,0,0,3,2,true,socks5测试
+8025,httpProxyServer,,,,,1,0,0,0,0,4,2,true,http测试

+ 76 - 0
server/base.go

@@ -0,0 +1,76 @@
+package server
+
+import (
+	"github.com/cnlh/easyProxy/bridge"
+	"github.com/cnlh/easyProxy/utils"
+	"sync"
+)
+
+//server base struct
+type server struct {
+	bridge *bridge.Tunnel
+	config *utils.ServerConfig
+	sync.Mutex
+}
+
+func (s *server) GetTunnelAndWriteHost(connType string, cnf *utils.ServerConfig, addr string) (*utils.Conn, error) {
+	var err error
+	link, err := s.bridge.GetTunnel(cnf.ClientId, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux)
+	if err != nil {
+		return nil, err
+	}
+	if _, err = link.WriteHost(connType, addr); err != nil {
+		link.Close()
+		return nil, err
+	}
+	return link, nil
+}
+
+func (s *server) FlowAdd(in, out int64) {
+	s.Lock()
+	defer s.Unlock()
+	if s.config.Flow == nil {
+		s.config.Flow = new(utils.Flow)
+	}
+	s.config.Flow.ExportFlow += out
+	s.config.Flow.InletFlow += in
+}
+
+func (s *server) FlowAddHost(host *utils.HostList, in, out int64) {
+	s.Lock()
+	defer s.Unlock()
+	if s.config.Flow == nil {
+		s.config.Flow = new(utils.Flow)
+	}
+	host.Flow.ExportFlow += out
+	host.Flow.InletFlow += in
+}
+
+//热更新配置
+func (s *server) ResetConfig() {
+	task, err := CsvDb.GetTask(s.config.Id)
+	if err != nil {
+		return
+	}
+	s.config.UseClientCnf = task.UseClientCnf
+	if s.config.UseClientCnf {
+		client, err := CsvDb.GetClient(s.config.ClientId)
+		if err == nil {
+			s.config.U = client.Cnf.U
+			s.config.P = client.Cnf.P
+			s.config.Compress = client.Cnf.Compress
+			s.config.Mux = client.Cnf.Mux
+			s.config.Crypt = client.Cnf.Crypt
+		}
+		s.config.CompressDecode, s.config.CompressEncode = utils.GetCompressType(client.Cnf.Compress)
+	} else {
+		if err == nil {
+			s.config.U = task.U
+			s.config.P = task.P
+			s.config.Compress = task.Compress
+			s.config.Mux = task.Mux
+			s.config.Crypt = task.Crypt
+		}
+		s.config.CompressDecode, s.config.CompressEncode = utils.GetCompressType(task.Compress)
+	}
+}

+ 0 - 271
server/file.go

@@ -1,271 +0,0 @@
-package server
-
-import (
-	"encoding/csv"
-	"errors"
-	"github.com/astaxie/beego"
-	"github.com/cnlh/easyProxy/utils"
-	"log"
-	"os"
-	"strconv"
-)
-
-type ServerConfig struct {
-	TcpPort        int    //服务端与客户端通信端口
-	Mode           string //启动方式
-	Target         string //目标
-	VerifyKey      string //flag
-	U              string //socks5验证用户名
-	P              string //socks5验证密码
-	Compress       string //压缩方式
-	Start          int    //是否开启
-	IsRun          int    //是否在运行
-	ClientStatus   int    //客s户端状态
-	Crypt          bool   //是否加密
-	Mux            bool   //是否加密
-	CompressEncode int    //加密方式
-	CompressDecode int    //解密方式
-}
-
-type HostList struct {
-	Vkey         string //服务端与客户端通信端口
-	Host         string //启动方式
-	Target       string //目标
-	HeaderChange string //host修改
-	HostChange   string //host修改
-}
-
-func NewCsv(runList map[string]interface{}) *Csv {
-	c := new(Csv)
-	c.RunList = runList
-	return c
-}
-
-type Csv struct {
-	Tasks   []*ServerConfig
-	Path    string
-	RunList map[string]interface{}
-	Hosts   []*HostList //域名列表
-}
-
-func (s *Csv) Init() {
-	s.LoadTaskFromCsv()
-	s.LoadHostFromCsv()
-}
-
-func (s *Csv) StoreTasksToCsv() {
-	// 创建文件
-	csvFile, err := os.Create(beego.AppPath + "/conf/tasks.csv")
-	if err != nil {
-		log.Fatalf(err.Error())
-	}
-	defer csvFile.Close()
-	writer := csv.NewWriter(csvFile)
-	for _, task := range s.Tasks {
-		record := []string{
-			strconv.Itoa(task.TcpPort),
-			task.Mode,
-			task.Target,
-			task.VerifyKey,
-			task.U,
-			task.P,
-			task.Compress,
-			strconv.Itoa(task.Start),
-			utils.GetStrByBool(task.Crypt),
-			utils.GetStrByBool(task.Mux),
-			strconv.Itoa(task.CompressEncode),
-			strconv.Itoa(task.CompressDecode),
-		}
-		err := writer.Write(record)
-		if err != nil {
-			log.Fatalf(err.Error())
-		}
-	}
-	writer.Flush()
-}
-
-func (s *Csv) openFile(path string) ([][]string, error) {
-	// 打开文件
-	file, err := os.Open(path)
-	if err != nil {
-		panic(err)
-	}
-	defer file.Close()
-
-	// 获取csv的reader
-	reader := csv.NewReader(file)
-
-	// 设置FieldsPerRecord为-1
-	reader.FieldsPerRecord = -1
-
-	// 读取文件中所有行保存到slice中
-	return reader.ReadAll()
-}
-
-func (s *Csv) LoadTaskFromCsv() {
-	path := beego.AppPath + "/conf/tasks.csv"
-	records, err := s.openFile(path)
-	if err != nil {
-		log.Fatal("配置文件打开错误:", path)
-	}
-	var tasks []*ServerConfig
-	// 将每一行数据保存到内存slice中
-	for _, item := range records {
-		post := &ServerConfig{
-			TcpPort:        utils.GetIntNoErrByStr(item[0]),
-			Mode:           item[1],
-			Target:         item[2],
-			VerifyKey:      item[3],
-			U:              item[4],
-			P:              item[5],
-			Compress:       item[6],
-			Start:          utils.GetIntNoErrByStr(item[7]),
-			Crypt:          utils.GetBoolByStr(item[8]),
-			Mux:            utils.GetBoolByStr(item[9]),
-			CompressEncode: utils.GetIntNoErrByStr(item[10]),
-			CompressDecode: utils.GetIntNoErrByStr(item[11]),
-		}
-		tasks = append(tasks, post)
-	}
-	s.Tasks = tasks
-}
-
-func (s *Csv) NewTask(t *ServerConfig) {
-	s.Tasks = append(s.Tasks, t)
-	s.StoreTasksToCsv()
-}
-
-func (s *Csv) UpdateTask(t *ServerConfig) error {
-	for k, v := range s.Tasks {
-		if v.VerifyKey == t.VerifyKey {
-			s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...)
-			s.Tasks = append(s.Tasks, t)
-			s.StoreTasksToCsv()
-			return nil
-		}
-	}
-	return errors.New("不存在")
-}
-
-func (s *Csv) UpdateHost(t *HostList) error {
-	for k, v := range s.Hosts {
-		if v.Host == t.Host {
-			s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...)
-			s.Hosts = append(s.Hosts, t)
-			s.StoreHostToCsv()
-			return nil
-		}
-	}
-	return errors.New("不存在")
-}
-
-func (s *Csv) AddRunList(vKey string, svr interface{}) {
-	s.RunList[vKey] = svr
-}
-
-func (s *Csv) DelRunList(vKey string) {
-	delete(s.RunList, vKey)
-}
-
-func (s *Csv) DelTask(vKey string) error {
-	for k, v := range s.Tasks {
-		if v.VerifyKey == vKey {
-			s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...)
-			s.StoreTasksToCsv()
-			return nil
-		}
-	}
-	return errors.New("不存在")
-}
-
-func (s *Csv) GetTask(vKey string) (v *ServerConfig, err error) {
-	for _, v = range s.Tasks {
-		if v.VerifyKey == vKey {
-			return
-		}
-	}
-	err = errors.New("未找到")
-	return
-}
-
-func (s *Csv) StoreHostToCsv() {
-	// 创建文件
-	csvFile, err := os.Create(beego.AppPath + "/conf/hosts.csv")
-	if err != nil {
-		panic(err)
-	}
-	defer csvFile.Close()
-	// 获取csv的Writer
-	writer := csv.NewWriter(csvFile)
-	// 将map中的Post转换成slice,因为csv的Write需要slice参数
-	// 并写入csv文件
-	for _, host := range s.Hosts {
-		record := []string{
-			host.Host,
-			host.Target,
-			host.Vkey,
-			host.HeaderChange,
-			host.HostChange,
-		}
-		err1 := writer.Write(record)
-		if err1 != nil {
-			panic(err1)
-		}
-	}
-	// 确保所有内存数据刷到csv文件
-	writer.Flush()
-}
-
-func (s *Csv) LoadHostFromCsv() {
-	path := beego.AppPath + "/conf/hosts.csv"
-	records, err := s.openFile(path)
-	if err != nil {
-		log.Fatal("配置文件打开错误:", path)
-	}
-	var hosts []*HostList
-	// 将每一行数据保存到内存slice中
-	for _, item := range records {
-		post := &HostList{
-			Vkey:         item[2],
-			Host:         item[0],
-			Target:       item[1],
-			HeaderChange: item[3],
-			HostChange:   item[4],
-		}
-		hosts = append(hosts, post)
-	}
-	s.Hosts = hosts
-}
-
-func (s *Csv) DelHost(host string) error {
-	for k, v := range s.Hosts {
-		if v.Host == host {
-			s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...)
-			s.StoreHostToCsv()
-			return nil
-		}
-	}
-	return errors.New("不存在")
-}
-
-func (s *Csv) NewHost(t *HostList) {
-	s.Hosts = append(s.Hosts, t)
-	s.StoreHostToCsv()
-
-}
-
-func (s *Csv) GetHostList(start, length int, vKey string) ([]*HostList, int) {
-	list := make([]*HostList, 0)
-	var cnt int
-	for _, v := range s.Hosts {
-		if v.Vkey == vKey {
-			cnt++
-			if start--; start < 0 {
-				if length--; length > 0 {
-					list = append(list, v)
-				}
-			}
-		}
-	}
-	return list, cnt
-}

+ 21 - 13
server/process.go

@@ -41,8 +41,8 @@ func ProcessHost(c *utils.Conn, s *TunnelModeServer) error {
 	var (
 		isConn = true
 		link   *utils.Conn
-		cnf    *ServerConfig
-		host   *HostList
+		client *utils.Client
+		host   *utils.HostList
 		wg     sync.WaitGroup
 	)
 	for {
@@ -52,48 +52,56 @@ func ProcessHost(c *utils.Conn, s *TunnelModeServer) error {
 		}
 		//首次获取conn
 		if isConn {
-			isConn = false
-			if host, cnf, err = GetKeyByHost(r.Host); err != nil {
+			if host, client, err = GetKeyByHost(r.Host); err != nil {
 				log.Printf("the host %s is not found !", r.Host)
 				break
 			}
 
-			if err = s.auth(r, c, cnf.U, cnf.P); err != nil {
+			client.Cnf.ClientId = host.ClientId
+			client.Cnf.CompressDecode, client.Cnf.CompressEncode = utils.GetCompressType(client.Cnf.Compress)
+			if err = s.auth(r, c, client.Cnf.U, client.Cnf.P); err != nil {
 				break
 			}
-
-			if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, cnf, host.Target); err != nil {
+			if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, client.Cnf, host.Target); err != nil {
 				log.Println("get bridge tunnel error: ", err)
 				break
 			}
-
 			if flag, err := link.ReadFlag(); err != nil || flag == utils.CONN_ERROR {
 				log.Printf("the host %s connection to %s error", r.Host, host.Target)
 				break
 			} else {
 				wg.Add(1)
 				go func() {
-					utils.Relay(c.Conn, link.Conn, cnf.CompressDecode, cnf.Crypt, cnf.Mux)
+					out, _ := utils.Relay(c.Conn, link.Conn, client.Cnf.CompressDecode, client.Cnf.Crypt, client.Cnf.Mux)
 					wg.Done()
+					s.FlowAddHost(host, 0, out)
 				}()
 			}
+			isConn = false
 		}
 		utils.ChangeHostAndHeader(r, host.HostChange, host.HeaderChange, c.Conn.RemoteAddr().String())
 		b, err := httputil.DumpRequest(r, true)
+		s.FlowAddHost(host, int64(len(b)), 0)
 		if err != nil {
 			break
 		}
-		if _, err := link.WriteTo(b, cnf.CompressEncode, cnf.Crypt); err != nil {
+		if _, err := link.WriteTo(b, client.Cnf.CompressEncode, client.Cnf.Crypt); err != nil {
 			break
 		}
 	}
 	wg.Wait()
-	if cnf != nil && cnf.Mux && link != nil {
-		link.WriteTo([]byte(utils.IO_EOF), cnf.CompressEncode, cnf.Crypt)
-		s.bridge.ReturnTunnel(link, getverifyval(cnf.VerifyKey))
+	if client != nil && client.Cnf != nil && client.Cnf.Mux && link != nil {
+		link.WriteTo([]byte(utils.IO_EOF), client.Cnf.CompressEncode, client.Cnf.Crypt)
+		s.bridge.ReturnTunnel(link, client.Id)
 	} else if link != nil {
 		link.Close()
 	}
+
+	if isConn {
+		s.writeConnFail(c.Conn)
+	}
 	c.Close()
 	return nil
 }
+
+

+ 128 - 57
server/server.go

@@ -11,35 +11,43 @@ import (
 	"sync"
 )
 
+type RunServer struct {
+	flag       int   //标志
+	ExportFlow int64 //出口流量
+	InletFlow  int64 //入口流量
+	service    interface{}
+	sync.Mutex
+}
+
 var (
 	Bridge    *bridge.Tunnel
-	RunList   map[string]interface{} //运行中的任务
-	CsvDb     *Csv
+	RunList   map[int]interface{} //运行中的任务
+	CsvDb     = utils.GetCsvDb()
 	VerifyKey string
 )
 
 func init() {
-	RunList = make(map[string]interface{})
+	RunList = make(map[int]interface{})
 }
 
 //从csv文件中恢复任务
 func InitFromCsv() {
 	for _, v := range CsvDb.Tasks {
 		if v.Start == 1 {
-			log.Println("启动模式:", v.Mode, "监听端口:", v.TcpPort, "客户端令牌:", v.VerifyKey)
+			log.Println("启动模式:", v.Mode, "监听端口:", v.TcpPort)
 			AddTask(v)
 		}
 	}
 }
 
 //start a new server
-func StartNewServer(bridgePort int, cnf *ServerConfig) {
+func StartNewServer(bridgePort int, cnf *utils.ServerConfig) {
 	Bridge = bridge.NewTunnel(bridgePort, RunList)
 	if err := Bridge.StartTunnel(); err != nil {
 		log.Fatalln("服务端开启失败", err)
 	}
 	if svr := NewMode(Bridge, cnf); svr != nil {
-		RunList[cnf.VerifyKey] = svr
+		RunList[cnf.Id] = svr
 		err := reflect.ValueOf(svr).MethodByName("Start").Call(nil)[0]
 		if err.Interface() != nil {
 			log.Println(err)
@@ -50,7 +58,8 @@ func StartNewServer(bridgePort int, cnf *ServerConfig) {
 }
 
 //new a server by mode name
-func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} {
+func NewMode(Bridge *bridge.Tunnel, c *utils.ServerConfig) interface{} {
+	config := utils.DeepCopyConfig(c)
 	switch config.Mode {
 	case "tunnelServer":
 		return NewTunnelModeServer(ProcessTunnel, Bridge, config)
@@ -61,14 +70,12 @@ func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} {
 	case "udpServer":
 		return NewUdpModeServer(Bridge, config)
 	case "webServer":
-		InitCsvDb()
 		InitFromCsv()
 		p, _ := beego.AppConfig.Int("hostPort")
-		t := &ServerConfig{
+		t := &utils.ServerConfig{
 			TcpPort:      p,
 			Mode:         "httpHostServer",
 			Target:       "",
-			VerifyKey:    "",
 			U:            "",
 			P:            "",
 			Compress:     "",
@@ -87,15 +94,10 @@ func NewMode(Bridge *bridge.Tunnel, config *ServerConfig) interface{} {
 }
 
 //stop server
-func StopServer(cFlag string) error {
-	if v, ok := RunList[cFlag]; ok {
+func StopServer(id int) error {
+	if v, ok := RunList[id]; ok {
 		reflect.ValueOf(v).MethodByName("Close").Call(nil)
-		delete(RunList, cFlag)
-		if VerifyKey == "" { //多客户端模式关闭相关隧道
-			Bridge.DelClientSignal(cFlag)
-			Bridge.DelClientTunnel(cFlag)
-		}
-		if t, err := CsvDb.GetTask(cFlag); err != nil {
+		if t, err := CsvDb.GetTask(id); err != nil {
 			return err
 		} else {
 			t.Start = 0
@@ -107,15 +109,14 @@ func StopServer(cFlag string) error {
 }
 
 //add task
-func AddTask(t *ServerConfig) error {
-	t.CompressDecode, t.CompressEncode = utils.GetCompressType(t.Compress)
+func AddTask(t *utils.ServerConfig) error {
 	if svr := NewMode(Bridge, t); svr != nil {
-		RunList[t.VerifyKey] = svr
+		RunList[t.Id] = svr
 		go func() {
 			err := reflect.ValueOf(svr).MethodByName("Start").Call(nil)[0]
 			if err.Interface() != nil {
-				log.Println("客户端", t.VerifyKey, "启动失败,错误:", err)
-				delete(RunList, t.VerifyKey)
+				log.Println("客户端", t.Id, "启动失败,错误:", err)
+				delete(RunList, t.Id)
 			}
 		}()
 	} else {
@@ -125,8 +126,8 @@ func AddTask(t *ServerConfig) error {
 }
 
 //start task
-func StartTask(vKey string) error {
-	if t, err := CsvDb.GetTask(vKey); err != nil {
+func StartTask(id int) error {
+	if t, err := CsvDb.GetTask(id); err != nil {
 		return err
 	} else {
 		AddTask(t)
@@ -137,35 +138,20 @@ func StartTask(vKey string) error {
 }
 
 //delete task
-func DelTask(vKey string) error {
-	if err := StopServer(vKey); err != nil {
+func DelTask(id int) error {
+	if err := StopServer(id); err != nil {
 		return err
 	}
-	for _, v := range CsvDb.Hosts {
-		if v.Vkey == vKey {
-			CsvDb.DelHost(v.Host)
-		}
-	}
-	return CsvDb.DelTask(vKey)
-}
-
-//init csv from file
-func InitCsvDb() *Csv {
-	var once sync.Once
-	once.Do(func() {
-		CsvDb = NewCsv(RunList)
-		CsvDb.Init()
-	})
-	return CsvDb
+	return CsvDb.DelTask(id)
 }
 
 //get key by host from x
-func GetKeyByHost(host string) (h *HostList, t *ServerConfig, err error) {
+func GetKeyByHost(host string) (h *utils.HostList, t *utils.Client, err error) {
 	for _, v := range CsvDb.Hosts {
 		s := strings.Split(host, ":")
 		if s[0] == v.Host {
 			h = v
-			t, err = CsvDb.GetTask(v.Vkey)
+			t, err = CsvDb.GetClient(v.ClientId)
 			return
 		}
 	}
@@ -174,22 +160,32 @@ func GetKeyByHost(host string) (h *HostList, t *ServerConfig, err error) {
 }
 
 //get task list by page num
-func GetServerConfig(start, length int, typeVal string) ([]*ServerConfig, int) {
-	list := make([]*ServerConfig, 0)
+func GetServerConfig(start, length int, typeVal string, clientId int) ([]*utils.ServerConfig, int) {
+	list := make([]*utils.ServerConfig, 0)
 	var cnt int
 	for _, v := range CsvDb.Tasks {
-		if v.Mode != typeVal {
+		if (typeVal != "" && v.Mode != typeVal) || (typeVal == "" && clientId != v.ClientId) {
 			continue
 		}
+		if v.UseClientCnf {
+			v = utils.DeepCopyConfig(v)
+			if c, err := CsvDb.GetClient(v.ClientId); err == nil {
+				v.Compress = c.Cnf.Compress
+				v.Mux = c.Cnf.Mux
+				v.Crypt = c.Cnf.Crypt
+				v.U = c.Cnf.U
+				v.P = c.Cnf.P
+			}
+		}
 		cnt++
 		if start--; start < 0 {
 			if length--; length > 0 {
-				if _, ok := RunList[v.VerifyKey]; ok {
+				if _, ok := RunList[v.Id]; ok {
 					v.IsRun = 1
 				} else {
 					v.IsRun = 0
 				}
-				if s, ok := Bridge.SignalList[getverifyval(v.VerifyKey)]; ok {
+				if s, ok := Bridge.SignalList[v.ClientId]; ok {
 					if s.Len() > 0 {
 						v.ClientStatus = 1
 					} else {
@@ -201,16 +197,91 @@ func GetServerConfig(start, length int, typeVal string) ([]*ServerConfig, int) {
 				list = append(list, v)
 			}
 		}
-
 	}
 	return list, cnt
 }
 
-//get verify value
-//when mode is webServer and vKey is not none
-func getverifyval(vkey string) string {
-	if VerifyKey != "" {
-		return utils.Md5(VerifyKey)
+//获取客户端列表
+func GetClientList(start, length int) (list []*utils.Client, cnt int) {
+	list, cnt = CsvDb.GetClientList(start, length)
+	dealClientData(list)
+	return
+}
+
+func dealClientData(list []*utils.Client) {
+	for _, v := range list {
+		if _, ok := Bridge.SignalList[v.Id]; ok {
+			v.IsConnect = true
+		} else {
+			v.IsConnect = false
+		}
+		v.Flow.InletFlow = 0
+		v.Flow.ExportFlow = 0
+		for _, h := range CsvDb.Hosts {
+			if h.ClientId == v.Id {
+				v.Flow.InletFlow += h.Flow.InletFlow
+				v.Flow.ExportFlow += h.Flow.ExportFlow
+			}
+		}
+		for _, t := range CsvDb.Tasks {
+			if t.ClientId == v.Id {
+				v.Flow.InletFlow += t.Flow.InletFlow
+				v.Flow.ExportFlow += t.Flow.ExportFlow
+			}
+		}
+	}
+	return
+}
+
+//根据客户端id删除其所属的所有隧道和域名
+func DelTunnelAndHostByClientId(clientId int) {
+	for _, v := range CsvDb.Tasks {
+		if v.ClientId == clientId {
+			DelTask(v.Id)
+		}
+	}
+	for _, v := range CsvDb.Hosts {
+		if v.ClientId == clientId {
+			CsvDb.DelHost(v.Host)
+		}
+	}
+}
+
+//关闭客户端连接
+func DelClientConnect(clientId int) {
+	Bridge.DelClientTunnel(clientId)
+	Bridge.DelClientSignal(clientId)
+}
+
+func GetDashboardData() map[string]int {
+	data := make(map[string]int)
+	data["hostCount"] = len(CsvDb.Hosts)
+	data["clientCount"] = len(CsvDb.Clients)
+	list := CsvDb.Clients
+	dealClientData(list)
+	c := 0
+	var in, out int64
+	for _, v := range list {
+		if v.IsConnect {
+			c += 1
+		}
+		in += v.Flow.InletFlow
+		out += v.Flow.ExportFlow
+	}
+	data["clientOnlineCount"] = c
+	data["inletFlowCount"] = int(in)
+	data["exportFlowCount"] = int(out)
+	for _, v := range CsvDb.Tasks {
+		switch v.Mode {
+		case "tunnelServer":
+			data["tunnelServerCount"] += 1
+		case "socks5Server":
+			data["socks5ServerCount"] += 1
+		case "httpProxyServer":
+			data["httpProxyServerCount"] += 1
+		case "udpServer":
+			data["udpServerCount"] += 1
+		}
 	}
-	return utils.Md5(vkey)
+	return data
 }

+ 8 - 5
server/socks5.go

@@ -160,13 +160,14 @@ func (s *Sock5ModeServer) handleConnect(c net.Conn) {
 	proxyConn, err := s.doConnect(c, connectMethod)
 	defer func() {
 		if s.config.Mux && proxyConn != nil {
-			s.bridge.ReturnTunnel(proxyConn, getverifyval(s.config.VerifyKey))
+			s.bridge.ReturnTunnel(proxyConn, s.config.ClientId)
 		}
 	}()
 	if err != nil {
 		c.Close()
 	} else {
-		utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
+		out, in := utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
+		s.FlowAdd(in, out)
 	}
 }
 
@@ -197,13 +198,14 @@ func (s *Sock5ModeServer) handleUDP(c net.Conn) {
 	proxyConn, err := s.doConnect(c, associateMethod)
 	defer func() {
 		if s.config.Mux && proxyConn != nil {
-			s.bridge.ReturnTunnel(proxyConn, getverifyval(s.config.VerifyKey))
+			s.bridge.ReturnTunnel(proxyConn, s.config.ClientId)
 		}
 	}()
 	if err != nil {
 		c.Close()
 	} else {
-		utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
+		out, in := utils.ReplayWaitGroup(proxyConn.Conn, c, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
+		s.FlowAdd(in, out)
 	}
 }
 
@@ -295,6 +297,7 @@ func (s *Sock5ModeServer) Start() error {
 			}
 			log.Fatal("accept error: ", err)
 		}
+		s.ResetConfig()
 		go s.handleConn(conn)
 	}
 	return nil
@@ -306,7 +309,7 @@ func (s *Sock5ModeServer) Close() error {
 }
 
 //new
-func NewSock5ModeServer(bridge *bridge.Tunnel, cnf *ServerConfig) *Sock5ModeServer {
+func NewSock5ModeServer(bridge *bridge.Tunnel, cnf *utils.ServerConfig) *Sock5ModeServer {
 	s := new(Sock5ModeServer)
 	s.bridge = bridge
 	s.config = cnf

+ 19 - 27
server/tcp.go

@@ -12,33 +12,15 @@ import (
 	"strings"
 )
 
-//server base struct
-type server struct {
-	bridge *bridge.Tunnel
-	config *ServerConfig
-}
-
-func (s *server) GetTunnelAndWriteHost(connType string, cnf *ServerConfig, addr string) (*utils.Conn, error) {
-	var err error
-	link, err := s.bridge.GetTunnel(getverifyval(cnf.VerifyKey), cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux)
-	if err != nil {
-		return nil, err
-	}
-	if _, err = link.WriteHost(connType, addr); err != nil {
-		link.Close()
-		return nil, err
-	}
-	return link, nil
-}
-
 type TunnelModeServer struct {
 	server
-	process  process
-	listener *net.TCPListener
+	errorContent []byte
+	process      process
+	listener     *net.TCPListener
 }
 
 //tcp|http|host
-func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *ServerConfig) *TunnelModeServer {
+func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *utils.ServerConfig) *TunnelModeServer {
 	s := new(TunnelModeServer)
 	s.bridge = bridge
 	s.process = process
@@ -49,6 +31,9 @@ func NewTunnelModeServer(process process, bridge *bridge.Tunnel, cnf *ServerConf
 //开始
 func (s *TunnelModeServer) Start() error {
 	var err error
+	if s.errorContent, err = utils.ReadAllFromFile(beego.AppPath + "/web/static/page/error.html"); err != nil {
+		s.errorContent = []byte("easyProxy 404")
+	}
 	s.listener, err = net.ListenTCP("tcp", &net.TCPAddr{net.ParseIP("0.0.0.0"), s.config.TcpPort, ""})
 	if err != nil {
 		return err
@@ -62,6 +47,7 @@ func (s *TunnelModeServer) Start() error {
 			log.Println(err)
 			continue
 		}
+		s.ResetConfig()
 		go s.process(utils.NewConn(conn), s)
 	}
 	return nil
@@ -70,20 +56,25 @@ func (s *TunnelModeServer) Start() error {
 //权限认证
 func (s *TunnelModeServer) auth(r *http.Request, c *utils.Conn, u, p string) error {
 	if u != "" && p != "" && !utils.CheckAuth(r, u, p) {
-		c.Write([]byte(utils.Unauthorized_BYTES))
+		c.Write([]byte(utils.UnauthorizedBytes))
 		c.Close()
 		return errors.New("401 Unauthorized")
 	}
 	return nil
 }
 
+func (s *TunnelModeServer) writeConnFail(c net.Conn) {
+	c.Write([]byte(utils.ConnectionFailBytes))
+	c.Write(s.errorContent)
+}
+
 //与客户端建立通道
-func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *ServerConfig, addr string, method string, rb []byte) error {
+func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *utils.ServerConfig, addr string, method string, rb []byte) error {
 	var link *utils.Conn
 	var err error
 	defer func() {
 		if cnf.Mux && link != nil {
-			s.bridge.ReturnTunnel(link, getverifyval(cnf.VerifyKey))
+			s.bridge.ReturnTunnel(link, cnf.ClientId)
 		}
 	}()
 	if link, err = s.GetTunnelAndWriteHost(utils.CONN_TCP, cnf, addr); err != nil {
@@ -97,7 +88,8 @@ func (s *TunnelModeServer) dealClient(c *utils.Conn, cnf *ServerConfig, addr str
 			} else if rb != nil {
 				link.WriteTo(rb, cnf.CompressEncode, cnf.Crypt)
 			}
-			utils.ReplayWaitGroup(link.Conn, c.Conn, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux)
+			out, in := utils.ReplayWaitGroup(link.Conn, c.Conn, cnf.CompressEncode, cnf.CompressDecode, cnf.Crypt, cnf.Mux)
+			s.FlowAdd(in, out)
 		}
 	}
 	return nil
@@ -139,7 +131,7 @@ func (s *HostServer) Start() error {
 	return nil
 }
 
-func NewHostServer(cnf *ServerConfig) *HostServer {
+func NewHostServer(cnf *utils.ServerConfig) *HostServer {
 	s := new(HostServer)
 	s.config = cnf
 	return s

+ 11 - 10
server/udp.go

@@ -10,13 +10,12 @@ import (
 )
 
 type UdpModeServer struct {
-	bridge   *bridge.Tunnel
+	server
 	listener *net.UDPConn
 	udpMap   map[string]*utils.Conn
-	config   *ServerConfig
 }
 
-func NewUdpModeServer(bridge *bridge.Tunnel, cnf *ServerConfig) *UdpModeServer {
+func NewUdpModeServer(bridge *bridge.Tunnel, cnf *utils.ServerConfig) *UdpModeServer {
 	s := new(UdpModeServer)
 	s.bridge = bridge
 	s.udpMap = make(map[string]*utils.Conn)
@@ -40,6 +39,7 @@ func (s *UdpModeServer) Start() error {
 			}
 			continue
 		}
+		s.ResetConfig()
 		go s.process(addr, data[:n])
 	}
 	return nil
@@ -47,7 +47,7 @@ func (s *UdpModeServer) Start() error {
 
 //TODO:效率问题有待解决
 func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) {
-	conn, err := s.bridge.GetTunnel(getverifyval(s.config.VerifyKey), s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
+	conn, err := s.bridge.GetTunnel(s.config.ClientId, s.config.CompressEncode, s.config.CompressDecode, s.config.Crypt, s.config.Mux)
 	if err != nil {
 		log.Println(err)
 		return
@@ -60,21 +60,22 @@ func (s *UdpModeServer) process(addr *net.UDPAddr, data []byte) {
 		defer func() {
 			if conn != nil && s.config.Mux {
 				conn.WriteTo([]byte(utils.IO_EOF), s.config.CompressEncode, s.config.Crypt)
-				s.bridge.ReturnTunnel(conn, getverifyval(s.config.VerifyKey))
+				s.bridge.ReturnTunnel(conn, s.config.ClientId)
 			} else {
 				conn.Close()
 			}
 		}()
 		if flag == utils.CONN_SUCCESS {
-			conn.WriteTo(data, s.config.CompressEncode, s.config.Crypt)
-			buf := make([]byte, 1024)
-			//conn.conn.SetReadDeadline(time.Now().Add(time.Duration(time.Second * 3)))
-			n, err := conn.ReadFrom(buf, s.config.CompressDecode, s.config.Crypt)
+			in, _ := conn.WriteTo(data, s.config.CompressEncode, s.config.Crypt)
+			buf := utils.BufPoolUdp.Get().([]byte)
+			out, err := conn.ReadFrom(buf, s.config.CompressDecode, s.config.Crypt)
 			if err != nil || err == io.EOF {
 				log.Println("revieve error:", err)
 				return
 			}
-			s.listener.WriteToUDP(buf[:n], addr)
+			s.listener.WriteToUDP(buf[:out], addr)
+			s.FlowAdd(int64(in), int64(out))
+			utils.BufPoolUdp.Put(buf)
 		}
 	}
 }

+ 4 - 4
utils/conn.go

@@ -107,14 +107,14 @@ func (s *SnappyConn) Write(b []byte) (n int, err error) {
 
 //snappy压缩读 包含解密
 func (s *SnappyConn) Read(b []byte) (n int, err error) {
+	buf := bufPool.Get().([]byte)
 	defer func() {
 		if err == nil && n == len(IO_EOF) && string(b[:n]) == IO_EOF {
 			err = io.EOF
 			n = 0
 		}
+		bufPool.Put(buf)
 	}()
-	buf := bufPool.Get().([]byte)
-	defer bufPool.Put(buf)
 	if n, err = s.r.Read(buf); err != nil {
 		return
 	}
@@ -153,8 +153,8 @@ func (s *Conn) ReadLen(cLen int) ([]byte, error) {
 		buf = bufPoolSmall.Get().([]byte)[:cLen]
 		defer bufPoolSmall.Put(buf)
 	} else {
-		buf = bufPool.Get().([]byte)[:cLen]
-		defer bufPool.Put(buf)
+		buf = bufPoolMax.Get().([]byte)[:cLen]
+		defer bufPoolMax.Put(buf)
 	}
 	if n, err := io.ReadFull(s, buf); err != nil || n != cLen {
 		return buf, errors.New("读取指定长度错误" + err.Error())

+ 487 - 0
utils/file.go

@@ -0,0 +1,487 @@
+package utils
+
+import (
+	"easyProxy/utils"
+	"encoding/csv"
+	"errors"
+	"github.com/astaxie/beego"
+	"log"
+	"os"
+	"strconv"
+	"sync"
+)
+
+var (
+	CsvDb *Csv
+	once  sync.Once
+)
+
+type Flow struct {
+	ExportFlow int64 //出口流量
+	InletFlow  int64 //入口流量
+}
+
+type Client struct {
+	Cnf       *ServerConfig
+	Id        int    //id
+	VerifyKey string //验证密钥
+	Addr      string //客户端ip地址
+	Remark    string //备注
+	Status    bool   //是否开启
+	IsConnect bool   //是否连接
+	Flow      *Flow
+}
+
+type ServerConfig struct {
+	TcpPort        int //服务端与客户端通信端口
+	VerifyKey      string
+	Mode           string //启动方式
+	Target         string //目标
+	U              string //socks5验证用户名
+	P              string //socks5验证密码
+	Compress       string //压缩方式
+	Start          int    //是否开启
+	IsRun          int    //是否在运行
+	ClientStatus   int    //客s户端状态
+	Crypt          bool   //是否加密
+	Mux            bool   //是否加密
+	CompressEncode int    //加密方式
+	CompressDecode int    //解密方式
+	Id             int    //Id
+	ClientId       int    //所属客户端id
+	UseClientCnf   bool   //是否继承客户端配置
+	Flow           *Flow
+	Remark         string //备注
+}
+
+type HostList struct {
+	ClientId     int    //服务端与客户端通信端口
+	Host         string //启动方式
+	Target       string //目标
+	HeaderChange string //host修改
+	HostChange   string //host修改
+	Flow         *Flow
+	Remark       string //备注
+}
+
+func NewCsv() *Csv {
+	c := new(Csv)
+	return c
+}
+
+type Csv struct {
+	Tasks            []*ServerConfig
+	Path             string
+	Hosts            []*HostList //域名列表
+	Clients          []*Client   //客户端
+	ClientIncreaseId int         //客户端id
+	TaskIncreaseId   int         //任务自增ID
+	sync.Mutex
+}
+
+func (s *Csv) Init() {
+	s.LoadTaskFromCsv()
+	s.LoadHostFromCsv()
+	s.LoadClientFromCsv()
+}
+
+func (s *Csv) StoreTasksToCsv() {
+	// 创建文件
+	csvFile, err := os.Create(beego.AppPath + "/conf/tasks.csv")
+	if err != nil {
+		log.Fatalf(err.Error())
+	}
+	defer csvFile.Close()
+	writer := csv.NewWriter(csvFile)
+	for _, task := range s.Tasks {
+		record := []string{
+			strconv.Itoa(task.TcpPort),
+			task.Mode,
+			task.Target,
+			task.U,
+			task.P,
+			task.Compress,
+			strconv.Itoa(task.Start),
+			GetStrByBool(task.Crypt),
+			GetStrByBool(task.Mux),
+			strconv.Itoa(task.CompressEncode),
+			strconv.Itoa(task.CompressDecode),
+			strconv.Itoa(task.Id),
+			strconv.Itoa(task.ClientId),
+			strconv.FormatBool(task.UseClientCnf),
+			task.Remark,
+		}
+		err := writer.Write(record)
+		if err != nil {
+			log.Fatalf(err.Error())
+		}
+	}
+	writer.Flush()
+}
+
+func (s *Csv) openFile(path string) ([][]string, error) {
+	// 打开文件
+	file, err := os.Open(path)
+	if err != nil {
+		panic(err)
+	}
+	defer file.Close()
+
+	// 获取csv的reader
+	reader := csv.NewReader(file)
+
+	// 设置FieldsPerRecord为-1
+	reader.FieldsPerRecord = -1
+
+	// 读取文件中所有行保存到slice中
+	return reader.ReadAll()
+}
+
+func (s *Csv) LoadTaskFromCsv() {
+	path := beego.AppPath + "/conf/tasks.csv"
+	records, err := s.openFile(path)
+	if err != nil {
+		log.Fatal("配置文件打开错误:", path)
+	}
+	var tasks []*ServerConfig
+	// 将每一行数据保存到内存slice中
+	for _, item := range records {
+		post := &ServerConfig{
+			TcpPort:        GetIntNoErrByStr(item[0]),
+			Mode:           item[1],
+			Target:         item[2],
+			U:              item[3],
+			P:              item[4],
+			Compress:       item[5],
+			Start:          GetIntNoErrByStr(item[6]),
+			Crypt:          GetBoolByStr(item[7]),
+			Mux:            GetBoolByStr(item[8]),
+			CompressEncode: GetIntNoErrByStr(item[9]),
+			CompressDecode: GetIntNoErrByStr(item[10]),
+			Id:             GetIntNoErrByStr(item[11]),
+			ClientId:       GetIntNoErrByStr(item[12]),
+			UseClientCnf:   GetBoolByStr(item[13]),
+			Remark:         item[14],
+		}
+		post.Flow = new(Flow)
+		tasks = append(tasks, post)
+		if post.Id > s.TaskIncreaseId {
+			s.TaskIncreaseId = post.Id
+		}
+	}
+	s.Tasks = tasks
+}
+
+func (s *Csv) GetTaskId() int {
+	s.Lock()
+	defer s.Unlock()
+	s.TaskIncreaseId++
+	return s.TaskIncreaseId
+}
+
+func (s *Csv) GetIdByVerifyKey(vKey string, addr string) (int, error) {
+	s.Lock()
+	defer s.Unlock()
+	for _, v := range s.Clients {
+		if utils.Getverifyval(v.VerifyKey) == vKey && v.Status {
+			v.Addr = addr
+			return v.Id, nil
+		}
+	}
+	return 0, errors.New("not found")
+}
+
+func (s *Csv) NewTask(t *ServerConfig) {
+	t.Flow = new(Flow)
+	s.Tasks = append(s.Tasks, t)
+	s.StoreTasksToCsv()
+}
+
+func (s *Csv) UpdateTask(t *ServerConfig) error {
+	for k, v := range s.Tasks {
+		if v.Id == t.Id {
+			s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...)
+			s.Tasks = append(s.Tasks, t)
+			s.StoreTasksToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) DelTask(id int) error {
+	for k, v := range s.Tasks {
+		if v.Id == id {
+			s.Tasks = append(s.Tasks[:k], s.Tasks[k+1:]...)
+			s.StoreTasksToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) GetTask(id int) (v *ServerConfig, err error) {
+	for _, v = range s.Tasks {
+		if v.Id == id {
+			return
+		}
+	}
+	err = errors.New("未找到")
+	return
+}
+
+func (s *Csv) StoreHostToCsv() {
+	// 创建文件
+	csvFile, err := os.Create(beego.AppPath + "/conf/hosts.csv")
+	if err != nil {
+		panic(err)
+	}
+	defer csvFile.Close()
+	// 获取csv的Writer
+	writer := csv.NewWriter(csvFile)
+	// 将map中的Post转换成slice,因为csv的Write需要slice参数
+	// 并写入csv文件
+	for _, host := range s.Hosts {
+		record := []string{
+			host.Host,
+			host.Target,
+			strconv.Itoa(host.ClientId),
+			host.HeaderChange,
+			host.HostChange,
+			host.Remark,
+		}
+		err1 := writer.Write(record)
+		if err1 != nil {
+			panic(err1)
+		}
+	}
+	// 确保所有内存数据刷到csv文件
+	writer.Flush()
+}
+
+func (s *Csv) LoadClientFromCsv() {
+	path := beego.AppPath + "/conf/clients.csv"
+	records, err := s.openFile(path)
+	if err != nil {
+		log.Fatal("配置文件打开错误:", path)
+	}
+	var clients []*Client
+	// 将每一行数据保存到内存slice中
+	for _, item := range records {
+		post := &Client{
+			Id:        GetIntNoErrByStr(item[0]),
+			VerifyKey: item[1],
+			Addr:      item[2],
+			Remark:    item[3],
+			Status:    GetBoolByStr(item[4]),
+			Cnf: &ServerConfig{
+				U:        item[5],
+				P:        item[6],
+				Crypt:    GetBoolByStr(item[7]),
+				Mux:      GetBoolByStr(item[8]),
+				Compress: item[9],
+			},
+		}
+		if post.Id > s.ClientIncreaseId {
+			s.ClientIncreaseId = post.Id
+		}
+		post.Flow = new(Flow)
+		clients = append(clients, post)
+	}
+	s.Clients = clients
+}
+
+func (s *Csv) LoadHostFromCsv() {
+	path := beego.AppPath + "/conf/hosts.csv"
+	records, err := s.openFile(path)
+	if err != nil {
+		log.Fatal("配置文件打开错误:", path)
+	}
+	var hosts []*HostList
+	// 将每一行数据保存到内存slice中
+	for _, item := range records {
+		post := &HostList{
+			ClientId:     GetIntNoErrByStr(item[2]),
+			Host:         item[0],
+			Target:       item[1],
+			HeaderChange: item[3],
+			HostChange:   item[4],
+			Remark:       item[5],
+		}
+		post.Flow = new(Flow)
+		hosts = append(hosts, post)
+	}
+	s.Hosts = hosts
+}
+
+func (s *Csv) DelHost(host string) error {
+	for k, v := range s.Hosts {
+		if v.Host == host {
+			s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...)
+			s.StoreHostToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) NewHost(t *HostList) {
+	t.Flow = new(Flow)
+	s.Hosts = append(s.Hosts, t)
+	s.StoreHostToCsv()
+
+}
+
+func (s *Csv) UpdateHost(t *HostList) error {
+	for k, v := range s.Hosts {
+		if v.Host == t.Host {
+			s.Hosts = append(s.Hosts[:k], s.Hosts[k+1:]...)
+			s.Hosts = append(s.Hosts, t)
+			s.StoreHostToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) GetHostList(start, length int, id int) ([]*HostList, int) {
+	list := make([]*HostList, 0)
+	var cnt int
+	for _, v := range s.Hosts {
+		if id == 0 || v.ClientId == id {
+			cnt++
+			if start--; start < 0 {
+				if length--; length > 0 {
+					list = append(list, v)
+				}
+			}
+		}
+	}
+	return list, cnt
+}
+
+func (s *Csv) DelClient(id int) error {
+	for k, v := range s.Clients {
+		if v.Id == id {
+			s.Clients = append(s.Clients[:k], s.Clients[k+1:]...)
+			s.StoreClientsToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) NewClient(c *Client) {
+	s.Lock()
+	defer s.Unlock()
+	c.Flow = new(Flow)
+	s.Clients = append(s.Clients, c)
+	s.StoreClientsToCsv()
+}
+
+func (s *Csv) GetClientId() int {
+	s.Lock()
+	defer s.Unlock()
+	s.ClientIncreaseId++
+	return s.ClientIncreaseId
+}
+
+func (s *Csv) UpdateClient(t *Client) error {
+	s.Lock()
+	defer s.Unlock()
+	for k, v := range s.Clients {
+		if v.Id == t.Id {
+			s.Clients = append(s.Clients[:k], s.Clients[k+1:]...)
+			s.Clients = append(s.Clients, t)
+			s.StoreClientsToCsv()
+			return nil
+		}
+	}
+	return errors.New("不存在")
+}
+
+func (s *Csv) GetClientList(start, length int) ([]*Client, int) {
+	list := make([]*Client, 0)
+	var cnt int
+	for _, v := range s.Clients {
+		cnt++
+		if start--; start < 0 {
+			if length--; length > 0 {
+				list = append(list, v)
+			}
+		}
+	}
+	return list, cnt
+}
+
+func (s *Csv) GetClient(id int) (v *Client, err error) {
+	for _, v = range s.Clients {
+		if v.Id == id {
+			return
+		}
+	}
+	err = errors.New("未找到")
+	return
+}
+func (s *Csv) StoreClientsToCsv() {
+	// 创建文件
+	csvFile, err := os.Create(beego.AppPath + "/conf/clients.csv")
+	if err != nil {
+		log.Fatalf(err.Error())
+	}
+	defer csvFile.Close()
+	writer := csv.NewWriter(csvFile)
+	for _, client := range s.Clients {
+		record := []string{
+			strconv.Itoa(client.Id),
+			client.VerifyKey,
+			client.Addr,
+			client.Remark,
+			strconv.FormatBool(client.Status),
+			client.Cnf.U,
+			client.Cnf.P,
+			utils.GetStrByBool(client.Cnf.Crypt),
+			utils.GetStrByBool(client.Cnf.Mux),
+			client.Cnf.Compress,
+		}
+		err := writer.Write(record)
+		if err != nil {
+			log.Fatalf(err.Error())
+		}
+	}
+	writer.Flush()
+}
+
+//init csv from file
+func GetCsvDb() *Csv {
+	once.Do(func() {
+		CsvDb = NewCsv()
+		CsvDb.Init()
+	})
+	return CsvDb
+}
+
+//深拷贝serverConfig
+func DeepCopyConfig(c *ServerConfig) *ServerConfig {
+	return &ServerConfig{
+		TcpPort:        c.TcpPort,
+		VerifyKey:      c.VerifyKey,
+		Mode:           c.Mode,
+		Target:         c.Target,
+		U:              c.U,
+		P:              c.P,
+		Compress:       c.Compress,
+		Start:          c.Start,
+		IsRun:          c.IsRun,
+		ClientStatus:   c.ClientStatus,
+		Crypt:          c.Crypt,
+		Mux:            c.Mux,
+		CompressEncode: c.CompressEncode,
+		CompressDecode: c.CompressDecode,
+		Id:             c.Id,
+		ClientId:       c.ClientId,
+		UseClientCnf:   c.UseClientCnf,
+		Flow:           c.Flow,
+		Remark:         c.Remark,
+	}
+}

+ 12 - 1
utils/pool.go

@@ -3,7 +3,8 @@ package utils
 import "sync"
 
 const poolSize = 64 * 1024
-const poolSizeSmall = 10
+const poolSizeSmall = 100
+const poolSizeUdp = 1472
 const poolSizeCopy = 32 * 1024
 
 var bufPool = sync.Pool{
@@ -11,6 +12,16 @@ var bufPool = sync.Pool{
 		return make([]byte, poolSize)
 	},
 }
+var BufPoolUdp = sync.Pool{
+	New: func() interface{} {
+		return make([]byte, poolSizeUdp)
+	},
+}
+var bufPoolMax = sync.Pool{
+	New: func() interface{} {
+		return make([]byte, poolSize)
+	},
+}
 var bufPoolSmall = sync.Pool{
 	New: func() interface{} {
 		return make([]byte, poolSizeSmall)

+ 38 - 22
utils/util.go

@@ -3,9 +3,11 @@ package utils
 import (
 	"encoding/base64"
 	"io"
+	"io/ioutil"
 	"log"
 	"net"
 	"net/http"
+	"os"
 	"regexp"
 	"strconv"
 	"strings"
@@ -18,48 +20,52 @@ const (
 	COMPRESS_NONE_DECODE
 	COMPRESS_SNAPY_ENCODE
 	COMPRESS_SNAPY_DECODE
-	VERIFY_EER         = "vkey"
-	WORK_MAIN          = "main"
-	WORK_CHAN          = "chan"
-	RES_SIGN           = "sign"
-	RES_MSG            = "msg0"
-	CONN_SUCCESS       = "sucs"
-	CONN_ERROR         = "fail"
-	TEST_FLAG          = "tst"
-	CONN_TCP           = "tcp"
-	CONN_UDP           = "udp"
-	Unauthorized_BYTES = `HTTP/1.1 401 Unauthorized
+	VERIFY_EER        = "vkey"
+	WORK_MAIN         = "main"
+	WORK_CHAN         = "chan"
+	RES_SIGN          = "sign"
+	RES_MSG           = "msg0"
+	CONN_SUCCESS      = "sucs"
+	CONN_ERROR        = "fail"
+	TEST_FLAG         = "tst"
+	CONN_TCP          = "tcp"
+	CONN_UDP          = "udp"
+	UnauthorizedBytes = `HTTP/1.1 401 Unauthorized
 Content-Type: text/plain; charset=utf-8
 WWW-Authenticate: Basic realm="easyProxy"
 
 401 Unauthorized`
-	IO_EOF = "PROXYEOF"
+	IO_EOF              = "PROXYEOF"
+	ConnectionFailBytes = `HTTP/1.1 404 Not Found
+
+`
 )
 
 //copy
-func Relay(in, out net.Conn, compressType int, crypt, mux bool) {
+func Relay(in, out net.Conn, compressType int, crypt, mux bool) (n int64, err error) {
 	switch compressType {
 	case COMPRESS_SNAPY_ENCODE:
-		copyBuffer(NewSnappyConn(in, crypt), out)
+		n, err = copyBuffer(NewSnappyConn(in, crypt), out)
 		out.Close()
 		NewSnappyConn(in, crypt).Write([]byte(IO_EOF))
 	case COMPRESS_SNAPY_DECODE:
-		copyBuffer(in, NewSnappyConn(out, crypt))
+		n, err = copyBuffer(in, NewSnappyConn(out, crypt))
 		in.Close()
 		if !mux {
 			out.Close()
 		}
 	case COMPRESS_NONE_ENCODE:
-		copyBuffer(NewCryptConn(in, crypt), out)
+		n, err = copyBuffer(NewCryptConn(in, crypt), out)
 		out.Close()
 		NewCryptConn(in, crypt).Write([]byte(IO_EOF))
 	case COMPRESS_NONE_DECODE:
-		copyBuffer(in, NewCryptConn(out, crypt))
+		n, err = copyBuffer(in, NewCryptConn(out, crypt))
 		in.Close()
 		if !mux {
 			out.Close()
 		}
 	}
+	return
 }
 
 //判断压缩方式
@@ -145,7 +151,6 @@ func GetIntNoErrByStr(str string) int {
 	return i
 }
 
-
 // io.copy的优化版,读取buffer长度原为32*1024,与snappy不同,导致读取出的内容存在差异,不利于解密
 //内存优化 用到pool,快速回收
 func copyBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
@@ -167,7 +172,7 @@ func copyBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
 				err = io.ErrShortWrite
 				break
 			}
-		}else {
+		} else {
 			bufPoolCopy.Put(buf)
 		}
 		if er != nil {
@@ -199,15 +204,17 @@ func Getverifyval(vkey string) string {
 }
 
 //wait replay group
-func ReplayWaitGroup(conn1 net.Conn, conn2 net.Conn, compressEncode, compressDecode int, crypt, mux bool) {
+//conn1 网桥 conn2
+func ReplayWaitGroup(conn1 net.Conn, conn2 net.Conn, compressEncode, compressDecode int, crypt, mux bool) (out int64, in int64) {
 	var wg sync.WaitGroup
 	wg.Add(1)
 	go func() {
-		Relay(conn1, conn2, compressEncode, crypt, mux)
+		in, _ = Relay(conn1, conn2, compressEncode, crypt, mux)
 		wg.Done()
 	}()
-	Relay(conn2, conn1, compressDecode, crypt, mux)
+	out, _ = Relay(conn2, conn1, compressDecode, crypt, mux)
 	wg.Wait()
+	return
 }
 
 func ChangeHostAndHeader(r *http.Request, host string, header string, addr string) {
@@ -227,3 +234,12 @@ func ChangeHostAndHeader(r *http.Request, host string, header string, addr strin
 	r.Header.Set("X-Forwarded-For", addr)
 	r.Header.Set("X-Real-IP", addr)
 }
+
+func ReadAllFromFile(filePth string) ([]byte, error) {
+	f, err := os.Open(filePth)
+	if err != nil {
+		return nil, err
+	}
+
+	return ioutil.ReadAll(f)
+}

+ 6 - 2
web/controllers/base.go

@@ -27,14 +27,18 @@ func (s *BaseController) Prepare() {
 //加载模板
 func (s *BaseController) display(tpl ...string) {
 	var tplname string
+	if s.Data["menu"] == nil {
+		s.Data["menu"] = s.actionName
+	}
 	if len(tpl) > 0 {
 		tplname = strings.Join([]string{tpl[0], "html"}, ".")
 	} else {
 		tplname = s.controllerName + "/" + s.actionName + ".html"
 	}
-	s.Data["menu"] = s.actionName
 	ip := s.Ctx.Request.Host
-	s.Data["ip"] = utils.GetHostByName(ip[0:strings.LastIndex(ip, ":")])
+	if strings.LastIndex(ip, ":") > 0 {
+		s.Data["ip"] = utils.GetHostByName(ip[0:])
+	}
 	s.Data["p"] = server.Bridge.TunnelPort
 	s.Data["proxyPort"] = beego.AppConfig.String("hostPort")
 	s.Layout = "public/layout.html"

+ 100 - 0
web/controllers/client.go

@@ -0,0 +1,100 @@
+package controllers
+
+import (
+	"github.com/cnlh/easyProxy/server"
+	"github.com/cnlh/easyProxy/utils"
+)
+
+type ClientController struct {
+	BaseController
+}
+
+func (s *ClientController) Client() {
+	if s.Ctx.Request.Method == "GET" {
+		s.Data["menu"] = "client"
+		s.SetInfo("客户端管理")
+		s.display("client/list")
+		return
+	}
+	start, length := s.GetAjaxParams()
+	list, cnt := server.GetClientList(start, length)
+	s.AjaxTable(list, cnt, cnt)
+}
+
+//添加客户端
+func (s *ClientController) Add() {
+	if s.Ctx.Request.Method == "GET" {
+		s.Data["menu"] = "client"
+		s.SetInfo("新增")
+		s.display()
+	} else {
+		t := &utils.Client{
+			VerifyKey: utils.GetRandomString(16),
+			Id:        server.CsvDb.GetClientId(),
+			Status:    true,
+			Remark:    s.GetString("Remark"),
+			Cnf: &utils.ServerConfig{
+				U:        s.GetString("u"),
+				P:        s.GetString("p"),
+				Compress: s.GetString("compress"),
+				Crypt:    s.GetBoolNoErr("crypt"),
+				Mux:      s.GetBoolNoErr("mux"),
+			},
+		}
+		server.CsvDb.NewClient(t)
+		s.AjaxOk("添加成功")
+	}
+}
+
+//修改客户端
+func (s *ClientController) Edit() {
+	if s.Ctx.Request.Method == "GET" {
+		s.Data["menu"] = "client"
+		id := s.GetIntNoErr("id")
+		if c, err := server.CsvDb.GetClient(id); err != nil {
+			s.error()
+		} else {
+			s.Data["c"] = c
+		}
+		s.SetInfo("修改")
+		s.display()
+	} else {
+		id := s.GetIntNoErr("Id")
+		if c, err := server.CsvDb.GetClient(id); err != nil {
+			s.error()
+		} else {
+			c.Remark = s.GetString("Remark")
+			c.Cnf.U = s.GetString("u")
+			c.Cnf.P = s.GetString("p")
+			c.Cnf.Compress = s.GetString("compress")
+			c.Cnf.Crypt = s.GetBoolNoErr("crypt")
+			c.Cnf.Mux = s.GetBoolNoErr("mux")
+			server.CsvDb.UpdateClient(c)
+		}
+		s.AjaxOk("修改成功")
+	}
+}
+
+//更改状态
+func (s *ClientController) ChangeStatus() {
+	id := s.GetIntNoErr("id")
+	if client, err := server.CsvDb.GetClient(id); err == nil {
+		client.Status = s.GetBoolNoErr("status")
+		if client.Status == false {
+			server.DelClientConnect(client.Id)
+		}
+		s.AjaxOk("修改成功")
+	}
+	s.AjaxErr("修改失败")
+}
+
+//删除客户端
+func (s *ClientController) Del() {
+	id := s.GetIntNoErr("id")
+	if err := server.CsvDb.DelClient(id); err != nil {
+		s.AjaxErr("删除失败")
+	}
+	server.DelTunnelAndHostByClientId(id)
+	server.DelClientConnect(id)
+	s.AjaxOk("删除成功")
+}

+ 62 - 38
web/controllers/index.go

@@ -10,9 +10,14 @@ type IndexController struct {
 }
 
 func (s *IndexController) Index() {
-	s.SetInfo("使用说明")
+	s.Data["data"] = server.GetDashboardData()
+	s.SetInfo("dashboard")
 	s.display("index/index")
 }
+func (s *IndexController) Help() {
+	s.SetInfo("使用说明")
+	s.display("index/help")
+}
 
 func (s *IndexController) Tcp() {
 	s.SetInfo("tcp隧道管理")
@@ -44,30 +49,44 @@ func (s *IndexController) Host() {
 	s.display("index/list")
 }
 
+func (s *IndexController) All() {
+	s.Data["menu"] = "client"
+	clientId := s.GetString("client_id")
+	s.Data["client_id"] = clientId
+	s.SetInfo("客户端" + clientId + "的所有隧道")
+	s.display("index/list")
+}
+
 func (s *IndexController) GetServerConfig() {
 	start, length := s.GetAjaxParams()
 	taskType := s.GetString("type")
-	list, cnt := server.GetServerConfig(start, length, taskType)
+	clientId := s.GetIntNoErr("client_id")
+	list, cnt := server.GetServerConfig(start, length, taskType, clientId)
 	s.AjaxTable(list, cnt, cnt)
 }
 
 func (s *IndexController) Add() {
 	if s.Ctx.Request.Method == "GET" {
 		s.Data["type"] = s.GetString("type")
+		s.Data["client_id"] = s.GetString("client_id")
 		s.SetInfo("新增")
 		s.display()
 	} else {
-		t := &server.ServerConfig{
-			TcpPort:   s.GetIntNoErr("port"),
-			Mode:      s.GetString("type"),
-			Target:    s.GetString("target"),
-			VerifyKey: utils.GetRandomString(16),
-			U:         s.GetString("u"),
-			P:         s.GetString("p"),
-			Compress:  s.GetString("compress"),
-			Crypt:     utils.GetBoolByStr(s.GetString("crypt")),
-			Mux:       utils.GetBoolByStr(s.GetString("mux")),
-			IsRun:     0,
+		t := &utils.ServerConfig{
+			TcpPort:      s.GetIntNoErr("port"),
+			Mode:         s.GetString("type"),
+			Target:       s.GetString("target"),
+			U:            s.GetString("u"),
+			P:            s.GetString("p"),
+			Compress:     s.GetString("compress"),
+			Crypt:        s.GetBoolNoErr("crypt"),
+			Mux:          s.GetBoolNoErr("mux"),
+			IsRun:        0,
+			Id:           server.CsvDb.GetTaskId(),
+			ClientId:     s.GetIntNoErr("client_id"),
+			UseClientCnf: s.GetBoolNoErr("use_client"),
+			Start:        1,
+			Remark:       s.GetString("remark"),
 		}
 		server.CsvDb.NewTask(t)
 		if err := server.AddTask(t); err != nil {
@@ -79,9 +98,9 @@ func (s *IndexController) Add() {
 }
 
 func (s *IndexController) Edit() {
+	id := s.GetIntNoErr("id")
 	if s.Ctx.Request.Method == "GET" {
-		vKey := s.GetString("vKey")
-		if t, err := server.CsvDb.GetTask(vKey); err != nil {
+		if t, err := server.CsvDb.GetTask(id); err != nil {
 			s.error()
 		} else {
 			s.Data["t"] = t
@@ -89,44 +108,46 @@ func (s *IndexController) Edit() {
 		s.SetInfo("修改")
 		s.display()
 	} else {
-		vKey := s.GetString("vKey")
-		if t, err := server.CsvDb.GetTask(vKey); err != nil {
+		if t, err := server.CsvDb.GetTask(id); err != nil {
 			s.error()
 		} else {
 			t.TcpPort = s.GetIntNoErr("port")
 			t.Mode = s.GetString("type")
 			t.Target = s.GetString("target")
+			t.Id = id
+			t.ClientId = s.GetIntNoErr("client_id")
 			t.U = s.GetString("u")
 			t.P = s.GetString("p")
 			t.Compress = s.GetString("compress")
-			t.Crypt = utils.GetBoolByStr(s.GetString("crypt"))
-			t.Mux = utils.GetBoolByStr(s.GetString("mux"))
+			t.Crypt = s.GetBoolNoErr("crypt")
+			t.Mux = s.GetBoolNoErr("mux")
+			t.UseClientCnf = s.GetBoolNoErr("use_client")
+			t.Remark = s.GetString("remark")
 			server.CsvDb.UpdateTask(t)
-			server.StopServer(t.VerifyKey)
-			server.StartTask(t.VerifyKey)
 		}
 		s.AjaxOk("修改成功")
 	}
 }
 
 func (s *IndexController) Stop() {
-	vKey := s.GetString("vKey")
-	if err := server.StopServer(vKey); err != nil {
+	id := s.GetIntNoErr("id")
+	if err := server.StopServer(id); err != nil {
 		s.AjaxErr("停止失败")
 	}
 	s.AjaxOk("停止成功")
 }
+
 func (s *IndexController) Del() {
-	vKey := s.GetString("vKey")
-	if err := server.DelTask(vKey); err != nil {
+	id := s.GetIntNoErr("id")
+	if err := server.DelTask(id); err != nil {
 		s.AjaxErr("删除失败")
 	}
 	s.AjaxOk("删除成功")
 }
 
 func (s *IndexController) Start() {
-	vKey := s.GetString("vKey")
-	if err := server.StartTask(vKey); err != nil {
+	id := s.GetIntNoErr("id")
+	if err := server.StartTask(id); err != nil {
 		s.AjaxErr("开启失败")
 	}
 	s.AjaxOk("开启成功")
@@ -134,13 +155,14 @@ func (s *IndexController) Start() {
 
 func (s *IndexController) HostList() {
 	if s.Ctx.Request.Method == "GET" {
-		s.Data["vkey"] = s.GetString("vkey")
+		s.Data["client_id"] = s.GetString("client_id")
+		s.Data["menu"] = "host"
 		s.SetInfo("域名列表")
 		s.display("index/hlist")
 	} else {
 		start, length := s.GetAjaxParams()
-		vkey := s.GetString("vkey")
-		list, cnt := server.CsvDb.GetHostList(start, length, vkey)
+		clientId := s.GetIntNoErr("client_id")
+		list, cnt := server.CsvDb.GetHostList(start, length, clientId)
 		s.AjaxTable(list, cnt, cnt)
 	}
 }
@@ -155,16 +177,18 @@ func (s *IndexController) DelHost() {
 
 func (s *IndexController) AddHost() {
 	if s.Ctx.Request.Method == "GET" {
-		s.Data["vkey"] = s.GetString("vkey")
+		s.Data["client_id"] = s.GetString("client_id")
+		s.Data["menu"] = "host"
 		s.SetInfo("新增")
 		s.display("index/hadd")
 	} else {
-		h := &server.HostList{
-			Vkey:         s.GetString("vkey"),
+		h := &utils.HostList{
+			ClientId:     s.GetIntNoErr("client_id"),
 			Host:         s.GetString("host"),
 			Target:       s.GetString("target"),
 			HeaderChange: s.GetString("header"),
 			HostChange:   s.GetString("hostchange"),
+			Remark:       s.GetString("remark"),
 		}
 		server.CsvDb.NewHost(h)
 		s.AjaxOk("添加成功")
@@ -172,26 +196,26 @@ func (s *IndexController) AddHost() {
 }
 
 func (s *IndexController) EditHost() {
+	host := s.GetString("host")
 	if s.Ctx.Request.Method == "GET" {
-		host := s.GetString("host")
-		if h, t, err := server.GetKeyByHost(host); err != nil {
+		s.Data["menu"] = "host"
+		if h, _, err := server.GetKeyByHost(host); err != nil {
 			s.error()
 		} else {
-			s.Data["t"] = t
 			s.Data["h"] = h
 		}
 		s.SetInfo("修改")
 		s.display("index/hedit")
 	} else {
-		host := s.GetString("host")
 		if h, _, err := server.GetKeyByHost(host); err != nil {
 			s.error()
 		} else {
-			h.Vkey = s.GetString("vkey")
+			h.ClientId = s.GetIntNoErr("client_id")
 			h.Host = s.GetString("nhost")
 			h.Target = s.GetString("target")
 			h.HeaderChange = s.GetString("header")
 			h.HostChange = s.GetString("hostchange")
+			h.Remark = s.GetString("remark")
 			server.CsvDb.UpdateHost(h)
 		}
 		s.AjaxOk("修改成功")

+ 1 - 0
web/routers/router.go

@@ -9,4 +9,5 @@ func init() {
 	beego.Router("/", &controllers.IndexController{}, "*:Index")
 	beego.AutoRouter(&controllers.IndexController{})
 	beego.AutoRouter(&controllers.LoginController{})
+	beego.AutoRouter(&controllers.ClientController{})
 }

+ 3477 - 0
web/static/js/chart.js

@@ -0,0 +1,3477 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 1.0.1
+ *
+ * Copyright 2015 Nick Downie
+ * Released under the MIT license
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
+ */
+
+
+(function(){
+
+	"use strict";
+
+	//Declare root variable - window in the browser, global on the server
+	var root = this,
+		previous = root.Chart;
+
+	//Occupy the global variable of Chart, and create a simple base class
+	var Chart = function(context){
+		var chart = this;
+		this.canvas = context.canvas;
+
+		this.ctx = context;
+
+		//Variables global to the chart
+		var computeDimension = function(element,dimension)
+		{
+			if (element['offset'+dimension])
+			{
+				return element['offset'+dimension];
+			}
+			else
+			{
+				return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
+			}
+		}
+
+		var width = this.width = computeDimension(context.canvas,'Width');
+		var height = this.height = computeDimension(context.canvas,'Height');
+
+		// Firefox requires this to work correctly
+		context.canvas.width  = width;
+		context.canvas.height = height;
+
+		var width = this.width = context.canvas.width;
+		var height = this.height = context.canvas.height;
+		this.aspectRatio = this.width / this.height;
+		//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
+		helpers.retinaScale(this);
+
+		return this;
+	};
+	//Globally expose the defaults to allow for user updating/changing
+	Chart.defaults = {
+		global: {
+			// Boolean - Whether to animate the chart
+			animation: true,
+
+			// Number - Number of animation steps
+			animationSteps: 60,
+
+			// String - Animation easing effect
+			animationEasing: "easeOutQuart",
+
+			// Boolean - If we should show the scale at all
+			showScale: true,
+
+			// Boolean - If we want to override with a hard coded scale
+			scaleOverride: false,
+
+			// ** Required if scaleOverride is true **
+			// Number - The number of steps in a hard coded scale
+			scaleSteps: null,
+			// Number - The value jump in the hard coded scale
+			scaleStepWidth: null,
+			// Number - The scale starting value
+			scaleStartValue: null,
+
+			// String - Colour of the scale line
+			scaleLineColor: "rgba(0,0,0,.1)",
+
+			// Number - Pixel width of the scale line
+			scaleLineWidth: 1,
+
+			// Boolean - Whether to show labels on the scale
+			scaleShowLabels: true,
+
+			// Interpolated JS string - can access value
+			scaleLabel: "<%=value%>",
+
+			// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
+			scaleIntegersOnly: true,
+
+			// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+			scaleBeginAtZero: false,
+
+			// String - Scale label font declaration for the scale label
+			scaleFontFamily: "inherit",
+
+			// Number - Scale label font size in pixels
+			scaleFontSize: 12,
+
+			// String - Scale label font weight style
+			scaleFontStyle: "normal",
+
+			// String - Scale label font colour
+			scaleFontColor: "#666",
+
+			// Boolean - whether or not the chart should be responsive and resize when the browser does.
+			responsive: true,
+
+			// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
+			maintainAspectRatio: true,
+
+			// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
+			showTooltips: true,
+
+			// Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
+			customTooltips: false,
+
+			// Array - Array of string names to attach tooltip events
+			tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
+
+			// String - Tooltip background colour
+			tooltipFillColor: "rgba(0,0,0,0.8)",
+
+			// String - Tooltip label font declaration for the scale label
+			tooltipFontFamily: "inherit",
+
+			// Number - Tooltip label font size in pixels
+			tooltipFontSize: 14,
+
+			// String - Tooltip font weight style
+			tooltipFontStyle: "normal",
+
+			// String - Tooltip label font colour
+			tooltipFontColor: "#fff",
+
+			// String - Tooltip title font declaration for the scale label
+			tooltipTitleFontFamily: "inherit",
+
+			// Number - Tooltip title font size in pixels
+			tooltipTitleFontSize: 14,
+
+			// String - Tooltip title font weight style
+			tooltipTitleFontStyle: "bold",
+
+			// String - Tooltip title font colour
+			tooltipTitleFontColor: "#fff",
+
+			// Number - pixel width of padding around tooltip text
+			tooltipYPadding: 6,
+
+			// Number - pixel width of padding around tooltip text
+			tooltipXPadding: 6,
+
+			// Number - Size of the caret on the tooltip
+			tooltipCaretSize: 8,
+
+			// Number - Pixel radius of the tooltip border
+			tooltipCornerRadius: 6,
+
+			// Number - Pixel offset from point x to tooltip edge
+			tooltipXOffset: 10,
+
+			// String - Template string for single tooltips
+			tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
+
+			// String - Template string for single tooltips
+			multiTooltipTemplate: "<%= value %>",
+
+			// String - Colour behind the legend colour block
+			multiTooltipKeyBackground: '#fff',
+
+			// Function - Will fire on animation progression.
+			onAnimationProgress: function(){},
+
+			// Function - Will fire on animation completion.
+			onAnimationComplete: function(){}
+
+		}
+	};
+
+	//Create a dictionary of chart types, to allow for extension of existing types
+	Chart.types = {};
+
+	//Global Chart helpers object for utility methods and classes
+	var helpers = Chart.helpers = {};
+
+		//-- Basic js utility methods
+	var each = helpers.each = function(loopable,callback,self){
+			var additionalArgs = Array.prototype.slice.call(arguments, 3);
+			// Check to see if null or undefined firstly.
+			if (loopable){
+				if (loopable.length === +loopable.length){
+					var i;
+					for (i=0; i<loopable.length; i++){
+						callback.apply(self,[loopable[i], i].concat(additionalArgs));
+					}
+				}
+				else{
+					for (var item in loopable){
+						callback.apply(self,[loopable[item],item].concat(additionalArgs));
+					}
+				}
+			}
+		},
+		clone = helpers.clone = function(obj){
+			var objClone = {};
+			each(obj,function(value,key){
+				if (obj.hasOwnProperty(key)) objClone[key] = value;
+			});
+			return objClone;
+		},
+		extend = helpers.extend = function(base){
+			each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
+				each(extensionObject,function(value,key){
+					if (extensionObject.hasOwnProperty(key)) base[key] = value;
+				});
+			});
+			return base;
+		},
+		merge = helpers.merge = function(base,master){
+			//Merge properties in left object over to a shallow clone of object right.
+			var args = Array.prototype.slice.call(arguments,0);
+			args.unshift({});
+			return extend.apply(null, args);
+		},
+		indexOf = helpers.indexOf = function(arrayToSearch, item){
+			if (Array.prototype.indexOf) {
+				return arrayToSearch.indexOf(item);
+			}
+			else{
+				for (var i = 0; i < arrayToSearch.length; i++) {
+					if (arrayToSearch[i] === item) return i;
+				}
+				return -1;
+			}
+		},
+		where = helpers.where = function(collection, filterCallback){
+			var filtered = [];
+
+			helpers.each(collection, function(item){
+				if (filterCallback(item)){
+					filtered.push(item);
+				}
+			});
+
+			return filtered;
+		},
+		findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to start of the array
+			if (!startIndex){
+				startIndex = -1;
+			}
+			for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to end of the array
+			if (!startIndex){
+				startIndex = arrayToSearch.length;
+			}
+			for (var i = startIndex - 1; i >= 0; i--) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		inherits = helpers.inherits = function(extensions){
+			//Basic javascript inheritance based on the model created in Backbone.js
+			var parent = this;
+			var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
+
+			var Surrogate = function(){ this.constructor = ChartElement;};
+			Surrogate.prototype = parent.prototype;
+			ChartElement.prototype = new Surrogate();
+
+			ChartElement.extend = inherits;
+
+			if (extensions) extend(ChartElement.prototype, extensions);
+
+			ChartElement.__super__ = parent.prototype;
+
+			return ChartElement;
+		},
+		noop = helpers.noop = function(){},
+		uid = helpers.uid = (function(){
+			var id=0;
+			return function(){
+				return "chart-" + id++;
+			};
+		})(),
+		warn = helpers.warn = function(str){
+			//Method for warning of errors
+			if (window.console && typeof window.console.warn == "function") console.warn(str);
+		},
+		amd = helpers.amd = (typeof define == 'function' && define.amd),
+		//-- Math methods
+		isNumber = helpers.isNumber = function(n){
+			return !isNaN(parseFloat(n)) && isFinite(n);
+		},
+		max = helpers.max = function(array){
+			return Math.max.apply( Math, array );
+		},
+		min = helpers.min = function(array){
+			return Math.min.apply( Math, array );
+		},
+		cap = helpers.cap = function(valueToCap,maxValue,minValue){
+			if(isNumber(maxValue)) {
+				if( valueToCap > maxValue ) {
+					return maxValue;
+				}
+			}
+			else if(isNumber(minValue)){
+				if ( valueToCap < minValue ){
+					return minValue;
+				}
+			}
+			return valueToCap;
+		},
+		getDecimalPlaces = helpers.getDecimalPlaces = function(num){
+			if (num%1!==0 && isNumber(num)){
+				return num.toString().split(".")[1].length;
+			}
+			else {
+				return 0;
+			}
+		},
+		toRadians = helpers.radians = function(degrees){
+			return degrees * (Math.PI/180);
+		},
+		// Gets the angle from vertical upright to the point about a centre.
+		getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
+			var distanceFromXCenter = anglePoint.x - centrePoint.x,
+				distanceFromYCenter = anglePoint.y - centrePoint.y,
+				radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
+
+
+			var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
+
+			//If the segment is in the top left quadrant, we need to add another rotation to the angle
+			if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
+				angle += Math.PI*2;
+			}
+
+			return {
+				angle: angle,
+				distance: radialDistanceFromCenter
+			};
+		},
+		aliasPixel = helpers.aliasPixel = function(pixelWidth){
+			return (pixelWidth % 2 === 0) ? 0 : 0.5;
+		},
+		splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
+			//Props to Rob Spencer at scaled innovation for his post on splining between points
+			//http://scaledinnovation.com/analytics/splines/aboutSplines.html
+			var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
+				d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
+				fa=t*d01/(d01+d12),// scaling factor for triangle Ta
+				fb=t*d12/(d01+d12);
+			return {
+				inner : {
+					x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
+				},
+				outer : {
+					x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
+				}
+			};
+		},
+		calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
+			return Math.floor(Math.log(val) / Math.LN10);
+		},
+		calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
+
+			//Set a minimum step of two - a point at the top of the graph, and a point at the base
+			var minSteps = 2,
+				maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
+				skipFitting = (minSteps >= maxSteps);
+
+			var maxValue = max(valuesArray),
+				minValue = min(valuesArray);
+
+			// We need some degree of seperation here to calculate the scales if all the values are the same
+			// Adding/minusing 0.5 will give us a range of 1.
+			if (maxValue === minValue){
+				maxValue += 0.5;
+				// So we don't end up with a graph with a negative start value if we've said always start from zero
+				if (minValue >= 0.5 && !startFromZero){
+					minValue -= 0.5;
+				}
+				else{
+					// Make up a whole number above the values
+					maxValue += 0.5;
+				}
+			}
+
+			var	valueRange = Math.abs(maxValue - minValue),
+				rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
+				graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphRange = graphMax - graphMin,
+				stepValue = Math.pow(10, rangeOrderOfMagnitude),
+				numberOfSteps = Math.round(graphRange / stepValue);
+
+			//If we have more space on the graph we'll use it to give more definition to the data
+			while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
+				if(numberOfSteps > maxSteps){
+					stepValue *=2;
+					numberOfSteps = Math.round(graphRange/stepValue);
+					// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
+					if (numberOfSteps % 1 !== 0){
+						skipFitting = true;
+					}
+				}
+				//We can fit in double the amount of scale points on the scale
+				else{
+					//If user has declared ints only, and the step value isn't a decimal
+					if (integersOnly && rangeOrderOfMagnitude >= 0){
+						//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
+						if(stepValue/2 % 1 === 0){
+							stepValue /=2;
+							numberOfSteps = Math.round(graphRange/stepValue);
+						}
+						//If it would make it a float break out of the loop
+						else{
+							break;
+						}
+					}
+					//If the scale doesn't have to be an int, make the scale more granular anyway.
+					else{
+						stepValue /=2;
+						numberOfSteps = Math.round(graphRange/stepValue);
+					}
+
+				}
+			}
+
+			if (skipFitting){
+				numberOfSteps = minSteps;
+				stepValue = graphRange / numberOfSteps;
+			}
+
+			return {
+				steps : numberOfSteps,
+				stepValue : stepValue,
+				min : graphMin,
+				max	: graphMin + (numberOfSteps * stepValue)
+			};
+
+		},
+		/* jshint ignore:start */
+		// Blows up jshint errors based on the new Function constructor
+		//Templating methods
+		//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
+		template = helpers.template = function(templateString, valuesObject){
+
+			// If templateString is function rather than string-template - call the function for valuesObject
+
+			if(templateString instanceof Function){
+			 	return templateString(valuesObject);
+		 	}
+
+			var cache = {};
+			function tmpl(str, data){
+				// Figure out if we're getting a template, or if we need to
+				// load the template - and be sure to cache the result.
+				var fn = !/\W/.test(str) ?
+				cache[str] = cache[str] :
+
+				// Generate a reusable function that will serve as a template
+				// generator (and which will be cached).
+				new Function("obj",
+					"var p=[],print=function(){p.push.apply(p,arguments);};" +
+
+					// Introduce the data as local variables using with(){}
+					"with(obj){p.push('" +
+
+					// Convert the template into pure JavaScript
+					str
+						.replace(/[\r\t\n]/g, " ")
+						.split("<%").join("\t")
+						.replace(/((^|%>)[^\t]*)'/g, "$1\r")
+						.replace(/\t=(.*?)%>/g, "',$1,'")
+						.split("\t").join("');")
+						.split("%>").join("p.push('")
+						.split("\r").join("\\'") +
+					"');}return p.join('');"
+				);
+
+				// Provide some basic currying to the user
+				return data ? fn( data ) : fn;
+			}
+			return tmpl(templateString,valuesObject);
+		},
+		/* jshint ignore:end */
+		generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
+			var labelsArray = new Array(numberOfSteps);
+			if (labelTemplateString){
+				each(labelsArray,function(val,index){
+					labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
+				});
+			}
+			return labelsArray;
+		},
+		//--Animation methods
+		//Easing functions adapted from Robert Penner's easing equations
+		//http://www.robertpenner.com/easing/
+		easingEffects = helpers.easingEffects = {
+			linear: function (t) {
+				return t;
+			},
+			easeInQuad: function (t) {
+				return t * t;
+			},
+			easeOutQuad: function (t) {
+				return -1 * t * (t - 2);
+			},
+			easeInOutQuad: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
+				return -1 / 2 * ((--t) * (t - 2) - 1);
+			},
+			easeInCubic: function (t) {
+				return t * t * t;
+			},
+			easeOutCubic: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t + 1);
+			},
+			easeInOutCubic: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
+				return 1 / 2 * ((t -= 2) * t * t + 2);
+			},
+			easeInQuart: function (t) {
+				return t * t * t * t;
+			},
+			easeOutQuart: function (t) {
+				return -1 * ((t = t / 1 - 1) * t * t * t - 1);
+			},
+			easeInOutQuart: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
+				return -1 / 2 * ((t -= 2) * t * t * t - 2);
+			},
+			easeInQuint: function (t) {
+				return 1 * (t /= 1) * t * t * t * t;
+			},
+			easeOutQuint: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
+			},
+			easeInOutQuint: function (t) {
+				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
+				return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
+			},
+			easeInSine: function (t) {
+				return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
+			},
+			easeOutSine: function (t) {
+				return 1 * Math.sin(t / 1 * (Math.PI / 2));
+			},
+			easeInOutSine: function (t) {
+				return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
+			},
+			easeInExpo: function (t) {
+				return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
+			},
+			easeOutExpo: function (t) {
+				return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
+			},
+			easeInOutExpo: function (t) {
+				if (t === 0) return 0;
+				if (t === 1) return 1;
+				if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
+				return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
+			},
+			easeInCirc: function (t) {
+				if (t >= 1) return t;
+				return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
+			},
+			easeOutCirc: function (t) {
+				return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
+			},
+			easeInOutCirc: function (t) {
+				if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
+				return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
+			},
+			easeInElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1) == 1) return 1;
+				if (!p) p = 1 * 0.3;
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+			},
+			easeOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1) == 1) return 1;
+				if (!p) p = 1 * 0.3;
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
+			},
+			easeInOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0) return 0;
+				if ((t /= 1 / 2) == 2) return 1;
+				if (!p) p = 1 * (0.3 * 1.5);
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
+				if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+				return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
+			},
+			easeInBack: function (t) {
+				var s = 1.70158;
+				return 1 * (t /= 1) * t * ((s + 1) * t - s);
+			},
+			easeOutBack: function (t) {
+				var s = 1.70158;
+				return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
+			},
+			easeInOutBack: function (t) {
+				var s = 1.70158;
+				if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
+				return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
+			},
+			easeInBounce: function (t) {
+				return 1 - easingEffects.easeOutBounce(1 - t);
+			},
+			easeOutBounce: function (t) {
+				if ((t /= 1) < (1 / 2.75)) {
+					return 1 * (7.5625 * t * t);
+				} else if (t < (2 / 2.75)) {
+					return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
+				} else if (t < (2.5 / 2.75)) {
+					return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
+				} else {
+					return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
+				}
+			},
+			easeInOutBounce: function (t) {
+				if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5;
+				return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
+			}
+		},
+		//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
+		requestAnimFrame = helpers.requestAnimFrame = (function(){
+			return window.requestAnimationFrame ||
+				window.webkitRequestAnimationFrame ||
+				window.mozRequestAnimationFrame ||
+				window.oRequestAnimationFrame ||
+				window.msRequestAnimationFrame ||
+				function(callback) {
+					return window.setTimeout(callback, 1000 / 60);
+				};
+		})(),
+		cancelAnimFrame = helpers.cancelAnimFrame = (function(){
+			return window.cancelAnimationFrame ||
+				window.webkitCancelAnimationFrame ||
+				window.mozCancelAnimationFrame ||
+				window.oCancelAnimationFrame ||
+				window.msCancelAnimationFrame ||
+				function(callback) {
+					return window.clearTimeout(callback, 1000 / 60);
+				};
+		})(),
+		animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
+
+			var currentStep = 0,
+				easingFunction = easingEffects[easingString] || easingEffects.linear;
+
+			var animationFrame = function(){
+				currentStep++;
+				var stepDecimal = currentStep/totalSteps;
+				var easeDecimal = easingFunction(stepDecimal);
+
+				callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
+				onProgress.call(chartInstance,easeDecimal,stepDecimal);
+				if (currentStep < totalSteps){
+					chartInstance.animationFrame = requestAnimFrame(animationFrame);
+				} else{
+					onComplete.apply(chartInstance);
+				}
+			};
+			requestAnimFrame(animationFrame);
+		},
+		//-- DOM methods
+		getRelativePosition = helpers.getRelativePosition = function(evt){
+			var mouseX, mouseY;
+			var e = evt.originalEvent || evt,
+				canvas = evt.currentTarget || evt.srcElement,
+				boundingRect = canvas.getBoundingClientRect();
+
+			if (e.touches){
+				mouseX = e.touches[0].clientX - boundingRect.left;
+				mouseY = e.touches[0].clientY - boundingRect.top;
+
+			}
+			else{
+				mouseX = e.clientX - boundingRect.left;
+				mouseY = e.clientY - boundingRect.top;
+			}
+
+			return {
+				x : mouseX,
+				y : mouseY
+			};
+
+		},
+		addEvent = helpers.addEvent = function(node,eventType,method){
+			if (node.addEventListener){
+				node.addEventListener(eventType,method);
+			} else if (node.attachEvent){
+				node.attachEvent("on"+eventType, method);
+			} else {
+				node["on"+eventType] = method;
+			}
+		},
+		removeEvent = helpers.removeEvent = function(node, eventType, handler){
+			if (node.removeEventListener){
+				node.removeEventListener(eventType, handler, false);
+			} else if (node.detachEvent){
+				node.detachEvent("on"+eventType,handler);
+			} else{
+				node["on" + eventType] = noop;
+			}
+		},
+		bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
+			// Create the events object if it's not already present
+			if (!chartInstance.events) chartInstance.events = {};
+
+			each(arrayOfEvents,function(eventName){
+				chartInstance.events[eventName] = function(){
+					handler.apply(chartInstance, arguments);
+				};
+				addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
+			});
+		},
+		unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
+			each(arrayOfEvents, function(handler,eventName){
+				removeEvent(chartInstance.chart.canvas, eventName, handler);
+			});
+		},
+		getMaximumWidth = helpers.getMaximumWidth = function(domNode){
+			var container = domNode.parentNode;
+			// TODO = check cross browser stuff with this.
+			return container.clientWidth;
+		},
+		getMaximumHeight = helpers.getMaximumHeight = function(domNode){
+			var container = domNode.parentNode;
+			// TODO = check cross browser stuff with this.
+			return container.clientHeight;
+		},
+		getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
+		retinaScale = helpers.retinaScale = function(chart){
+			var ctx = chart.ctx,
+				width = chart.canvas.width,
+				height = chart.canvas.height;
+
+			if (window.devicePixelRatio) {
+				ctx.canvas.style.width = width + "px";
+				ctx.canvas.style.height = height + "px";
+				ctx.canvas.height = height * window.devicePixelRatio;
+				ctx.canvas.width = width * window.devicePixelRatio;
+				ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+			}
+		},
+		//-- Canvas methods
+		clear = helpers.clear = function(chart){
+			chart.ctx.clearRect(0,0,chart.width,chart.height);
+		},
+		fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
+			return fontStyle + " " + pixelSize+"px " + fontFamily;
+		},
+		longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
+			ctx.font = font;
+			var longest = 0;
+			each(arrayOfStrings,function(string){
+				var textWidth = ctx.measureText(string).width;
+				longest = (textWidth > longest) ? textWidth : longest;
+			});
+			return longest;
+		},
+		drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
+			ctx.beginPath();
+			ctx.moveTo(x + radius, y);
+			ctx.lineTo(x + width - radius, y);
+			ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+			ctx.lineTo(x + width, y + height - radius);
+			ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+			ctx.lineTo(x + radius, y + height);
+			ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+			ctx.lineTo(x, y + radius);
+			ctx.quadraticCurveTo(x, y, x + radius, y);
+			ctx.closePath();
+		};
+
+
+	//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
+	//Destroy method on the chart will remove the instance of the chart from this reference.
+	Chart.instances = {};
+
+	Chart.Type = function(data,options,chart){
+		this.options = options;
+		this.chart = chart;
+		this.id = uid();
+		//Add the chart instance to the global namespace
+		Chart.instances[this.id] = this;
+
+		// Initialize is always called when a chart type is created
+		// By default it is a no op, but it should be extended
+		if (options.responsive){
+			this.resize();
+		}
+		this.initialize.call(this,data);
+	};
+
+	//Core methods that'll be a part of every chart type
+	extend(Chart.Type.prototype,{
+		initialize : function(){return this;},
+		clear : function(){
+			clear(this.chart);
+			return this;
+		},
+		stop : function(){
+			// Stops any current animation loop occuring
+			cancelAnimFrame(this.animationFrame);
+			return this;
+		},
+		resize : function(callback){
+			this.stop();
+			var canvas = this.chart.canvas,
+				newWidth = getMaximumWidth(this.chart.canvas),
+				newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
+
+			canvas.width = this.chart.width = newWidth;
+			canvas.height = this.chart.height = newHeight;
+
+			retinaScale(this.chart);
+
+			if (typeof callback === "function"){
+				callback.apply(this, Array.prototype.slice.call(arguments, 1));
+			}
+			return this;
+		},
+		reflow : noop,
+		render : function(reflow){
+			if (reflow){
+				this.reflow();
+			}
+			if (this.options.animation && !reflow){
+				helpers.animationLoop(
+					this.draw,
+					this.options.animationSteps,
+					this.options.animationEasing,
+					this.options.onAnimationProgress,
+					this.options.onAnimationComplete,
+					this
+				);
+			}
+			else{
+				this.draw();
+				this.options.onAnimationComplete.call(this);
+			}
+			return this;
+		},
+		generateLegend : function(){
+			return template(this.options.legendTemplate,this);
+		},
+		destroy : function(){
+			this.clear();
+			unbindEvents(this, this.events);
+			var canvas = this.chart.canvas;
+
+			// Reset canvas height/width attributes starts a fresh with the canvas context
+			canvas.width = this.chart.width;
+			canvas.height = this.chart.height;
+
+			// < IE9 doesn't support removeProperty
+			if (canvas.style.removeProperty) {
+				canvas.style.removeProperty('width');
+				canvas.style.removeProperty('height');
+			} else {
+				canvas.style.removeAttribute('width');
+				canvas.style.removeAttribute('height');
+			}
+
+			delete Chart.instances[this.id];
+		},
+		showTooltip : function(ChartElements, forceRedraw){
+			// Only redraw the chart if we've actually changed what we're hovering on.
+			if (typeof this.activeElements === 'undefined') this.activeElements = [];
+
+			var isChanged = (function(Elements){
+				var changed = false;
+
+				if (Elements.length !== this.activeElements.length){
+					changed = true;
+					return changed;
+				}
+
+				each(Elements, function(element, index){
+					if (element !== this.activeElements[index]){
+						changed = true;
+					}
+				}, this);
+				return changed;
+			}).call(this, ChartElements);
+
+			if (!isChanged && !forceRedraw){
+				return;
+			}
+			else{
+				this.activeElements = ChartElements;
+			}
+			this.draw();
+			if(this.options.customTooltips){
+				this.options.customTooltips(false);
+			}
+			if (ChartElements.length > 0){
+				// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
+				if (this.datasets && this.datasets.length > 1) {
+					var dataArray,
+						dataIndex;
+
+					for (var i = this.datasets.length - 1; i >= 0; i--) {
+						dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
+						dataIndex = indexOf(dataArray, ChartElements[0]);
+						if (dataIndex !== -1){
+							break;
+						}
+					}
+					var tooltipLabels = [],
+						tooltipColors = [],
+						medianPosition = (function(index) {
+
+							// Get all the points at that particular index
+							var Elements = [],
+								dataCollection,
+								xPositions = [],
+								yPositions = [],
+								xMax,
+								yMax,
+								xMin,
+								yMin;
+							helpers.each(this.datasets, function(dataset){
+								dataCollection = dataset.points || dataset.bars || dataset.segments;
+								if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
+									Elements.push(dataCollection[dataIndex]);
+								}
+							});
+
+							helpers.each(Elements, function(element) {
+								xPositions.push(element.x);
+								yPositions.push(element.y);
+
+
+								//Include any colour information about the element
+								tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
+								tooltipColors.push({
+									fill: element._saved.fillColor || element.fillColor,
+									stroke: element._saved.strokeColor || element.strokeColor
+								});
+
+							}, this);
+
+							yMin = min(yPositions);
+							yMax = max(yPositions);
+
+							xMin = min(xPositions);
+							xMax = max(xPositions);
+
+							return {
+								x: (xMin > this.chart.width/2) ? xMin : xMax,
+								y: (yMin + yMax)/2
+							};
+						}).call(this, dataIndex);
+
+					new Chart.MultiTooltip({
+						x: medianPosition.x,
+						y: medianPosition.y,
+						xPadding: this.options.tooltipXPadding,
+						yPadding: this.options.tooltipYPadding,
+						xOffset: this.options.tooltipXOffset,
+						fillColor: this.options.tooltipFillColor,
+						textColor: this.options.tooltipFontColor,
+						fontFamily: this.options.tooltipFontFamily,
+						fontStyle: this.options.tooltipFontStyle,
+						fontSize: this.options.tooltipFontSize,
+						titleTextColor: this.options.tooltipTitleFontColor,
+						titleFontFamily: this.options.tooltipTitleFontFamily,
+						titleFontStyle: this.options.tooltipTitleFontStyle,
+						titleFontSize: this.options.tooltipTitleFontSize,
+						cornerRadius: this.options.tooltipCornerRadius,
+						labels: tooltipLabels,
+						legendColors: tooltipColors,
+						legendColorBackground : this.options.multiTooltipKeyBackground,
+						title: ChartElements[0].label,
+						chart: this.chart,
+						ctx: this.chart.ctx,
+						custom: this.options.customTooltips
+					}).draw();
+
+				} else {
+					each(ChartElements, function(Element) {
+						var tooltipPosition = Element.tooltipPosition();
+						new Chart.Tooltip({
+							x: Math.round(tooltipPosition.x),
+							y: Math.round(tooltipPosition.y),
+							xPadding: this.options.tooltipXPadding,
+							yPadding: this.options.tooltipYPadding,
+							fillColor: this.options.tooltipFillColor,
+							textColor: this.options.tooltipFontColor,
+							fontFamily: this.options.tooltipFontFamily,
+							fontStyle: this.options.tooltipFontStyle,
+							fontSize: this.options.tooltipFontSize,
+							caretHeight: this.options.tooltipCaretSize,
+							cornerRadius: this.options.tooltipCornerRadius,
+							text: template(this.options.tooltipTemplate, Element),
+							chart: this.chart,
+							custom: this.options.customTooltips
+						}).draw();
+					}, this);
+				}
+			}
+			return this;
+		},
+		toBase64Image : function(){
+			return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
+		}
+	});
+
+	Chart.Type.extend = function(extensions){
+
+		var parent = this;
+
+		var ChartType = function(){
+			return parent.apply(this,arguments);
+		};
+
+		//Copy the prototype object of the this class
+		ChartType.prototype = clone(parent.prototype);
+		//Now overwrite some of the properties in the base class with the new extensions
+		extend(ChartType.prototype, extensions);
+
+		ChartType.extend = Chart.Type.extend;
+
+		if (extensions.name || parent.prototype.name){
+
+			var chartName = extensions.name || parent.prototype.name;
+			//Assign any potential default values of the new chart type
+
+			//If none are defined, we'll use a clone of the chart type this is being extended from.
+			//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
+			//doesn't define some defaults of their own.
+
+			var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
+
+			Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
+
+			Chart.types[chartName] = ChartType;
+
+			//Register this new chart type in the Chart prototype
+			Chart.prototype[chartName] = function(data,options){
+				var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
+				return new ChartType(data,config,this);
+			};
+		} else{
+			warn("Name not provided for this chart, so it hasn't been registered");
+		}
+		return parent;
+	};
+
+	Chart.Element = function(configuration){
+		extend(this,configuration);
+		this.initialize.apply(this,arguments);
+		this.save();
+	};
+	extend(Chart.Element.prototype,{
+		initialize : function(){},
+		restore : function(props){
+			if (!props){
+				extend(this,this._saved);
+			} else {
+				each(props,function(key){
+					this[key] = this._saved[key];
+				},this);
+			}
+			return this;
+		},
+		save : function(){
+			this._saved = clone(this);
+			delete this._saved._saved;
+			return this;
+		},
+		update : function(newProps){
+			each(newProps,function(value,key){
+				this._saved[key] = this[key];
+				this[key] = value;
+			},this);
+			return this;
+		},
+		transition : function(props,ease){
+			each(props,function(value,key){
+				this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
+			},this);
+			return this;
+		},
+		tooltipPosition : function(){
+			return {
+				x : this.x,
+				y : this.y
+			};
+		},
+		hasValue: function(){
+			return isNumber(this.value);
+		}
+	});
+
+	Chart.Element.extend = inherits;
+
+
+	Chart.Point = Chart.Element.extend({
+		display: true,
+		inRange: function(chartX,chartY){
+			var hitDetectionRange = this.hitDetectionRadius + this.radius;
+			return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
+		},
+		draw : function(){
+			if (this.display){
+				var ctx = this.ctx;
+				ctx.beginPath();
+
+				ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
+				ctx.closePath();
+
+				ctx.strokeStyle = this.strokeColor;
+				ctx.lineWidth = this.strokeWidth;
+
+				ctx.fillStyle = this.fillColor;
+
+				ctx.fill();
+				ctx.stroke();
+			}
+
+
+			//Quick debug for bezier curve splining
+			//Highlights control points and the line between them.
+			//Handy for dev - stripped in the min version.
+
+			// ctx.save();
+			// ctx.fillStyle = "black";
+			// ctx.strokeStyle = "black"
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
+			// ctx.lineTo(this.x, this.y);
+			// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
+			// ctx.stroke();
+
+			// ctx.restore();
+
+
+
+		}
+	});
+
+	Chart.Arc = Chart.Element.extend({
+		inRange : function(chartX,chartY){
+
+			var pointRelativePosition = helpers.getAngleFromPoint(this, {
+				x: chartX,
+				y: chartY
+			});
+
+			//Check if within the range of the open/close angle
+			var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle),
+				withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
+
+			return (betweenAngles && withinRadius);
+			//Ensure within the outside of the arc centre, but inside arc outer
+		},
+		tooltipPosition : function(){
+			var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
+				rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
+			return {
+				x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
+				y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
+			};
+		},
+		draw : function(animationPercent){
+
+			var easingDecimal = animationPercent || 1;
+
+			var ctx = this.ctx;
+
+			ctx.beginPath();
+
+			ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle);
+
+			ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true);
+
+			ctx.closePath();
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			ctx.fillStyle = this.fillColor;
+
+			ctx.fill();
+			ctx.lineJoin = 'bevel';
+
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		}
+	});
+
+	Chart.Rectangle = Chart.Element.extend({
+		draw : function(){
+			var ctx = this.ctx,
+				halfWidth = this.width/2,
+				leftX = this.x - halfWidth,
+				rightX = this.x + halfWidth,
+				top = this.base - (this.base - this.y),
+				halfStroke = this.strokeWidth / 2;
+
+			// Canvas doesn't allow us to stroke inside the width so we can
+			// adjust the sizes to fit if we're setting a stroke on the line
+			if (this.showStroke){
+				leftX += halfStroke;
+				rightX -= halfStroke;
+				top += halfStroke;
+			}
+
+			ctx.beginPath();
+
+			ctx.fillStyle = this.fillColor;
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			// It'd be nice to keep this class totally generic to any rectangle
+			// and simply specify which border to miss out.
+			ctx.moveTo(leftX, this.base);
+			ctx.lineTo(leftX, top);
+			ctx.lineTo(rightX, top);
+			ctx.lineTo(rightX, this.base);
+			ctx.fill();
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		},
+		height : function(){
+			return this.base - this.y;
+		},
+		inRange : function(chartX,chartY){
+			return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
+		}
+	});
+
+	Chart.Tooltip = Chart.Element.extend({
+		draw : function(){
+
+			var ctx = this.chart.ctx;
+
+			ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.xAlign = "center";
+			this.yAlign = "above";
+
+			//Distance between the actual element.y position and the start of the tooltip caret
+			var caretPadding = this.caretPadding = 2;
+
+			var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
+				tooltipRectHeight = this.fontSize + 2*this.yPadding,
+				tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
+
+			if (this.x + tooltipWidth/2 >this.chart.width){
+				this.xAlign = "left";
+			} else if (this.x - tooltipWidth/2 < 0){
+				this.xAlign = "right";
+			}
+
+			if (this.y - tooltipHeight < 0){
+				this.yAlign = "below";
+			}
+
+
+			var tooltipX = this.x - tooltipWidth/2,
+				tooltipY = this.y - tooltipHeight;
+
+			ctx.fillStyle = this.fillColor;
+
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				switch(this.yAlign)
+				{
+				case "above":
+					//Draw a caret above the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x,this.y - caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.closePath();
+					ctx.fill();
+					break;
+				case "below":
+					tooltipY = this.y + caretPadding + this.caretHeight;
+					//Draw a caret below the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x, this.y + caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.closePath();
+					ctx.fill();
+					break;
+				}
+
+				switch(this.xAlign)
+				{
+				case "left":
+					tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
+					break;
+				case "right":
+					tooltipX = this.x - (this.cornerRadius + this.caretHeight);
+					break;
+				}
+
+				drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
+
+				ctx.fill();
+
+				ctx.fillStyle = this.textColor;
+				ctx.textAlign = "center";
+				ctx.textBaseline = "middle";
+				ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
+			}
+		}
+	});
+
+	Chart.MultiTooltip = Chart.Element.extend({
+		initialize : function(){
+			this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
+
+			this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5;
+
+			this.ctx.font = this.titleFont;
+
+			var titleWidth = this.ctx.measureText(this.title).width,
+				//Label has a legend square as well so account for this.
+				labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
+				longestTextWidth = max([labelWidth,titleWidth]);
+
+			this.width = longestTextWidth + (this.xPadding*2);
+
+
+			var halfHeight = this.height/2;
+
+			//Check to ensure the height will fit on the canvas
+			if (this.y - halfHeight < 0 ){
+				this.y = halfHeight;
+			} else if (this.y + halfHeight > this.chart.height){
+				this.y = this.chart.height - halfHeight;
+			}
+
+			//Decide whether to align left or right based on position on canvas
+			if (this.x > this.chart.width/2){
+				this.x -= this.xOffset + this.width;
+			} else {
+				this.x += this.xOffset;
+			}
+
+
+		},
+		getLineHeight : function(index){
+			var baseLineHeight = this.y - (this.height/2) + this.yPadding,
+				afterTitleIndex = index-1;
+
+			//If the index is zero, we're getting the title
+			if (index === 0){
+				return baseLineHeight + this.titleFontSize/2;
+			} else{
+				return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5;
+			}
+
+		},
+		draw : function(){
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
+				var ctx = this.ctx;
+				ctx.fillStyle = this.fillColor;
+				ctx.fill();
+				ctx.closePath();
+
+				ctx.textAlign = "left";
+				ctx.textBaseline = "middle";
+				ctx.fillStyle = this.titleTextColor;
+				ctx.font = this.titleFont;
+
+				ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
+
+				ctx.font = this.font;
+				helpers.each(this.labels,function(label,index){
+					ctx.fillStyle = this.textColor;
+					ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
+
+					//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
+					//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+					//Instead we'll make a white filled block to put the legendColour palette over.
+
+					ctx.fillStyle = this.legendColorBackground;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+					ctx.fillStyle = this.legendColors[index].fill;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+
+				},this);
+			}
+		}
+	});
+
+	Chart.Scale = Chart.Element.extend({
+		initialize : function(){
+			this.fit();
+		},
+		buildYLabels : function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+			this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0;
+		},
+		addXLabel : function(label){
+			this.xLabels.push(label);
+			this.valuesCount++;
+			this.fit();
+		},
+		removeXLabel : function(){
+			this.xLabels.shift();
+			this.valuesCount--;
+			this.fit();
+		},
+		// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
+		fit: function(){
+			// First we need the width of the yLabels, assuming the xLabels aren't rotated
+
+			// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
+			this.startPoint = (this.display) ? this.fontSize : 0;
+			this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
+
+			// Apply padding settings to the start and end point.
+			this.startPoint += this.padding;
+			this.endPoint -= this.padding;
+
+			// Cache the starting height, so can determine if we need to recalculate the scale yAxis
+			var cachedHeight = this.endPoint - this.startPoint,
+				cachedYLabelWidth;
+
+			// Build the current yLabels so we have an idea of what size they'll be to start
+			/*
+			 *	This sets what is returned from calculateScaleRange as static properties of this class:
+			 *
+				this.steps;
+				this.stepValue;
+				this.min;
+				this.max;
+			 *
+			 */
+			this.calculateYRange(cachedHeight);
+
+			// With these properties set we can now build the array of yLabels
+			// and also the width of the largest yLabel
+			this.buildYLabels();
+
+			this.calculateXLabelRotation();
+
+			while((cachedHeight > this.endPoint - this.startPoint)){
+				cachedHeight = this.endPoint - this.startPoint;
+				cachedYLabelWidth = this.yLabelWidth;
+
+				this.calculateYRange(cachedHeight);
+				this.buildYLabels();
+
+				// Only go through the xLabel loop again if the yLabel width has changed
+				if (cachedYLabelWidth < this.yLabelWidth){
+					this.calculateXLabelRotation();
+				}
+			}
+
+		},
+		calculateXLabelRotation : function(){
+			//Get the width of each grid by calculating the difference
+			//between x offsets between 0 and 1.
+
+			this.ctx.font = this.font;
+
+			var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
+				lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
+				firstRotated,
+				lastRotated;
+
+
+			this.xScalePaddingRight = lastWidth/2 + 3;
+			this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
+
+			this.xLabelRotation = 0;
+			if (this.display){
+				var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
+					cosRotation,
+					firstRotatedWidth;
+				this.xLabelWidth = originalLabelWidth;
+				//Allow 3 pixels x2 padding either side for label readability
+				var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
+
+				//Max label rotate should be 90 - also act as a loop counter
+				while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
+					cosRotation = Math.cos(toRadians(this.xLabelRotation));
+
+					firstRotated = cosRotation * firstWidth;
+					lastRotated = cosRotation * lastWidth;
+
+					// We're right aligning the text now.
+					if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){
+						this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
+					}
+					this.xScalePaddingRight = this.fontSize/2;
+
+
+					this.xLabelRotation++;
+					this.xLabelWidth = cosRotation * originalLabelWidth;
+
+				}
+				if (this.xLabelRotation > 0){
+					this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
+				}
+			}
+			else{
+				this.xLabelWidth = 0;
+				this.xScalePaddingRight = this.padding;
+				this.xScalePaddingLeft = this.padding;
+			}
+
+		},
+		// Needs to be overidden in each Chart type
+		// Otherwise we need to pass all the data into the scale class
+		calculateYRange: noop,
+		drawingArea: function(){
+			return this.startPoint - this.endPoint;
+		},
+		calculateY : function(value){
+			var scalingFactor = this.drawingArea() / (this.min - this.max);
+			return this.endPoint - (scalingFactor * (value - this.min));
+		},
+		calculateX : function(index){
+			var isRotated = (this.xLabelRotation > 0),
+				// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
+				innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
+				valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
+				valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
+
+			if (this.offsetGridLines){
+				valueOffset += (valueWidth/2);
+			}
+
+			return Math.round(valueOffset);
+		},
+		update : function(newProps){
+			helpers.extend(this, newProps);
+			this.fit();
+		},
+		draw : function(){
+			var ctx = this.ctx,
+				yLabelGap = (this.endPoint - this.startPoint) / this.steps,
+				xStart = Math.round(this.xScalePaddingLeft);
+			if (this.display){
+				ctx.fillStyle = this.textColor;
+				ctx.font = this.font;
+				each(this.yLabels,function(labelString,index){
+					var yLabelCenter = this.endPoint - (yLabelGap * index),
+						linePositionY = Math.round(yLabelCenter),
+						drawHorizontalLine = this.showHorizontalLines;
+
+					ctx.textAlign = "right";
+					ctx.textBaseline = "middle";
+					if (this.showLabels){
+						ctx.fillText(labelString,xStart - 10,yLabelCenter);
+					}
+
+					// This is X axis, so draw it
+					if (index === 0 && !drawHorizontalLine){
+						drawHorizontalLine = true;
+					}
+
+					if (drawHorizontalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					linePositionY += helpers.aliasPixel(ctx.lineWidth);
+
+					if(drawHorizontalLine){
+						ctx.moveTo(xStart, linePositionY);
+						ctx.lineTo(this.width, linePositionY);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+					ctx.beginPath();
+					ctx.moveTo(xStart - 5, linePositionY);
+					ctx.lineTo(xStart, linePositionY);
+					ctx.stroke();
+					ctx.closePath();
+
+				},this);
+
+				each(this.xLabels,function(label,index){
+					var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
+						// Check to see if line/bar here and decide where to place the line
+						linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
+						isRotated = (this.xLabelRotation > 0),
+						drawVerticalLine = this.showVerticalLines;
+
+					// This is Y axis, so draw it
+					if (index === 0 && !drawVerticalLine){
+						drawVerticalLine = true;
+					}
+
+					if (drawVerticalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					if (drawVerticalLine){
+						ctx.moveTo(linePos,this.endPoint);
+						ctx.lineTo(linePos,this.startPoint - 3);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+
+
+					// Small lines at the bottom of the base grid line
+					ctx.beginPath();
+					ctx.moveTo(linePos,this.endPoint);
+					ctx.lineTo(linePos,this.endPoint + 5);
+					ctx.stroke();
+					ctx.closePath();
+
+					ctx.save();
+					ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
+					ctx.rotate(toRadians(this.xLabelRotation)*-1);
+					ctx.font = this.font;
+					ctx.textAlign = (isRotated) ? "right" : "center";
+					ctx.textBaseline = (isRotated) ? "middle" : "top";
+					ctx.fillText(label, 0, 0);
+					ctx.restore();
+				},this);
+
+			}
+		}
+
+	});
+
+	Chart.RadialScale = Chart.Element.extend({
+		initialize: function(){
+			this.size = min([this.height, this.width]);
+			this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+		},
+		calculateCenterOffset: function(value){
+			// Take into account half font size + the yPadding of the top value
+			var scalingFactor = this.drawingArea / (this.max - this.min);
+
+			return (value - this.min) * scalingFactor;
+		},
+		update : function(){
+			if (!this.lineArc){
+				this.setScaleSize();
+			} else {
+				this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+			}
+			this.buildYLabels();
+		},
+		buildYLabels: function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+		},
+		getCircumference : function(){
+			return ((Math.PI*2) / this.valuesCount);
+		},
+		setScaleSize: function(){
+			/*
+			 * Right, this is really confusing and there is a lot of maths going on here
+			 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
+			 *
+			 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
+			 *
+			 * Solution:
+			 *
+			 * We assume the radius of the polygon is half the size of the canvas at first
+			 * at each index we check if the text overlaps.
+			 *
+			 * Where it does, we store that angle and that index.
+			 *
+			 * After finding the largest index and angle we calculate how much we need to remove
+			 * from the shape radius to move the point inwards by that x.
+			 *
+			 * We average the left and right distances to get the maximum shape radius that can fit in the box
+			 * along with labels.
+			 *
+			 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
+			 * on each side, removing that from the size, halving it and adding the left x protrusion width.
+			 *
+			 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
+			 * and position it in the most space efficient manner
+			 *
+			 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
+			 */
+
+
+			// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
+			// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
+			var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
+				pointPosition,
+				i,
+				textWidth,
+				halfTextWidth,
+				furthestRight = this.width,
+				furthestRightIndex,
+				furthestRightAngle,
+				furthestLeft = 0,
+				furthestLeftIndex,
+				furthestLeftAngle,
+				xProtrusionLeft,
+				xProtrusionRight,
+				radiusReductionRight,
+				radiusReductionLeft,
+				maxWidthRadius;
+			this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+			for (i=0;i<this.valuesCount;i++){
+				// 5px to space the text slightly out - similar to what we do in the draw function.
+				pointPosition = this.getPointPosition(i, largestPossibleRadius);
+				textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
+				if (i === 0 || i === this.valuesCount/2){
+					// If we're at index zero, or exactly the middle, we're at exactly the top/bottom
+					// of the radar chart, so text will be aligned centrally, so we'll half it and compare
+					// w/left and right text sizes
+					halfTextWidth = textWidth/2;
+					if (pointPosition.x + halfTextWidth > furthestRight) {
+						furthestRight = pointPosition.x + halfTextWidth;
+						furthestRightIndex = i;
+					}
+					if (pointPosition.x - halfTextWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - halfTextWidth;
+						furthestLeftIndex = i;
+					}
+				}
+				else if (i < this.valuesCount/2) {
+					// Less than half the values means we'll left align the text
+					if (pointPosition.x + textWidth > furthestRight) {
+						furthestRight = pointPosition.x + textWidth;
+						furthestRightIndex = i;
+					}
+				}
+				else if (i > this.valuesCount/2){
+					// More than half the values means we'll right align the text
+					if (pointPosition.x - textWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - textWidth;
+						furthestLeftIndex = i;
+					}
+				}
+			}
+
+			xProtrusionLeft = furthestLeft;
+
+			xProtrusionRight = Math.ceil(furthestRight - this.width);
+
+			furthestRightAngle = this.getIndexAngle(furthestRightIndex);
+
+			furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
+
+			radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
+
+			radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
+
+			// Ensure we actually need to reduce the size of the chart
+			radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
+			radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
+
+			this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
+
+			//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
+			this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
+
+		},
+		setCenterPoint: function(leftMovement, rightMovement){
+
+			var maxRight = this.width - rightMovement - this.drawingArea,
+				maxLeft = leftMovement + this.drawingArea;
+
+			this.xCenter = (maxLeft + maxRight)/2;
+			// Always vertically in the centre as the text height doesn't change
+			this.yCenter = (this.height/2);
+		},
+
+		getIndexAngle : function(index){
+			var angleMultiplier = (Math.PI * 2) / this.valuesCount;
+			// Start from the top instead of right, so remove a quarter of the circle
+
+			return index * angleMultiplier - (Math.PI/2);
+		},
+		getPointPosition : function(index, distanceFromCenter){
+			var thisAngle = this.getIndexAngle(index);
+			return {
+				x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
+				y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
+			};
+		},
+		draw: function(){
+			if (this.display){
+				var ctx = this.ctx;
+				each(this.yLabels, function(label, index){
+					// Don't draw a centre value
+					if (index > 0){
+						var yCenterOffset = index * (this.drawingArea/this.steps),
+							yHeight = this.yCenter - yCenterOffset,
+							pointPosition;
+
+						// Draw circular lines around the scale
+						if (this.lineWidth > 0){
+							ctx.strokeStyle = this.lineColor;
+							ctx.lineWidth = this.lineWidth;
+
+							if(this.lineArc){
+								ctx.beginPath();
+								ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
+								ctx.closePath();
+								ctx.stroke();
+							} else{
+								ctx.beginPath();
+								for (var i=0;i<this.valuesCount;i++)
+								{
+									pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
+									if (i === 0){
+										ctx.moveTo(pointPosition.x, pointPosition.y);
+									} else {
+										ctx.lineTo(pointPosition.x, pointPosition.y);
+									}
+								}
+								ctx.closePath();
+								ctx.stroke();
+							}
+						}
+						if(this.showLabels){
+							ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+							if (this.showLabelBackdrop){
+								var labelWidth = ctx.measureText(label).width;
+								ctx.fillStyle = this.backdropColor;
+								ctx.fillRect(
+									this.xCenter - labelWidth/2 - this.backdropPaddingX,
+									yHeight - this.fontSize/2 - this.backdropPaddingY,
+									labelWidth + this.backdropPaddingX*2,
+									this.fontSize + this.backdropPaddingY*2
+								);
+							}
+							ctx.textAlign = 'center';
+							ctx.textBaseline = "middle";
+							ctx.fillStyle = this.fontColor;
+							ctx.fillText(label, this.xCenter, yHeight);
+						}
+					}
+				}, this);
+
+				if (!this.lineArc){
+					ctx.lineWidth = this.angleLineWidth;
+					ctx.strokeStyle = this.angleLineColor;
+					for (var i = this.valuesCount - 1; i >= 0; i--) {
+						if (this.angleLineWidth > 0){
+							var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max));
+							ctx.beginPath();
+							ctx.moveTo(this.xCenter, this.yCenter);
+							ctx.lineTo(outerPosition.x, outerPosition.y);
+							ctx.stroke();
+							ctx.closePath();
+						}
+						// Extra 3px out for some label spacing
+						var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
+						ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+						ctx.fillStyle = this.pointLabelFontColor;
+
+						var labelsCount = this.labels.length,
+							halfLabelsCount = this.labels.length/2,
+							quarterLabelsCount = halfLabelsCount/2,
+							upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
+							exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
+						if (i === 0){
+							ctx.textAlign = 'center';
+						} else if(i === halfLabelsCount){
+							ctx.textAlign = 'center';
+						} else if (i < halfLabelsCount){
+							ctx.textAlign = 'left';
+						} else {
+							ctx.textAlign = 'right';
+						}
+
+						// Set the correct text baseline based on outer positioning
+						if (exactQuarter){
+							ctx.textBaseline = 'middle';
+						} else if (upperHalf){
+							ctx.textBaseline = 'bottom';
+						} else {
+							ctx.textBaseline = 'top';
+						}
+
+						ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
+					}
+				}
+			}
+		}
+	});
+
+	// Attach global event to resize each chart instance when the browser resizes
+	helpers.addEvent(window, "resize", (function(){
+		// Basic debounce of resize function so it doesn't hurt performance when resizing browser.
+		var timeout;
+		return function(){
+			clearTimeout(timeout);
+			timeout = setTimeout(function(){
+				each(Chart.instances,function(instance){
+					// If the responsive flag is set in the chart instance config
+					// Cascade the resize event down to the chart.
+					if (instance.options.responsive){
+						instance.resize(instance.render, true);
+					}
+				});
+			}, 50);
+		};
+	})());
+
+
+	if (amd) {
+		define(function(){
+			return Chart;
+		});
+	} else if (typeof module === 'object' && module.exports) {
+		module.exports = Chart;
+	}
+
+	root.Chart = Chart;
+
+	Chart.noConflict = function(){
+		root.Chart = previous;
+		return Chart;
+	};
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+	var defaultConfig = {
+		//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+		scaleBeginAtZero : true,
+
+		//Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - If there is a stroke on each bar
+		barShowStroke : true,
+
+		//Number - Pixel width of the bar stroke
+		barStrokeWidth : 2,
+
+		//Number - Spacing between each of the X value sets
+		barValueSpacing : 5,
+
+		//Number - Spacing between data sets within X values
+		barDatasetSpacing : 1,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Bar",
+		defaults : defaultConfig,
+		initialize:  function(data){
+
+			//Expose options as a scope variable here so we can access it in the ScaleClass
+			var options = this.options;
+
+			this.ScaleClass = Chart.Scale.extend({
+				offsetGridLines : true,
+				calculateBarX : function(datasetCount, datasetIndex, barIndex){
+					//Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
+					var xWidth = this.calculateBaseWidth(),
+						xAbsolute = this.calculateX(barIndex) - (xWidth/2),
+						barWidth = this.calculateBarWidth(datasetCount);
+
+					return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
+				},
+				calculateBaseWidth : function(){
+					return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
+				},
+				calculateBarWidth : function(datasetCount){
+					//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
+					var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
+
+					return (baseWidth / datasetCount);
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
+
+					this.eachBars(function(bar){
+						bar.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activeBars, function(activeBar){
+						activeBar.fillColor = activeBar.highlightFill;
+						activeBar.strokeColor = activeBar.highlightStroke;
+					});
+					this.showTooltip(activeBars);
+				});
+			}
+
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.BarClass = Chart.Rectangle.extend({
+				strokeWidth : this.options.barStrokeWidth,
+				showStroke : this.options.barShowStroke,
+				ctx : this.chart.ctx
+			});
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset,datasetIndex){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					bars : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.bars.push(new this.BarClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.strokeColor,
+						fillColor : dataset.fillColor,
+						highlightFill : dataset.highlightFill || dataset.fillColor,
+						highlightStroke : dataset.highlightStroke || dataset.strokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.buildScale(data.labels);
+
+			this.BarClass.prototype.base = this.scale.endPoint;
+
+			this.eachBars(function(bar, index, datasetIndex){
+				helpers.extend(bar, {
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+					y: this.scale.endPoint
+				});
+				bar.save();
+			}, this);
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+
+			this.eachBars(function(bar){
+				bar.save();
+			});
+			this.render();
+		},
+		eachBars : function(callback){
+			helpers.each(this.datasets,function(dataset, datasetIndex){
+				helpers.each(dataset.bars, callback, this, datasetIndex);
+			},this);
+		},
+		getBarsAtEvent : function(e){
+			var barsArray = [],
+				eventPosition = helpers.getRelativePosition(e),
+				datasetIterator = function(dataset){
+					barsArray.push(dataset.bars[barIndex]);
+				},
+				barIndex;
+
+			for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
+				for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
+					if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
+						helpers.each(this.datasets, datasetIterator);
+						return barsArray;
+					}
+				}
+			}
+
+			return barsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachBars(function(bar){
+					values.push(bar.value);
+				});
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange: function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+			this.scale = new this.ScaleClass(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].bars.push(new this.BarClass({
+					value : value,
+					label : label,
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					base : this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].strokeColor,
+					fillColor : this.datasets[datasetIndex].fillColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.bars.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			helpers.extend(this.BarClass.prototype,{
+				y: this.scale.endPoint,
+				base : this.scale.endPoint
+			});
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			this.scale.draw(easingDecimal);
+
+			//Draw all the bars for each dataset
+			helpers.each(this.datasets,function(dataset,datasetIndex){
+				helpers.each(dataset.bars,function(bar,index){
+					if (bar.hasValue()){
+						bar.base = this.scale.endPoint;
+						//Transition then draw
+						bar.transition({
+							x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+							y : this.scale.calculateY(bar.value),
+							width : this.scale.calculateBarWidth(this.datasets.length)
+						}, easingDecimal).draw();
+					}
+				},this);
+
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Whether we should show a stroke on each segment
+		segmentShowStroke : true,
+
+		//String - The colour of each segment stroke
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of each segment stroke
+		segmentStrokeWidth : 2,
+
+		//The percentage of the chart that we cut out of the middle.
+		percentageInnerCutout : 50,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether we animate the rotation of the Doughnut
+		animateRotate : true,
+
+		//Boolean - Whether we animate scaling the Doughnut from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "Doughnut",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+
+			//Declare segments as a static property to prevent inheriting across the Chart type prototype
+			this.segments = [];
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+
+			this.SegmentArc = Chart.Arc.extend({
+				ctx : this.chart.ctx,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+			this.calculateTotal(data);
+
+			helpers.each(data,function(datapoint, index){
+				this.addData(datapoint, index, true);
+			},this);
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex || this.segments.length;
+			this.segments.splice(index, 0, new this.SegmentArc({
+				value : segment.value,
+				outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
+				innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
+				fillColor : segment.color,
+				highlightColor : segment.highlight || segment.color,
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				startAngle : Math.PI * 1.5,
+				circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
+				label : segment.label
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		calculateCircumference : function(value){
+			return (Math.PI*2)*(Math.abs(value) / this.total);
+		},
+		calculateTotal : function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += Math.abs(segment.value);
+			},this);
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor']);
+			});
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			this.render();
+		},
+
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				});
+			}, this);
+		},
+		draw : function(easeDecimal){
+			var animDecimal = (easeDecimal) ? easeDecimal : 1;
+			this.clear();
+			helpers.each(this.segments,function(segment,index){
+				segment.transition({
+					circumference : this.calculateCircumference(segment.value),
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				},animDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				segment.draw();
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length-1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+			},this);
+
+		}
+	});
+
+	Chart.types.Doughnut.extend({
+		name : "Pie",
+		defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
+	});
+
+}).call(this);
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+
+		///Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - Whether the line is curved between points
+		bezierCurve : true,
+
+		//Number - Tension of the bezier curve between points
+		bezierCurveTension : 0.4,
+
+		//Boolean - Whether to show a dot for each point
+		pointDot : true,
+
+		//Number - Radius of each point dot in pixels
+		pointDotRadius : 4,
+
+		//Number - Pixel width of point dot stroke
+		pointDotStrokeWidth : 1,
+
+		//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+		pointHitDetectionRadius : 20,
+
+		//Boolean - Whether to show a stroke for datasets
+		datasetStroke : true,
+
+		//Number - Pixel width of dataset stroke
+		datasetStrokeWidth : 2,
+
+		//Boolean - Whether to fill the dataset with a colour
+		datasetFill : true,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Line",
+		defaults : defaultConfig,
+		initialize:  function(data){
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.PointClass = Chart.Point.extend({
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx,
+				inRange : function(mouseX){
+					return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePoints, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+					this.showTooltip(activePoints);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+				this.buildScale(data.labels);
+
+
+				this.eachPoints(function(point, index){
+					helpers.extend(point, {
+						x: this.scale.calculateX(index),
+						y: this.scale.endPoint
+					});
+					point.save();
+				}, this);
+
+			},this);
+
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+		getPointsAtEvent : function(e){
+			var pointsArray = [],
+				eventPosition = helpers.getRelativePosition(e);
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,function(point){
+					if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
+				});
+			},this);
+			return pointsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachPoints(function(point){
+					values.push(point.value);
+				});
+
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange : function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+
+			this.scale = new Chart.Scale(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					x: this.scale.calculateX(this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			// Some helper methods for getting the next/prev points
+			var hasValue = function(item){
+				return item.value !== null;
+			},
+			nextPoint = function(point, collection, index){
+				return helpers.findNextWhere(collection, hasValue, index) || point;
+			},
+			previousPoint = function(point, collection, index){
+				return helpers.findPreviousWhere(collection, hasValue, index) || point;
+			};
+
+			this.scale.draw(easingDecimal);
+
+
+			helpers.each(this.datasets,function(dataset){
+				var pointsWithValues = helpers.where(dataset.points, hasValue);
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				//We can use this extra loop to calculate the control points of this dataset also in this loop
+
+				helpers.each(dataset.points, function(point, index){
+					if (point.hasValue()){
+						point.transition({
+							y : this.scale.calculateY(point.value),
+							x : this.scale.calculateX(index)
+						}, easingDecimal);
+					}
+				},this);
+
+
+				// Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point
+				// This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
+				if (this.options.bezierCurve){
+					helpers.each(pointsWithValues, function(point, index){
+						var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
+						point.controlPoints = helpers.splineCurve(
+							previousPoint(point, pointsWithValues, index),
+							point,
+							nextPoint(point, pointsWithValues, index),
+							tension
+						);
+
+						// Prevent the bezier going outside of the bounds of the graph
+
+						// Cap puter bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.outer.y > this.scale.endPoint){
+							point.controlPoints.outer.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.outer.y < this.scale.startPoint){
+							point.controlPoints.outer.y = this.scale.startPoint;
+						}
+
+						// Cap inner bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.inner.y > this.scale.endPoint){
+							point.controlPoints.inner.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.inner.y < this.scale.startPoint){
+							point.controlPoints.inner.y = this.scale.startPoint;
+						}
+					},this);
+				}
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+
+				helpers.each(pointsWithValues, function(point, index){
+					if (index === 0){
+						ctx.moveTo(point.x, point.y);
+					}
+					else{
+						if(this.options.bezierCurve){
+							var previous = previousPoint(point, pointsWithValues, index);
+
+							ctx.bezierCurveTo(
+								previous.controlPoints.outer.x,
+								previous.controlPoints.outer.y,
+								point.controlPoints.inner.x,
+								point.controlPoints.inner.y,
+								point.x,
+								point.y
+							);
+						}
+						else{
+							ctx.lineTo(point.x,point.y);
+						}
+					}
+				}, this);
+
+				ctx.stroke();
+
+				if (this.options.datasetFill && pointsWithValues.length > 0){
+					//Round off the line by going to the base of the chart, back to the start, then fill.
+					ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
+					ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
+					ctx.fillStyle = dataset.fillColor;
+					ctx.closePath();
+					ctx.fill();
+				}
+
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(pointsWithValues,function(point){
+					point.draw();
+				});
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Show a backdrop to the scale label
+		scaleShowLabelBackdrop : true,
+
+		//String - The colour of the label backdrop
+		scaleBackdropColor : "rgba(255,255,255,0.75)",
+
+		// Boolean - Whether the scale should begin at zero
+		scaleBeginAtZero : true,
+
+		//Number - The backdrop padding above & below the label in pixels
+		scaleBackdropPaddingY : 2,
+
+		//Number - The backdrop padding to the side of the label in pixels
+		scaleBackdropPaddingX : 2,
+
+		//Boolean - Show line for each value in the scale
+		scaleShowLine : true,
+
+		//Boolean - Stroke a line around each segment in the chart
+		segmentShowStroke : true,
+
+		//String - The colour of the stroke on each segement.
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of the stroke value in pixels
+		segmentStrokeWidth : 2,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect.
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether to animate the rotation of the chart
+		animateRotate : true,
+
+		//Boolean - Whether to animate scaling the chart from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
+	};
+
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "PolarArea",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+			this.segments = [];
+			//Declare segment class as a chart instance specific class, so it can share props for this instance
+			this.SegmentArc = Chart.Arc.extend({
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				ctx : this.chart.ctx,
+				innerRadius : 0,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				lineArc: true,
+				width: this.chart.width,
+				height: this.chart.height,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				valuesCount: data.length
+			});
+
+			this.updateScaleRange(data);
+
+			this.scale.update();
+
+			helpers.each(data,function(segment,index){
+				this.addData(segment,index,true);
+			},this);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex || this.segments.length;
+
+			this.segments.splice(index, 0, new this.SegmentArc({
+				fillColor: segment.color,
+				highlightColor: segment.highlight || segment.color,
+				label: segment.label,
+				value: segment.value,
+				outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
+				circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
+				startAngle: Math.PI * 1.5
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+		calculateTotal: function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += segment.value;
+			},this);
+			this.scale.valuesCount = this.segments.length;
+		},
+		updateScaleRange: function(datapoints){
+			var valuesArray = [];
+			helpers.each(datapoints,function(segment){
+				valuesArray.push(segment.value);
+			});
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes,
+				{
+					size: helpers.min([this.chart.width, this.chart.height]),
+					xCenter: this.chart.width/2,
+					yCenter: this.chart.height/2
+				}
+			);
+
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			
+			this.reflow();
+			this.render();
+		},
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.updateScaleRange(this.segments);
+			this.scale.update();
+
+			helpers.extend(this.scale,{
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				});
+			}, this);
+
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			//Clear & draw the canvas
+			this.clear();
+			helpers.each(this.segments,function(segment, index){
+				segment.transition({
+					circumference : this.scale.getCircumference(),
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				},easingDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				// If we've removed the first segment we need to set the first one to
+				// start at the top.
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length - 1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+				segment.draw();
+			}, this);
+			this.scale.draw();
+		}
+	});
+
+}).call(this);
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+
+	Chart.Type.extend({
+		name: "Radar",
+		defaults:{
+			//Boolean - Whether to show lines for each scale point
+			scaleShowLine : true,
+
+			//Boolean - Whether we show the angle lines out of the radar
+			angleShowLineOut : true,
+
+			//Boolean - Whether to show labels on the scale
+			scaleShowLabels : false,
+
+			// Boolean - Whether the scale should begin at zero
+			scaleBeginAtZero : true,
+
+			//String - Colour of the angle line
+			angleLineColor : "rgba(0,0,0,.1)",
+
+			//Number - Pixel width of the angle line
+			angleLineWidth : 1,
+
+			//String - Point label font declaration
+			pointLabelFontFamily : "'Arial'",
+
+			//String - Point label font weight
+			pointLabelFontStyle : "normal",
+
+			//Number - Point label font size in pixels
+			pointLabelFontSize : 10,
+
+			//String - Point label font colour
+			pointLabelFontColor : "#666",
+
+			//Boolean - Whether to show a dot for each point
+			pointDot : true,
+
+			//Number - Radius of each point dot in pixels
+			pointDotRadius : 3,
+
+			//Number - Pixel width of point dot stroke
+			pointDotStrokeWidth : 1,
+
+			//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+			pointHitDetectionRadius : 20,
+
+			//Boolean - Whether to show a stroke for datasets
+			datasetStroke : true,
+
+			//Number - Pixel width of dataset stroke
+			datasetStrokeWidth : 2,
+
+			//Boolean - Whether to fill the dataset with a colour
+			datasetFill : true,
+
+			//String - A legend template
+			legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+
+		},
+
+		initialize: function(data){
+			this.PointClass = Chart.Point.extend({
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx
+			});
+
+			this.datasets = [];
+
+			this.buildScale(data);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePointsCollection, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+
+					this.showTooltip(activePointsCollection);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label: dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					var pointPosition;
+					if (!this.scale.animation){
+						pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
+					}
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
+						y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+
+		getPointsAtEvent : function(evt){
+			var mousePosition = helpers.getRelativePosition(evt),
+				fromCenter = helpers.getAngleFromPoint({
+					x: this.scale.xCenter,
+					y: this.scale.yCenter
+				}, mousePosition);
+
+			var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
+				pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
+				activePointsCollection = [];
+
+			// If we're at the top, make the pointIndex 0 to get the first of the array.
+			if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
+				pointIndex = 0;
+			}
+
+			if (fromCenter.distance <= this.scale.drawingArea){
+				helpers.each(this.datasets, function(dataset){
+					activePointsCollection.push(dataset.points[pointIndex]);
+				});
+			}
+
+			return activePointsCollection;
+		},
+
+		buildScale : function(data){
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				angleLineColor : this.options.angleLineColor,
+				angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
+				// Point labels at the edge of each line
+				pointLabelFontColor : this.options.pointLabelFontColor,
+				pointLabelFontSize : this.options.pointLabelFontSize,
+				pointLabelFontFamily : this.options.pointLabelFontFamily,
+				pointLabelFontStyle : this.options.pointLabelFontStyle,
+				height : this.chart.height,
+				width: this.chart.width,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				labels: data.labels,
+				valuesCount: data.datasets[0].data.length
+			});
+
+			this.scale.setScaleSize();
+			this.updateScaleRange(data.datasets);
+			this.scale.buildYLabels();
+		},
+		updateScaleRange: function(datasets){
+			var valuesArray = (function(){
+				var totalDataArray = [];
+				helpers.each(datasets,function(dataset){
+					if (dataset.data){
+						totalDataArray = totalDataArray.concat(dataset.data);
+					}
+					else {
+						helpers.each(dataset.points, function(point){
+							totalDataArray.push(point.value);
+						});
+					}
+				});
+				return totalDataArray;
+			})();
+
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes
+			);
+
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			this.scale.valuesCount++;
+			helpers.each(valuesArray,function(value,datasetIndex){
+				var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					x: pointPosition.x,
+					y: pointPosition.y,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.labels.push(label);
+
+			this.reflow();
+
+			this.update();
+		},
+		removeData : function(){
+			this.scale.valuesCount--;
+			this.scale.labels.shift();
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.reflow();
+			this.update();
+		},
+		update : function(){
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.reflow();
+			this.render();
+		},
+		reflow: function(){
+			helpers.extend(this.scale, {
+				width : this.chart.width,
+				height: this.chart.height,
+				size : helpers.min([this.chart.width, this.chart.height]),
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+			this.updateScaleRange(this.datasets);
+			this.scale.setScaleSize();
+			this.scale.buildYLabels();
+		},
+		draw : function(ease){
+			var easeDecimal = ease || 1,
+				ctx = this.chart.ctx;
+			this.clear();
+			this.scale.draw();
+
+			helpers.each(this.datasets,function(dataset){
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				helpers.each(dataset.points,function(point,index){
+					if (point.hasValue()){
+						point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
+					}
+				},this);
+
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+				helpers.each(dataset.points,function(point,index){
+					if (index === 0){
+						ctx.moveTo(point.x,point.y);
+					}
+					else{
+						ctx.lineTo(point.x,point.y);
+					}
+				},this);
+				ctx.closePath();
+				ctx.stroke();
+
+				ctx.fillStyle = dataset.fillColor;
+				ctx.fill();
+
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(dataset.points,function(point){
+					if (point.hasValue()){
+						point.draw();
+					}
+				});
+
+			},this);
+
+		}
+
+	});
+
+
+
+
+
+}).call(this);

+ 43 - 3
web/static/js/datatables.min.js

@@ -4,10 +4,10 @@
  *
  * To rebuild or modify this file with the latest versions of the included
  * software please visit:
- *   https://datatables.net/download/#bs4/jq-3.3.1/dt-1.10.18
+ *   https://datatables.net/download/#bs4/jq-3.3.1/dt-1.10.18/r-2.2.2
  *
  * Included libraries:
- *   jQuery 3 3.3.1, DataTables 1.10.18
+ *   jQuery 3 3.3.1, DataTables 1.10.18, Responsive 2.2.2
  */
 
 /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
@@ -164,7 +164,7 @@ Z(n.defaults.column);n.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender
 aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",
 iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==y(this)?1*this._iRecordsTotal:
 this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==y(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};n.ext=x={buttons:{},
-classes:{},build:"bs4/jq-3.3.1/dt-1.10.18",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});
+classes:{},build:"bs4/jq-3.3.1/dt-1.10.18/r-2.2.2",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:n.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:n.version};h.extend(x,{afnFiltering:x.search,aTypes:x.type.detect,ofnSearch:x.type.search,oSort:x.type.order,afnSortData:x.order,aoFeatures:x.feature,oApi:x.internal,oStdClasses:x.classes,oPagination:x.pager});
 h.extend(n.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",
 sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",
 sJUIHeader:"",sJUIFooter:""});var Kb=n.ext.pager;h.extend(Kb,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(a,b){return[ia(a,b)]},simple_numbers:function(a,b){return["previous",ia(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ia(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ia(a,b),"last"]},_numbers:ia,numbers_length:7});h.extend(!0,n.ext.renderer,{pageButton:{_:function(a,b,c,d,e,
@@ -192,3 +192,43 @@ renderer:"bootstrap"});b.extend(f.ext.classes,{sWrapper:"dataTables_wrapper dt-b
 {"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("<a>",{href:"#","aria-controls":a.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:a.iTabIndex,"class":"page-link"}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(v){}q(b(h).empty().html('<ul class="pagination"/>').children("ul"),s);i!==m&&b(h).find("[data-dt-idx="+i+"]").focus()};return f});
 
 
+/*!
+ Responsive 2.2.2
+ 2014-2018 SpryMedia Ltd - datatables.net/license
+*/
+(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(l){return d(l,window,document)}):"object"===typeof exports?module.exports=function(l,j){l||(l=window);if(!j||!j.fn.dataTable)j=require("datatables.net")(l,j).$;return d(j,l,l.document)}:d(jQuery,window,document)})(function(d,l,j,q){function t(a,b,c){var e=b+"-"+c;if(n[e])return n[e];for(var d=[],a=a.cell(b,c).node().childNodes,b=0,c=a.length;b<c;b++)d.push(a[b]);return n[e]=d}function r(a,b,d){var e=b+
+"-"+d;if(n[e]){for(var a=a.cell(b,d).node(),d=n[e][0].parentNode.childNodes,b=[],f=0,g=d.length;f<g;f++)b.push(d[f]);d=0;for(f=b.length;d<f;d++)a.appendChild(b[d]);n[e]=q}}var o=d.fn.dataTable,i=function(a,b){if(!o.versionCheck||!o.versionCheck("1.10.10"))throw"DataTables Responsive requires DataTables 1.10.10 or newer";this.s={dt:new o.Api(a),columns:[],current:[]};this.s.dt.settings()[0].responsive||(b&&"string"===typeof b.details?b.details={type:b.details}:b&&!1===b.details?b.details={type:!1}:
+b&&!0===b.details&&(b.details={type:"inline"}),this.c=d.extend(!0,{},i.defaults,o.defaults.responsive,b),a.responsive=this,this._constructor())};d.extend(i.prototype,{_constructor:function(){var a=this,b=this.s.dt,c=b.settings()[0],e=d(l).width();b.settings()[0]._responsive=this;d(l).on("resize.dtr orientationchange.dtr",o.util.throttle(function(){var b=d(l).width();b!==e&&(a._resize(),e=b)}));c.oApi._fnCallbackReg(c,"aoRowCreatedCallback",function(e){-1!==d.inArray(!1,a.s.current)&&d(">td, >th",
+e).each(function(e){e=b.column.index("toData",e);!1===a.s.current[e]&&d(this).css("display","none")})});b.on("destroy.dtr",function(){b.off(".dtr");d(b.table().body()).off(".dtr");d(l).off("resize.dtr orientationchange.dtr");d.each(a.s.current,function(b,e){!1===e&&a._setColumnVis(b,!0)})});this.c.breakpoints.sort(function(a,b){return a.width<b.width?1:a.width>b.width?-1:0});this._classLogic();this._resizeAuto();c=this.c.details;!1!==c.type&&(a._detailsInit(),b.on("column-visibility.dtr",function(){a._timer&&
+clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize();a._redrawChildren()},100)}),b.on("draw.dtr",function(){a._redrawChildren()}),d(b.table().node()).addClass("dtr-"+c.type));b.on("column-reorder.dtr",function(){a._classLogic();a._resizeAuto();a._resize()});b.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});b.on("preXhr.dtr",function(){var e=[];b.rows().every(function(){this.child.isShown()&&e.push(this.id(true))});b.one("draw.dtr",
+function(){a._resizeAuto();a._resize();b.rows(e).every(function(){a._detailsDisplay(this,false)})})});b.on("init.dtr",function(){a._resizeAuto();a._resize();d.inArray(false,a.s.current)&&b.columns.adjust()});this._resize()},_columnsVisiblity:function(a){var b=this.s.dt,c=this.s.columns,e,f,g=c.map(function(a,b){return{columnIdx:b,priority:a.priority}}).sort(function(a,b){return a.priority!==b.priority?a.priority-b.priority:a.columnIdx-b.columnIdx}),h=d.map(c,function(e,c){return!1===b.column(c).visible()?
+"not-visible":e.auto&&null===e.minWidth?!1:!0===e.auto?"-":-1!==d.inArray(a,e.includeIn)}),m=0;e=0;for(f=h.length;e<f;e++)!0===h[e]&&(m+=c[e].minWidth);e=b.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;m=b.table().container().offsetWidth-e-m;e=0;for(f=h.length;e<f;e++)c[e].control&&(m-=c[e].minWidth);var s=!1;e=0;for(f=g.length;e<f;e++){var k=g[e].columnIdx;"-"===h[k]&&(!c[k].control&&c[k].minWidth)&&(s||0>m-c[k].minWidth?(s=!0,h[k]=!1):h[k]=!0,m-=c[k].minWidth)}g=!1;e=0;for(f=c.length;e<f;e++)if(!c[e].control&&
+!c[e].never&&!1===h[e]){g=!0;break}e=0;for(f=c.length;e<f;e++)c[e].control&&(h[e]=g),"not-visible"===h[e]&&(h[e]=!1);-1===d.inArray(!0,h)&&(h[0]=!0);return h},_classLogic:function(){var a=this,b=this.c.breakpoints,c=this.s.dt,e=c.columns().eq(0).map(function(a){var b=this.column(a),e=b.header().className,a=c.settings()[0].aoColumns[a].responsivePriority;a===q&&(b=d(b.header()).data("priority"),a=b!==q?1*b:1E4);return{className:e,includeIn:[],auto:!1,control:!1,never:e.match(/\bnever\b/)?!0:!1,priority:a}}),
+f=function(a,b){var c=e[a].includeIn;-1===d.inArray(b,c)&&c.push(b)},g=function(d,c,g,k){if(g)if("max-"===g){k=a._find(c).width;c=0;for(g=b.length;c<g;c++)b[c].width<=k&&f(d,b[c].name)}else if("min-"===g){k=a._find(c).width;c=0;for(g=b.length;c<g;c++)b[c].width>=k&&f(d,b[c].name)}else{if("not-"===g){c=0;for(g=b.length;c<g;c++)-1===b[c].name.indexOf(k)&&f(d,b[c].name)}}else e[d].includeIn.push(c)};e.each(function(a,e){for(var c=a.className.split(" "),f=!1,i=0,l=c.length;i<l;i++){var j=d.trim(c[i]);
+if("all"===j){f=!0;a.includeIn=d.map(b,function(a){return a.name});return}if("none"===j||a.never){f=!0;return}if("control"===j){f=!0;a.control=!0;return}d.each(b,function(a,b){var d=b.name.split("-"),c=j.match(RegExp("(min\\-|max\\-|not\\-)?("+d[0]+")(\\-[_a-zA-Z0-9])?"));c&&(f=!0,c[2]===d[0]&&c[3]==="-"+d[1]?g(e,b.name,c[1],c[2]+c[3]):c[2]===d[0]&&!c[3]&&g(e,b.name,c[1],c[2]))})}f||(a.auto=!0)});this.s.columns=e},_detailsDisplay:function(a,b){var c=this,e=this.s.dt,f=this.c.details;if(f&&!1!==f.type){var g=
+f.display(a,b,function(){return f.renderer(e,a[0],c._detailsObj(a[0]))});(!0===g||!1===g)&&d(e.table().node()).triggerHandler("responsive-display.dt",[e,a,g,b])}},_detailsInit:function(){var a=this,b=this.s.dt,c=this.c.details;"inline"===c.type&&(c.target="td:first-child, th:first-child");b.on("draw.dtr",function(){a._tabIndexes()});a._tabIndexes();d(b.table().body()).on("keyup.dtr","td, th",function(a){a.keyCode===13&&d(this).data("dtr-keyboard")&&d(this).click()});var e=c.target;d(b.table().body()).on("click.dtr mousedown.dtr mouseup.dtr",
+"string"===typeof e?e:"td, th",function(c){if(d(b.table().node()).hasClass("collapsed")&&d.inArray(d(this).closest("tr").get(0),b.rows().nodes().toArray())!==-1){if(typeof e==="number"){var g=e<0?b.columns().eq(0).length+e:e;if(b.cell(this).index().column!==g)return}g=b.row(d(this).closest("tr"));c.type==="click"?a._detailsDisplay(g,false):c.type==="mousedown"?d(this).css("outline","none"):c.type==="mouseup"&&d(this).blur().css("outline","")}})},_detailsObj:function(a){var b=this,c=this.s.dt;return d.map(this.s.columns,
+function(e,d){if(!e.never&&!e.control)return{title:c.settings()[0].aoColumns[d].sTitle,data:c.cell(a,d).render(b.c.orthogonal),hidden:c.column(d).visible()&&!b.s.current[d],columnIndex:d,rowIndex:a}})},_find:function(a){for(var b=this.c.breakpoints,c=0,e=b.length;c<e;c++)if(b[c].name===a)return b[c]},_redrawChildren:function(){var a=this,b=this.s.dt;b.rows({page:"current"}).iterator("row",function(c,e){b.row(e);a._detailsDisplay(b.row(e),!0)})},_resize:function(){var a=this,b=this.s.dt,c=d(l).width(),
+e=this.c.breakpoints,f=e[0].name,g=this.s.columns,h,m=this.s.current.slice();for(h=e.length-1;0<=h;h--)if(c<=e[h].width){f=e[h].name;break}var i=this._columnsVisiblity(f);this.s.current=i;e=!1;h=0;for(c=g.length;h<c;h++)if(!1===i[h]&&!g[h].never&&!g[h].control&&!1===!b.column(h).visible()){e=!0;break}d(b.table().node()).toggleClass("collapsed",e);var k=!1,j=0;b.columns().eq(0).each(function(b,c){!0===i[c]&&j++;i[c]!==m[c]&&(k=!0,a._setColumnVis(b,i[c]))});k&&(this._redrawChildren(),d(b.table().node()).trigger("responsive-resize.dt",
+[b,this.s.current]),0===b.page.info().recordsDisplay&&d("td",b.table().body()).eq(0).attr("colspan",j))},_resizeAuto:function(){var a=this.s.dt,b=this.s.columns;if(this.c.auto&&-1!==d.inArray(!0,d.map(b,function(a){return a.auto}))){d.isEmptyObject(n)||d.each(n,function(b){b=b.split("-");r(a,1*b[0],1*b[1])});a.table().node();var c=a.table().node().cloneNode(!1),e=d(a.table().header().cloneNode(!1)).appendTo(c),f=d(a.table().body()).clone(!1,!1).empty().appendTo(c),g=a.columns().header().filter(function(b){return a.column(b).visible()}).to$().clone(!1).css("display",
+"table-cell").css("min-width",0);d(f).append(d(a.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(f=a.table().footer()){var f=d(f.cloneNode(!1)).appendTo(c),h=a.columns().footer().filter(function(b){return a.column(b).visible()}).to$().clone(!1).css("display","table-cell");d("<tr/>").append(h).appendTo(f)}d("<tr/>").append(g).appendTo(e);"inline"===this.c.details.type&&d(c).addClass("dtr-inline collapsed");d(c).find("[name]").removeAttr("name");d(c).css("position","relative");
+c=d("<div/>").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(c);c.insertBefore(a.table().node());g.each(function(c){c=a.column.index("fromVisible",c);b[c].minWidth=this.offsetWidth||0});c.remove()}},_setColumnVis:function(a,b){var c=this.s.dt,e=b?"":"none";d(c.column(a).header()).css("display",e);d(c.column(a).footer()).css("display",e);c.column(a).nodes().to$().css("display",e);d.isEmptyObject(n)||c.cells(null,a).indexes().each(function(a){r(c,a.row,a.column)})},_tabIndexes:function(){var a=
+this.s.dt,b=a.cells({page:"current"}).nodes().to$(),c=a.settings()[0],e=this.c.details.target;b.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");"number"===typeof e?a.cells(null,e,{page:"current"}).nodes().to$().attr("tabIndex",c.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===e&&(e=">td:first-child, >th:first-child"),d(e,a.rows({page:"current"}).nodes()).attr("tabIndex",c.iTabIndex).data("dtr-keyboard",1))}});i.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",
+width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];i.display={childRow:function(a,b,c){if(b){if(d(a.node()).hasClass("parent"))return a.child(c(),"child").show(),!0}else{if(a.child.isShown())return a.child(!1),d(a.node()).removeClass("parent"),!1;a.child(c(),"child").show();d(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,b,c){if(!b&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),d(a.node()).removeClass("parent"),
+!1;a.child(c(),"child").show();d(a.node()).addClass("parent");return!0},modal:function(a){return function(b,c,e){if(c)d("div.dtr-modal-content").empty().append(e());else{var f=function(){g.remove();d(j).off("keypress.dtr")},g=d('<div class="dtr-modal"/>').append(d('<div class="dtr-modal-display"/>').append(d('<div class="dtr-modal-content"/>').append(e())).append(d('<div class="dtr-modal-close">&times;</div>').click(function(){f()}))).append(d('<div class="dtr-modal-background"/>').click(function(){f()})).appendTo("body");
+d(j).on("keyup.dtr",function(a){27===a.keyCode&&(a.stopPropagation(),f())})}a&&a.header&&d("div.dtr-modal-content").prepend("<h2>"+a.header(b)+"</h2>")}}};var n={};i.renderer={listHiddenNodes:function(){return function(a,b,c){var e=d('<ul data-dtr-index="'+b+'" class="dtr-details"/>'),f=!1;d.each(c,function(b,c){c.hidden&&(d('<li data-dtr-index="'+c.columnIndex+'" data-dt-row="'+c.rowIndex+'" data-dt-column="'+c.columnIndex+'"><span class="dtr-title">'+c.title+"</span> </li>").append(d('<span class="dtr-data"/>').append(t(a,
+c.rowIndex,c.columnIndex))).appendTo(e),f=!0)});return f?e:!1}},listHidden:function(){return function(a,b,c){return(a=d.map(c,function(a){return a.hidden?'<li data-dtr-index="'+a.columnIndex+'" data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><span class="dtr-title">'+a.title+'</span> <span class="dtr-data">'+a.data+"</span></li>":""}).join(""))?d('<ul data-dtr-index="'+b+'" class="dtr-details"/>').append(a):!1}},tableAll:function(a){a=d.extend({tableClass:""},a);return function(b,
+c,e){b=d.map(e,function(a){return'<tr data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><td>'+a.title+":</td> <td>"+a.data+"</td></tr>"}).join("");return d('<table class="'+a.tableClass+' dtr-details" width="100%"/>').append(b)}}};i.defaults={breakpoints:i.breakpoints,auto:!0,details:{display:i.display.childRow,renderer:i.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var p=d.fn.dataTable.Api;p.register("responsive()",function(){return this});p.register("responsive.index()",
+function(a){a=d(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});p.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});p.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});p.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==d.inArray(!1,a._responsive.s.current):
+!1});p.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,b){return a._responsive?a._responsive.s.current[b]:!1},1)});i.version="2.2.2";d.fn.dataTable.Responsive=i;d.fn.DataTable.Responsive=i;d(j).on("preInit.dt.dtr",function(a,b){if("dt"===a.namespace&&(d(b.nTable).hasClass("responsive")||d(b.nTable).hasClass("dt-responsive")||b.oInit.responsive||o.defaults.responsive)){var c=b.oInit.responsive;!1!==c&&new i(b,d.isPlainObject(c)?
+c:{})}});return i});
+
+
+/*!
+ Bootstrap 4 integration for DataTables' Responsive
+ ©2016 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-responsive"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-bs4")(a,b).$;b.fn.dataTable.Responsive||require("datatables.net-responsive")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable,b=a.Responsive.display,g=b.modal,e=c('<div class="modal fade dtr-bs-modal" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"/></div></div></div>');
+b.modal=function(a){return function(b,d,f){if(c.fn.modal){if(!d){if(a&&a.header){var d=e.find("div.modal-header"),h=d.find("button").detach();d.empty().append('<h4 class="modal-title">'+a.header(b)+"</h4>").append(h)}e.find("div.modal-body").empty().append(f());e.appendTo("body").modal()}}else g(b,d,f)}};return a.Responsive});
+
+

+ 40 - 19
web/static/js/main.js

@@ -1,27 +1,48 @@
 (function () {
-	"use strict";
+    "use strict";
 
-	var treeviewMenu = $('.app-menu');
+    var treeviewMenu = $('.app-menu');
 
-	// Toggle Sidebar
-	$('[data-toggle="sidebar"]').click(function(event) {
-		event.preventDefault();
-		$('.app').toggleClass('sidenav-toggled');
-	});
+    // Toggle Sidebar
+    $('[data-toggle="sidebar"]').click(function (event) {
+        event.preventDefault();
+        $('.app').toggleClass('sidenav-toggled');
+    });
 
-	// Activate sidebar treeview toggle
-	$("[data-toggle='treeview']").click(function(event) {
-		event.preventDefault();
-		if(!$(this).parent().hasClass('is-expanded')) {
-			treeviewMenu.find("[data-toggle='treeview']").parent().removeClass('is-expanded');
-		}
-		$(this).parent().toggleClass('is-expanded');
-	});
+    // Activate sidebar treeview toggle
+    $("[data-toggle='treeview']").click(function (event) {
+        event.preventDefault();
+        if (!$(this).parent().hasClass('is-expanded')) {
+            treeviewMenu.find("[data-toggle='treeview']").parent().removeClass('is-expanded');
+        }
+        $(this).parent().toggleClass('is-expanded');
+    });
 
-	// Set initial active toggle
-	$("[data-toggle='treeview.'].is-expanded").parent().toggleClass('is-expanded');
+    // Set initial active toggle
+    $("[data-toggle='treeview.'].is-expanded").parent().toggleClass('is-expanded');
 
-	//Activate bootstrip tooltips
-	// $("[data-toggle='tooltip']").tooltip();
+    //Activate bootstrip tooltips
+    // $("[data-toggle='tooltip']").tooltip();
 
 })();
+
+function change(limit) {
+    var size = "";
+    if (limit < 0.1 * 1024) {
+        size = limit.toFixed(2) + "B"
+    } else if (limit < 0.1 * 1024 * 1024) {
+        size = (limit / 1024).toFixed(2) + "KB"
+    } else if (limit < 0.1 * 1024 * 1024 * 1024) {
+        size = (limit / (1024 * 1024)).toFixed(2) + "MB"
+    } else {
+        size = (limit / (1024 * 1024 * 1024)).toFixed(2) + "GB"
+    }
+
+    var sizeStr = size + "";
+    var index = sizeStr.indexOf(".");
+    var dou = sizeStr.substr(index + 1, 2)
+    if (dou == "00") {
+        return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
+    }
+    return size;
+}

+ 10 - 0
web/static/page/error.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>easyProxy error</title>
+</head>
+<body>
+404 not found,power by easyProxy
+</body>
+</html>

+ 67 - 0
web/views/client/add.html

@@ -0,0 +1,67 @@
+<div class="row tile">
+    <div class="col-md-6 col-md-auto">
+        <div>
+            <h3 class="tile-title">添加</h3>
+            <div class="tile-body">
+                <form>
+                    <div class="form-group" id="target">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" type="text" name="Remark" placeholder="客户端备注">
+                    </div>
+                    <div class="form-group" id="u">
+                        <label class="control-label">验证用户名(仅socks5,web穿透支持)</label>
+                        <input class="form-control" type="text" name="u" placeholder="不填则无需验证">
+                    </div>
+                    <div class="form-group" id="p">
+                        <label class="control-label">验证密码(仅socks5,web穿透支持)</label>
+                        <input class="form-control" type="text" name="p" placeholder="不填则无需验证">
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">数据压缩方式</label>
+                        <select class="form-control" name="compress">
+                            <option value="">不压缩</option>
+                            <option value="snappy">snappy</option>
+                        </select>
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">是否加密传输</label>
+                        <select class="form-control" name="crypt">
+                            <option value="0">不加密</option>
+                            <option value="1">加密</option>
+                        </select>
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">是否TCP复用</label>
+                        <select class="form-control" name="crypt">
+                            <option value="0">不启用</option>
+                            <option value="1">启用</option>
+                        </select>
+                    </div>
+                </form>
+            </div>
+            <div class="tile-footer">
+                &nbsp;&nbsp;<button class="btn btn-success" href="#" id="add"><i
+                    class="fa fa-fw fa-lg fa-eye"></i>添加
+            </button>
+            </div>
+        </div>
+    </div>
+</div>
+</main>
+<script>
+    $(function () {
+        $("#add").on("click", function () {
+            $.ajax({
+                type: "POST",
+                url: "/client/add",
+                data: $("form").serializeArray(),
+                success: function (res) {
+                    alert(res.msg)
+                    if (res.status) {
+                        history.back(-1)
+                    }
+                }
+            })
+        })
+    })
+</script>

+ 71 - 0
web/views/client/edit.html

@@ -0,0 +1,71 @@
+<div class="row tile">
+    <div class="col-md-6 col-md-auto">
+        <div>
+            <h3 class="tile-title">添加</h3>
+            <div class="tile-body">
+                <form>
+                    <input type="hidden" name="Id" value="{{.c.Id}}">
+                    <div class="form-group" id="target">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" value="{{.c.Remark}}" type="text" name="Remark" placeholder="客户端备注">
+                    </div>
+                    <div class="form-group" id="u">
+                        <label class="control-label">验证用户名(仅socks5,web穿透支持)</label>
+                        <input class="form-control" value="{{.c.Cnf.U}}" type="text" name="u"
+                               placeholder="不填则无需验证">
+                    </div>
+                    <div class="form-group" id="p">
+                        <label class="control-label">验证密码(仅socks5,web穿透支持)</label>
+                        <input class="form-control" value="{{.c.Cnf.P}}" type="text" name="p"
+                               placeholder="不填则无需验证">
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">数据压缩方式(所有模式均支持)</label>
+                        <select class="form-control" name="compress">
+                            <option {{if eq "" .c.Cnf.Compress}}selected{{end}} value="">不压缩</option>
+                            <option {{if eq "snappy" .c.Cnf.Compress}}selected{{end}} value="snappy">snappy压缩</option>
+                        </select>
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">是否加密传输(所有模式均支持)</label>
+                        <select class="form-control" name="crypt">
+                            <option {{if eq false .c.Cnf.Crypt}}selected{{end}} value="0">不加密</option>
+                            <option {{if eq  true .c.Cnf.Crypt}}selected{{end}} value="1">加密</option>
+                        </select>
+                    </div>
+                    <div class="form-group" id="compress">
+                        <label class="control-label">是否启用TCP复用(所有模式均支持)</label>
+                        <select class="form-control" name="mux">
+                            <option {{if eq false .c.Cnf.Mux}}selected{{end}} value="0">不启用</option>
+                            <option {{if eq  true .c.Cnf.Mux}}selected{{end}} value="1">启用</option>
+                        </select>
+                    </div>
+                </form>
+            </div>
+            <div class="tile-footer">
+                &nbsp;&nbsp;<button class="btn btn-success" href="#" id="add"><i
+                    class="fa fa-fw fa-lg fa-eye"></i>保存
+            </button>
+            </div>
+        </div>
+    </div>
+</div>
+</main>
+<script>
+
+    $(function () {
+        $("#add").on("click", function () {
+            $.ajax({
+                type: "POST",
+                url: "/client/edit",
+                data: $("form").serializeArray(),
+                success: function (res) {
+                    alert(res.msg)
+                    if (res.status) {
+                        history.back(-1)
+                    }
+                }
+            })
+        })
+    })
+</script>

+ 234 - 0
web/views/client/list.html

@@ -0,0 +1,234 @@
+<div class="row">
+    <div class="col-md-12">
+        <div class="tile">
+            <div class="tile-body">
+                <table class="table table-hover table-bordered" id="sampleTable">
+                    <thead>
+                    <tr>
+                        <th>Id</th>
+                        <th>验证密钥</th>
+                        <th>客户端地址</th>
+                        <th>备注</th>
+                        <th>压缩方式</th>
+                        <th>加密</th>
+                        <th>多路复用</th>
+                        <th>用户名</th>
+                        <th>密码</th>
+                        <th>状态</th>
+                        <th>客户端连接状态</th>
+                        <th>出口流量</th>
+                        <th>入口流量</th>
+                        <th>操作</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+</main>
+
+<script type="text/javascript">
+    function del(id) {
+        if (confirm("确定要删除数据吗?")) {
+            $.ajax({
+                type: "POST",
+                url: "/client/del",
+                data: {"id": id},
+                success: function (res) {
+                    alert(res.msg)
+                    if (res.status) {
+                        document.location.reload();
+                    }
+                }
+            })
+        }
+    }
+
+    function start(id) {
+        if (confirm("确定要开始任务吗?")) {
+            $.ajax({
+                type: "POST",
+                url: "/client/changestatus",
+                data: {"id": id, "status": 1},
+                success: function (res) {
+                    alert(res.msg)
+                    if (res.status) {
+                        document.location.reload();
+                    }
+                }
+            })
+        }
+    }
+
+    function stop(id) {
+        if (confirm("确定要暂停吗?")) {
+            $.ajax({
+                type: "POST",
+                url: "/client/changestatus",
+                data: {
+                    "id": id, "status": 0
+                },
+                success:
+
+                        function (res) {
+                            alert(res.msg)
+                            if (res.status) {
+                                document.location.reload();
+                            }
+                        }
+            })
+        }
+    }
+
+    function edit(id) {
+        window.location.href = "/client/edit?id=" + id
+    }
+
+    function add() {
+        window.location.href = "/client/add"
+    }
+
+    function show_tunnel_list(id) {
+        window.location.href = "/index/all?client_id=" + id
+    }
+
+    function show_host_list(id) {
+        window.location.href = "/index/hostlist?client_id=" + id
+    }
+
+    $(document)
+
+            .ready(function () {
+                var table = $('#sampleTable').DataTable({
+                    responsive: {
+                        details: {
+                            display: $.fn.dataTable.Responsive.display.childRowImmediate
+                        }
+                    },
+                    dom: 'Bfrtip',
+                    processing: true,
+                    serverSide: true,
+                    autoWidth: false,
+                    ordering: false,
+                    ajax: {
+                        url: '/client/client',
+                        type: 'POST'
+                    },
+                    dom: '<"top"fl><"toolbar">rt<"bottom"ip><"clear">',
+                    columns: [ //这个是显示到界面上的个数据 格式为 {data:'显示的字段名'}
+                        {data: 'Id'},
+                        {data: 'VerifyKey'},
+                        {data: "Addr"},
+                        {data: "Remark"},
+                        {data: "Addr"},
+                        {data: "Addr"},
+                        {data: "Addr"},
+                        {data: "Addr"},
+                        {data: "Remark"},
+                        {data: "Status"},
+                        {data: "IsConnect"},
+                        {data: "Addr"},
+                        {data: "Addr"},
+                        {data: ""},
+                    ],
+                    bFilter: false,
+                    columnDefs: [{
+                        targets: -1,
+                        render: function (data, type, row, meta) {
+                            if (row.Status == true) {
+                                btn = "<button onclick=\"stop('" + row.Id + "')\" class=\"btn btn-secondary btn-sm\" type=\"button\">关闭</button>"
+                            } else {
+                                btn = "<button onclick=\"start('" + row.Id + "')\" class=\"btn btn-success btn-sm\" type=\"button\">打开</button>"
+                            }
+                            btn_del = '<button onclick="del(\'' + row.Id + '\')"  class="btn btn-danger btn-sm">删除</button> '
+                            btn_edit = '<button onclick="edit(\'' + row.Id + '\')"  class="btn btn-primary btn-sm">查看编辑</button> '
+                            btn_list = '<button onclick="show_tunnel_list(\'' + row.Id + '\')"  class="btn btn-info btn-sm">隧道</button> '
+                            btn_host = '<button onclick="show_host_list(\'' + row.Id + '\')"  class="btn btn-info btn-sm">域名</button> '
+                            return '<div class="btn-group" role="group" aria-label="..."> ' + btn + btn_del + btn_edit + btn_host + btn_list + '</div>'
+                        }
+                    },
+                        {
+                            targets: -5,
+                            render: function (data, type, row, meta) {
+                                if (data == false) {
+                                    return "<span class=\"badge badge-pill badge-secondary\">暂停</span>"
+                                } else {
+                                    return "<span class=\"badge badge-pill badge-success\">正常</span>"
+                                }
+                            }
+                        },
+                        {
+                            targets: -4,
+                            render: function (data, type, row, meta) {
+                                if (data == false) {
+                                    return "<span class=\"badge badge-pill badge-secondary\">未连接</span>"
+                                } else {
+                                    return "<span class=\"badge badge-pill badge-success\">已连接</span>"
+                                }
+                            }
+                        },
+                        {
+                            targets: -2,
+                            render: function (data, type, row, meta) {
+                                return change(row.Flow.InletFlow)
+                            }
+                        },
+                        {
+                            targets: -3,
+                            render: function (data, type, row, meta) {
+                                return change(row.Flow.ExportFlow)
+                            }
+                        },
+                        {
+                            targets: -6,
+                            render: function (data, type, row, meta) {
+                                return row.Cnf.P
+                            }
+                        },
+                        {
+                            targets: -7,
+                            render: function (data, type, row, meta) {
+                                return row.Cnf.U
+                            }
+                        },
+                        {
+                            targets: -8,
+                            render: function (data, type, row, meta) {
+                                if (row.Cnf.Mux == "0") {
+                                    return "不启用"
+                                } else {
+                                    return "启用"
+                                }
+                            }
+                        }
+                        ,
+                        {
+                            targets: -9,
+                            render: function (data, type, row, meta) {
+                                if (row.Cnf.Crypt == "0") {
+                                    return "不加密"
+                                } else {
+                                    return "加密"
+                                }
+                            }
+                        }
+                        ,
+                        {
+                            targets: -10,
+                            render: function (data, type, row, meta) {
+                                return row.Cnf.Compress
+                            }
+                        }
+                    ],
+
+                    buttons: []
+                });
+                $("#sampleTable_length").html('<button class="btn btn-primary" onclick="add()" type="button">新增</button>')
+            })
+    ;
+
+
+</script>

+ 38 - 5
web/views/index/add.html

@@ -19,9 +19,12 @@
                             <option {{if eq "udpServer" .type}}selected{{end}} value="udpServer">udp隧道</option>
                             <option {{if eq "socks5Server" .type}}selected{{end}} value="socks5Server">socks5代理</option>
                             <option {{if eq "httpProxyServer" .type}}selected{{end}} value="httpProxyServer">http代理
-                            <option {{if eq "hostServer" .type}}selected{{end}} value="hostServer">host客户端</option>
                         </select>
                     </div>
+                    <div class="form-group">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" type="text" name="remark" placeholder="备注">
+                    </div>
                     <div class="form-group" id="port">
                         <label class="control-label">监听的端口</label>
                         <input class="form-control" type="text" name="port" placeholder="公网服务器对外访问端口,例如8024">
@@ -30,6 +33,18 @@
                         <label class="control-label">内网目标(ip:端口)</label>
                         <input class="form-control" type="text" name="target" placeholder="内网代理地址,例如10.1.50.203:22">
                     </div>
+                    <div class="form-group" id="client_id">
+                        <label class="control-label">客户端ID</label>
+                        <input value="{{.client_id}}" class="form-control" type="text" name="client_id"
+                               placeholder="客户端id">
+                    </div>
+                    <div class="form-group" id="use_client">
+                        <label class="control-label">是否使用客户端配置</label>
+                        <select class="form-control" name="use_client">
+                            <option value="1">是</option>
+                            <option value="0">否</option>
+                        </select>
+                    </div>
                     <div class="form-group" id="compress">
                         <label class="control-label">数据压缩方式</label>
                         <select class="form-control" name="compress">
@@ -37,16 +52,16 @@
                             <option value="snappy">snappy</option>
                         </select>
                     </div>
-                    <div class="form-group" id="compress">
+                    <div class="form-group" id="crypt">
                         <label class="control-label">是否加密传输</label>
                         <select class="form-control" name="crypt">
                             <option value="0">不加密</option>
                             <option value="1">加密</option>
                         </select>
                     </div>
-                    <div class="form-group" id="compress">
+                    <div class="form-group" id="mux">
                         <label class="control-label">是否TCP复用</label>
-                        <select class="form-control" name="crypt">
+                        <select class="form-control" name="mux">
                             <option value="0">不启用</option>
                             <option value="1">启用</option>
                         </select>
@@ -77,7 +92,7 @@
     arr["udpServer"] = ["type", "port", "target", "compress", "udp隧道模式,提供一条udp隧道,适用于dns、内网dns访问等,添加后会自动生成一个客户端验证key<br>在内网机器执行<span style='color: red'>./easyProxy -vkey=生成的key -server=公网服务器ip:下面设定的端口</span><br>建立成功后,访问公网服务器的设定端口,则相当于访问内网目标地址的udp目标端口"]
     arr["socks5Server"] = ["type", "port", "compress", "u", "p", "socks5代理模式,内网socks5代理,配合proxifer,可如同使用vpn一样访问内网设备或资源,添加后会自动生成一个客户端验证key<br>在内网机器执行<span style='color: red'>./easyProxy -vkey=生成的key -server=公网服务器ip:下面设定的端口</span><br>建立成功后,在外网环境下本机配置socks5代理,即访问内网设备或者资源 "]
     arr["httpProxyServer"] = ["type", "port", "compress", "u", "p", " http代理模式,内网http代理,可访问内网网站,添加后会自动生成一个客户端验证key<br>在内网机器执行<span style='color: red'>./easyProxy -vkey=生成的key -server=公网服务器ip:下面设定的端口</span><br>建立成功后,在外网环境下本机配置http代理,即访问内网站点"]
-    arr["hostServer"] = ["type", "compress", "u", "p", "域名分发模式,使用域名代理内网服务,适用于小程序开发、公众号开发、站点演示等,添加后会自动生成一个客户端验证key<br>在内网机器执行<span style='color: red'>./easyProxy -vkey=生成的key -server=公网服务器ip:下面设定的端口</span><br>建立成功后,使用nginx将请求反向代理到本程序,再进行域名配置,即可解析"]
+    arrClientHide = ["compress", "u", "p", "crypt", "mux"]
 
     function resetForm() {
         for (var i = 0; i < arr["all"].length; i++) {
@@ -90,10 +105,28 @@
         $("#info").html(arr[o][arr[o].length - 1])
     }
 
+    function resetClientCnf() {
+        for (var i = 0; i < arrClientHide.length; i++) {
+            $("#" + arrClientHide[i]).css("display", "block")
+        }
+        op = $("#use_client  option:selected").val()
+        if (op == 1) {
+            for (var i = 0; i < arrClientHide.length; i++) {
+                $("#" + arrClientHide[i]).css("display", "none")
+            }
+        }
+    }
+
     $(function () {
         resetForm()
+        resetClientCnf()
         $("#type").on("change", function () {
             resetForm()
+            resetClientCnf()
+        })
+        $("#use_client").on("change", function () {
+            resetForm()
+            resetClientCnf()
         })
         $("#add").on("click", function () {
             $.ajax({

+ 40 - 6
web/views/index/edit.html

@@ -4,17 +4,21 @@
             <h3 class="tile-title">添加</h3>
             <div class="tile-body">
                 <form>
-                    <input type="hidden" name="vKey" value="{{.t.VerifyKey}}">
+                    <input type="hidden" name="id" value="{{.t.Id}}">
                     <div class="form-group">
                         <label class="control-label">模式</label>
                         <select class="form-control" name="type" id="type">
                             <option {{if eq "tunnelServer" .t.Mode}}selected{{end}} value="tunnelServer">tcp隧道</option>
                             <option {{if eq "udpServer" .t.Mode}}selected{{end}} value="udpServer">udp隧道</option>
-                            <option {{if eq "socks5Server" .t.Mode}}selected{{end}} value="socks5Server">socks5代理</option>
+                            <option {{if eq "socks5Server" .t.Mode}}selected{{end}} value="socks5Server">socks5代理
+                            </option>
                             <option {{if eq "httpProxyServer" .t.Mode}}selected{{end}} value="httpProxyServer">http代理
-                            <option {{if eq "hostServer" .t.Mode}}selected{{end}} value="hostServer">host客户端</option>
                         </select>
                     </div>
+                    <div class="form-group">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" value="{{.t.Remark}}" type="text" name="remark" placeholder="备注">
+                    </div>
                     <div class="form-group" id="port">
                         <label class="control-label">监听的端口</label>
                         <input class="form-control" value="{{.t.TcpPort}}" type="text" name="port"
@@ -25,6 +29,18 @@
                         <input class="form-control" value="{{.t.Target}}" type="text" name="target"
                                placeholder="内网隧道目标,例如10.1.50.203:22">
                     </div>
+                    <div class="form-group" id="client_id">
+                        <label class="control-label">客户端ID</label>
+                        <input class="form-control" value="{{.t.ClientId}}" type="text" name="client_id"
+                               placeholder="客户端id">
+                    </div>
+                    <div class="form-group" id="use_client">
+                        <label class="control-label">是否使用客户端配置</label>
+                        <select class="form-control" name="use_client">
+                            <option {{if eq false .t.UseClientCnf}}selected{{end}} value="0">否</option>
+                            <option {{if eq  true .t.UseClientCnf}}selected{{end}} value="1">是</option>
+                        </select>
+                    </div>
                     <div class="form-group" id="compress">
                         <label class="control-label">数据压缩方式(所有模式均支持)</label>
                         <select class="form-control" name="compress">
@@ -32,14 +48,14 @@
                             <option {{if eq "snappy" .t.Compress}}selected{{end}} value="snappy">snappy压缩</option>
                         </select>
                     </div>
-                    <div class="form-group" id="compress">
+                    <div class="form-group" id="crypt">
                         <label class="control-label">是否加密传输(所有模式均支持)</label>
                         <select class="form-control" name="crypt">
                             <option {{if eq false .t.Crypt}}selected{{end}} value="0">不加密</option>
                             <option {{if eq  true .t.Crypt}}selected{{end}} value="1">加密</option>
                         </select>
                     </div>
-                    <div class="form-group" id="compress">
+                    <div class="form-group" id="mux">
                         <label class="control-label">是否启用TCP复用(所有模式均支持)</label>
                         <select class="form-control" name="mux">
                             <option {{if eq false .t.Mux}}selected{{end}} value="0">不启用</option>
@@ -74,7 +90,7 @@
     arr["udpServer"] = ["type", "port", "target", "compress"]
     arr["socks5Server"] = ["type", "port", "compress", "u", "p"]
     arr["httpProxyServer"] = ["type", "port", "compress", "u", "p"]
-    arr["hostServer"] = ["type", "compress", "u", "p"]
+    arrClientHide = ["compress", "u", "p", "crypt", "mux"]
 
     function resetForm() {
         for (var i = 0; i < arr["all"].length; i++) {
@@ -86,10 +102,28 @@
         }
     }
 
+    function resetClientCnf() {
+        for (var i = 0; i < arrClientHide.length; i++) {
+            $("#" + arrClientHide[i]).css("display", "block")
+        }
+        op = $("#use_client  option:selected").val()
+        if (op == 1) {
+            for (var i = 0; i < arrClientHide.length; i++) {
+                $("#" + arrClientHide[i]).css("display", "none")
+            }
+        }
+    }
+
     $(function () {
         resetForm()
+        resetClientCnf()
         $("#type").on("change", function () {
             resetForm()
+            resetClientCnf()
+        })
+        $("#use_client").on("change", function () {
+            resetForm()
+            resetClientCnf()
         })
         $("#add").on("click", function () {
             $.ajax({

+ 10 - 2
web/views/index/hadd.html

@@ -4,18 +4,26 @@
             <h3 class="tile-title">添加</h3>
             <div class="tile-body">
                 <form>
-                    <input type="hidden" name="vkey" value="{{.vkey}}">
+                    <div class="form-group">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" type="text" name="remark" placeholder="备注">
+                    </div>
                     <div class="form-group">
                         <label class="control-label">域名</label>
                         <input class="form-control" type="text" name="host" placeholder="域名">
                     </div>
+                    <div class="form-group">
+                        <label class="control-label">客户端id</label>
+                        <input value="{{.client_id}}" class="form-control" type="text" name="client_id" placeholder="客户端id">
+                    </div>
                     <div class="form-group">
                         <label class="control-label">内网目标</label>
                         <input class="form-control" type="text" name="target" placeholder="内网隧道目标,例如10.1.50.203:22">
                     </div>
                     <div class="form-group" id="header">
                         <label class="control-label">header头修改(冒号分隔,多个请换行填写)</label>
-                        <textarea class="form-control" rows="4" type="text" name="header" placeholder="Cache-Control: no-cache"></textarea>
+                        <textarea class="form-control" rows="4" type="text" name="header"
+                                  placeholder="Cache-Control: no-cache"></textarea>
                     </div>
                     <div class="form-group" id="hostchange">
                         <label class="control-label">host修改</label>

+ 9 - 1
web/views/index/hedit.html

@@ -4,12 +4,20 @@
             <h3 class="tile-title">修改</h3>
             <div class="tile-body">
                 <form>
-                    <input type="hidden" name="vkey" value="{{.t.VerifyKey}}">
                     <div class="form-group">
                         <label class="control-label">域名</label>
                         <input class="form-control" value="{{.h.Host}}" type="hidden" name="host" placeholder="域名">
                         <input class="form-control" value="{{.h.Host}}" type="text" name="nhost" placeholder="域名">
                     </div>
+                    <div class="form-group">
+                        <label class="control-label">备注</label>
+                        <input class="form-control" value="{{.h.Remark}}" type="text" name="remark" placeholder="备注">
+                    </div>
+                    <div class="form-group">
+                        <label class="control-label">客户端id</label>
+                        <input class="form-control" value="{{.h.ClientId}}" type="text" name="client_id"
+                               placeholder="客户端id">
+                    </div>
                     <div class="form-group">
                         <label class="control-label">内网目标</label>
                         <input class="form-control" value="{{.h.Target}}" type="text" name="target"

+ 146 - 0
web/views/index/help.html

@@ -0,0 +1,146 @@
+<div class="row">
+    <div class="col-md-12">
+        <div class="tile">
+            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=star&count=true&size=large"
+                    frameborder="0" scrolling="0" width="160px" height="30px"></iframe>
+            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=watch&count=true&size=large&v=2"
+                    frameborder="0" scrolling="0" width="160px" height="30px"></iframe>
+            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=fork&count=true&size=large"
+                    frameborder="0" scrolling="0" width="158px" height="30px"></iframe>
+        </div>
+    </div>
+</div>
+<div class="row">
+
+    <div class="col-md-12">
+        <div class="tile">
+            <h3 class="tile-title">域名代理模式</h3>
+            <p>
+                <b>适用范围:</b> 小程序开发、微信公众号开发、产品演示
+            </p>
+            <p>
+                <b>假设场景:</b>
+            <li>有一个域名proxy.com,有一台公网机器ip为{{.ip}}</li>
+            <li>两个内网开发站点127.0.0.1:81,127.0.0.1:82</li>
+            <li>想通过a.proxy.com访问127.0.0.1:81,通过b.proxy.com访问127.0.0.1:82</li>
+            </p>
+            <p><b>使用步骤:</b></p>
+            <ul>
+                <li>将a.proxy.com,b.proxy.com解析到公网服务器{{.ip}}</li>
+                <li>使用nginx监听这两个个域名,并配置ssl等……</li>
+                <li>在nginx配置中添加反向代理(或直接将http监听端口改为80)<br>
+                    <pre><code>
+                    server {
+                        listen 80;
+                        server_name a.proxy.com b.proxy.com;#也可以是泛解析*.proxy.com
+                        #ssl等配置
+                        <b>location / {
+                            proxy_pass http://127.0.0.1:{{.proxyPort}};
+                        }</b>
+                    }
+                        </code></pre>
+                </li>
+                <li>在域名代理管理中添加一个客户端,选择压缩方式,保存。 <a href="/index/add?type=hostServer">立即添加</a></li>
+            {{/*<li>在域名代理管理中找到新加的客户端(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
+                <li>点击该客户端的域名管理,添加两条规则规则:1、域名:a.proxy.com,内网目标:127.0.0.1:81,2、域名:b.proxy.com,内网目标:127.0.0.1:82</li>
+                <li>现在访问a.proxy.com,b.proxy.com即可成功</li>
+            </ul>
+            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+        </div>
+    </div>
+</div>
+<div class="row">
+    <div class="col-md-6">
+        <div class="tile">
+            <h3 class="tile-title">tcp隧道模式</h3>
+            <p>
+                <b>适用范围:</b> ssh、远程桌面等tcp连接场景
+            </p>
+            <p>
+                <b>假设场景:</b> 想通过访问公网服务器{{.ip}}的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
+            </p>
+            <p><b>使用步骤:</b></p>
+            <ul>
+                <li>在tcp隧道管理中添加一条隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),选择压缩方式,保存。 <a
+                        href="/index/add?type=tunnelServer">立即添加</a></li>
+            {{/*<li>在tcp管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
+                <li>访问公网服务器ip({{.ip}}):填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:ssh -p 8001 root@{{.ip}}</li>
+            </ul>
+            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+        </div>
+    </div>
+    <div class="col-md-6">
+        <div class="tile">
+            <h3 class="tile-title">udp隧道模式</h3>
+            <p>
+                <b>适用范围:</b> 内网dns解析等udp连接场景
+            </p>
+            <p>
+                <b>假设场景:</b> 内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为{{.ip}}
+            </p>
+            <p><b>使用步骤:</b></p>
+            <ul>
+                <li>在udp隧道管理中添加一条隧道,填写监听的端口(8002)、内网目标ip和目标端口(10.1.50.102:53),选择压缩方式,保存。 <a
+                        href="/index/add?type=udpServer">立即添加</a></li>
+            {{/*<li>在udp管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
+                <li>修改本机dns为{{.ip}},则相当于使用10.1.50.202作为dns服务器</li>
+            </ul>
+            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+        </div>
+    </div>
+</div>
+<div class="row">
+    <div class="col-md-6">
+        <div class="tile">
+            <h3 class="tile-title">socks5代理模式</h3>
+            <p>
+                <b>适用范围:</b> 在外网环境下如同使用vpn一样访问内网设备或者资源
+            </p>
+            <p>
+                <b>假设场景:</b> 想将公网服务器{{.ip}}的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
+            </p>
+            <p><b>使用步骤:</b></p>
+            <ul>
+                <li>在socks5隧道管理中添加一条隧道,填写监听的端口(8003),验证用户名和密码自行选择(建议先不填,部分客户端不支持,proxifer支持),选择压缩方式,保存。 <a
+                        href="/index/add?type=sock5Server">立即添加</a></li>
+            {{/*<li>在socks5代理管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
+                <li>在外网环境的本机配置socks5代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8003),即可畅享内网了</li>
+            </ul>
+            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+        </div>
+    </div>
+    <div class="col-md-6">
+        <div class="tile">
+            <h3 class="tile-title">http代理模式</h3>
+            <p>
+                <b>适用范围:</b> 在外网环境下访问内网站点
+            </p>
+            <p>
+                <b>假设场景:</b> 想将公网服务器{{.ip}}的8004端口作为http代理,访问内网网站
+            </p>
+            <p><b>使用步骤:</b></p>
+            <ul>
+                <li>在http隧道管理中添加一条隧道,填写监听的端口(8004),选择压缩方式,保存。 <a
+                        href="/index/add?type=httpProxyServer">立即添加</a></li>
+                <li>在http代理管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>
+                <li>在外网环境的本机配置http代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8004),即可访问了</li>
+            </ul>
+            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+        </div>
+    </div>
+</div>
+<div class="row">
+    <div class="col-md-12">
+        <div class="tile">
+            <p>
+                <b>多客户端模式:</b>
+            <li>服务端启动:./proxy_server</li>
+            <li>客户端启动:./proxy_client -server={{.ip}}:{{.p}} -vkey=xxx(见管理列表的客户端启动模式)</li>
+            </p>
+            <p><b>支持客户端同时建立多条隧道,例如单个通道时命令为./proxy_client -server={{.ip}}:{{.p}}
+                -vkey=ccc,如果要支持另外一个隧道,则对应的执行命令为./proxy_client
+                -server={{.ip}}:{{.p}} -vkey=ccc,ddd,即用逗号分隔开多个vkey,适用于所有模式!</b></p>
+        </div>
+    </div>
+</div>
+</main>

+ 29 - 4
web/views/index/hlist.html

@@ -5,9 +5,13 @@
                 <table class="table table-hover table-bordered" id="sampleTable">
                     <thead>
                     <tr>
-                        <th>客户端key</th>
+                        <th>客户端id</th>
+                        <th>备注</th>
                         <th>host</th>
                         <th>内网目标</th>
+                        <th>host改写</th>
+                        <th>出口流量</th>
+                        <th>入口流量</th>
                         <th>操作</th>
                     </tr>
                     </thead>
@@ -39,7 +43,7 @@
     }
 
     function add() {
-        window.location.href = "/index/addhost?vkey={{.vkey}}"
+        window.location.href = "/index/addhost?vkey={{.task_id}}&client_id={{.client_id}}"
     }
 
     function edit(host) {
@@ -48,6 +52,11 @@
 
     $(document).ready(function () {
         var table = $('#sampleTable').DataTable({
+            responsive: {
+                details: {
+                    display: $.fn.dataTable.Responsive.display.childRowImmediate
+                }
+            },
             dom: 'Bfrtip',
             processing: true,
             serverSide: true,
@@ -59,9 +68,13 @@
             },
             dom: '<"top"fl><"toolbar">rt<"bottom"ip><"clear">',
             columns: [ //这个是显示到界面上的个数据 格式为 {data:'显示的字段名'}
-                {data: 'Vkey'},
+                {data: 'ClientId'},
+                {data: 'Remark'},
                 {data: 'Host'},
                 {data: 'Target'},
+                {data: 'HostChange'},
+                {data: 'HostChange'},
+                {data: 'HostChange'},
                 {data: 'Target'},
             ],
             bFilter: false,
@@ -74,7 +87,19 @@
                             '<button onclick="edit(\'' + row.Host + '\')" type="button"   class="btn btn-primary btn-sm">编辑查看</button> '
                             + ' </div>'
                 }
-            }
+            },
+                {
+                    targets: -2,
+                    render: function (data, type, row, meta) {
+                        return change(row.Flow.InletFlow)
+                    }
+                },
+                {
+                    targets: -3,
+                    render: function (data, type, row, meta) {
+                        return change(row.Flow.ExportFlow)
+                    }
+                }
             ],
             buttons: []
         });

+ 84 - 127
web/views/index/index.html

@@ -1,147 +1,104 @@
 <div class="row">
-    <div class="col-md-12">
-        <div class="tile">
-            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=star&count=true&size=large"
-                    frameborder="0" scrolling="0" width="160px" height="30px"></iframe>
-            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=watch&count=true&size=large&v=2"
-                    frameborder="0" scrolling="0" width="160px" height="30px"></iframe>
-            <iframe src="https://ghbtns.com/github-btn.html?user=cnlh&repo=easyProxy&type=fork&count=true&size=large"
-                    frameborder="0" scrolling="0" width="158px" height="30px"></iframe>
+    <div class="col-md-3">
+        <div class="widget-small warning coloured-icon"><i class="icon fa fa-html5 fa-3x"></i>
+            <div class="info">
+                <h4>HTTP端口</h4>
+                <p><b>{{.p}}</b></p>
+            </div>
         </div>
     </div>
-</div>
-
-
-<div class="row">
-    <div class="col-md-12">
-        <div class="tile">
-            <h3 class="tile-title">域名代理模式</h3>
-            <p>
-                <b>适用范围:</b> 小程序开发、微信公众号开发、产品演示
-            </p>
-            <p>
-                <b>假设场景:</b>
-            <li>有一个域名proxy.com,有一台公网机器ip为{{.ip}}</li>
-            <li>两个内网开发站点127.0.0.1:81,127.0.0.1:82</li>
-            <li>想通过a.proxy.com访问127.0.0.1:81,通过b.proxy.com访问127.0.0.1:82</li>
-            </p>
-            <p><b>使用步骤:</b></p>
-            <ul>
-                <li>将a.proxy.com,b.proxy.com解析到公网服务器{{.ip}}</li>
-                <li>使用nginx监听这两个个域名,并配置ssl等……</li>
-                <li>在nginx配置中添加反向代理(或直接将http监听端口改为80)<br>
-                    <pre><code>
-                    server {
-                        listen 80;
-                        server_name a.proxy.com b.proxy.com;#也可以是泛解析*.proxy.com
-                        #ssl等配置
-                        <b>location / {
-                            proxy_pass http://127.0.0.1:{{.proxyPort}};
-                        }</b>
-                    }
-                        </code></pre>
-                </li>
-                <li>在域名代理管理中添加一个客户端,选择压缩方式,保存。 <a href="/index/add?type=hostServer">立即添加</a></li>
-            {{/*<li>在域名代理管理中找到新加的客户端(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
-                <li>点击该客户端的域名管理,添加两条规则规则:1、域名:a.proxy.com,内网目标:127.0.0.1:81,2、域名:b.proxy.com,内网目标:127.0.0.1:82</li>
-                <li>现在访问a.proxy.com,b.proxy.com即可成功</li>
-            </ul>
-            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+    <div class="col-md-3">
+        <div class="widget-small danger coloured-icon"><i class="icon fa fa-home fa-3x"></i>
+            <div class="info">
+                <h4>域名解析数</h4>
+                <p><b>{{.data.hostCount}}</b></p>
+            </div>
         </div>
     </div>
-</div>
-<div class="row">
-    <div class="col-md-6">
-        <div class="tile">
-            <h3 class="tile-title">tcp隧道模式</h3>
-            <p>
-                <b>适用范围:</b> ssh、远程桌面等tcp连接场景
-            </p>
-            <p>
-                <b>假设场景:</b> 想通过访问公网服务器{{.ip}}的8001端口,连接内网机器10.1.50.101的22端口,实现ssh连接
-            </p>
-            <p><b>使用步骤:</b></p>
-            <ul>
-                <li>在tcp隧道管理中添加一条隧道,填写监听的端口(8001)、内网目标ip和目标端口(10.1.50.101:22),选择压缩方式,保存。 <a
-                        href="/index/add?type=tunnelServer">立即添加</a></li>
-            {{/*<li>在tcp管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
-                <li>访问公网服务器ip({{.ip}}):填写的监听端口(8001),相当于访问内网ip(10.1.50.101):目标端口(22),例如:ssh -p 8001 root@{{.ip}}</li>
-            </ul>
-            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+    <div class="col-md-3">
+        <div class="widget-small primary coloured-icon"><i class="icon fa fa-users fa-3x"></i>
+            <div class="info">
+                <h4>总客户端数</h4>
+                <p><b>{{.data.clientCount}}</b></p>
+            </div>
         </div>
     </div>
-    <div class="col-md-6">
-        <div class="tile">
-            <h3 class="tile-title">udp隧道模式</h3>
-            <p>
-                <b>适用范围:</b> 内网dns解析等udp连接场景
-            </p>
-            <p>
-                <b>假设场景:</b> 内网有一台dns(10.1.50.102:53),在非内网环境下想使用该dns,公网服务器为{{.ip}}
-            </p>
-            <p><b>使用步骤:</b></p>
-            <ul>
-                <li>在udp隧道管理中添加一条隧道,填写监听的端口(8002)、内网目标ip和目标端口(10.1.50.102:53),选择压缩方式,保存。 <a
-                        href="/index/add?type=udpServer">立即添加</a></li>
-            {{/*<li>在udp管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
-                <li>修改本机dns为{{.ip}},则相当于使用10.1.50.202作为dns服务器</li>
-            </ul>
-            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+    <div class="col-md-3">
+        <div class="widget-small info coloured-icon"><i class="icon fa fa-user-secret fa-3x"></i>
+            <div class="info">
+                <h4>在线客户端数</h4>
+                <p><b>{{.data.clientOnlineCount}}</b></p>
+            </div>
         </div>
     </div>
 </div>
 <div class="row">
     <div class="col-md-6">
         <div class="tile">
-            <h3 class="tile-title">socks5代理模式</h3>
-            <p>
-                <b>适用范围:</b> 在外网环境下如同使用vpn一样访问内网设备或者资源
-            </p>
-            <p>
-                <b>假设场景:</b> 想将公网服务器{{.ip}}的8003端口作为socks5代理,达到访问内网任意设备或者资源的效果
-            </p>
-            <p><b>使用步骤:</b></p>
-            <ul>
-                <li>在socks5隧道管理中添加一条隧道,填写监听的端口(8003),验证用户名和密码自行选择(建议先不填,部分客户端不支持,proxifer支持),选择压缩方式,保存。 <a
-                        href="/index/add?type=sock5Server">立即添加</a></li>
-            {{/*<li>在socks5代理管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>*/}}
-                <li>在外网环境的本机配置socks5代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8003),即可畅享内网了</li>
-            </ul>
-            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
+            <h3 class="tile-title">流量</h3>
+            <div class="embed-responsive embed-responsive-16by9">
+                <canvas class="embed-responsive-item" id="flow"></canvas>
+            </div>
         </div>
     </div>
     <div class="col-md-6">
         <div class="tile">
-            <h3 class="tile-title">http代理模式</h3>
-            <p>
-                <b>适用范围:</b> 在外网环境下访问内网站点
-            </p>
-            <p>
-                <b>假设场景:</b> 想将公网服务器{{.ip}}的8004端口作为http代理,访问内网网站
-            </p>
-            <p><b>使用步骤:</b></p>
-            <ul>
-                <li>在http隧道管理中添加一条隧道,填写监听的端口(8004),选择压缩方式,保存。 <a
-                        href="/index/add?type=httpProxyServer">立即添加</a></li>
-                <li>在http代理管理列表中找到新加的隧道(查看其vkey或者客户端命令),任意内网机器执行客户端命令</li>
-                <li>在外网环境的本机配置http代理,ip为公网服务器ip({{.ip}}),端口为填写的监听端口(8004),即可访问了</li>
-            </ul>
-            <p>注:上文中提到公网ip({{.ip}})为系统自动识别,如果是在测试环境中请自行对应,默认启动方式为单客户端模式,默认内网客户端已经启动</p>
-        </div>
-    </div>
-</div>
-<div class="row">
-    <div class="col-md-12">
-        <div class="tile">
-            <p>
-                <b>多客户端模式:</b>
-            <li>服务端启动:./proxy_server</li>
-            <li>客户端启动:./proxy_client -server={{.ip}}:{{.p}} -vkey=xxx(见管理列表的客户端启动模式)</li>
-            </p>
-            <p><b>支持客户端同时建立多条隧道,例如单个通道时命令为./proxy_client -server={{.ip}}:{{.p}} -vkey=ccc,如果要支持另外一个隧道,则对应的执行命令为./proxy_client
-                -server={{.ip}}:{{.p}} -vkey=ccc,ddd,即用逗号分隔开多个vkey,适用于所有模式!</b></p>
+            <h3 class="tile-title">代理类型</h3>
+            <div class="embed-responsive embed-responsive-16by9">
+                <canvas class="embed-responsive-item" id="types"></canvas>
+            </div>
         </div>
     </div>
 </div>
-
-</main>
+<script>
+    var pdataFlow = [
+        {
+            value: {{.data.inletFlowCount}},
+            color: "#46BFBD",
+            highlight: "#5AD3D1",
+            label: "入口流量"
+        },
+        {
+            value: {{.data.exportFlowCount}},
+            color: "#FDB45C",
+            highlight: "#FFC870",
+            label: "出口流量"
+        }
+    ]
+    var pdataTypes = [
+        {
+            value: {{.data.tunnelServerCount}},
+            color: "#46BFBD",
+            highlight: "#5AD3D1",
+            label: "tcp隧道"
+        },
+        {
+            value: {{.data.socks5ServerCount}},
+            color: "#85FEAA",
+            highlight: "#85FEAA",
+            label: "socks5隧道"
+        },
+        {
+            value: {{.data.httpProxyServerCount}},
+            color: "#4B653C",
+            highlight: "#4B653C",
+            label: "http代理"
+        },
+        {
+            value: {{.data.udpServerCount}},
+            color: "#90653C",
+            highlight: "#90653C",
+            label: "udp代理"
+        },
+        {
+            value: {{.data.hostCount}},
+            color: "#FDB45C",
+            highlight: "#FDB45C",
+            label: "域名解析"
+        }
+    ]
+    var ctxp = $("#flow").get(0).getContext("2d");
+    var pieChart = new Chart(ctxp).Pie(pdataFlow);
+    var ctxd = $("#types").get(0).getContext("2d");
+    var doughnutChart = new Chart(ctxd).Doughnut(pdataTypes);
+</script>

+ 46 - 44
web/views/index/list.html

@@ -5,10 +5,11 @@
                 <table class="table table-hover table-bordered" id="sampleTable">
                     <thead>
                     <tr>
-                    {{/*<th>模式</th>*/}}
+                        <th>Id</th>
+                        <th>备注</th>
+                        <th>客户端Id</th>
                         <th>监听端口</th>
                         <th>内网目标</th>
-                        <th>多客户端模式客户端执行命令</th>
                         <th>压缩方式</th>
                         <th>加密传输</th>
                         <th>TCP多路复用</th>
@@ -16,11 +17,12 @@
                         <th>密码</th>
                         <th>客户端状态</th>
                         <th>状态</th>
+                        <th>出口流量</th>
+                        <th>入口流量</th>
                         <th>操作</th>
                     </tr>
                     </thead>
                     <tbody>
-
                     </tbody>
                 </table>
             </div>
@@ -30,12 +32,12 @@
 </main>
 
 <script type="text/javascript">
-    function del(vKey) {
+    function del(id) {
         if (confirm("确定要删除数据吗?")) {
             $.ajax({
                 type: "POST",
                 url: "/index/del",
-                data: {"vKey": vKey},
+                data: {"id": id},
                 success: function (res) {
                     alert(res.msg)
                     if (res.status) {
@@ -46,12 +48,12 @@
         }
     }
 
-    function start(vKey) {
+    function start(id) {
         if (confirm("确定要开始任务吗?")) {
             $.ajax({
                 type: "POST",
                 url: "/index/start",
-                data: {"vKey": vKey},
+                data: {"id": id},
                 success: function (res) {
                     alert(res.msg)
                     if (res.status) {
@@ -62,12 +64,12 @@
         }
     }
 
-    function stop(vKey) {
+    function stop(id) {
         if (confirm("确定要暂停吗?")) {
             $.ajax({
                 type: "POST",
                 url: "/index/stop",
-                data: {"vKey": vKey},
+                data: {"id": id},
                 success: function (res) {
                     alert(res.msg)
                     if (res.status) {
@@ -78,35 +80,38 @@
         }
     }
 
-    function edit(vKey) {
-        window.location.href = "/index/edit?vKey=" + vKey
+    function edit(id) {
+        window.location.href = "/index/edit?id=" + id
     }
 
     function add() {
-        window.location.href = "/index/add?type=" +{{.type}}
+        window.location.href = "/index/add?type={{.type}}" + "&client_id={{.client_id}}"
     }
 
-    function hostList(vkey) {
-        window.location.href = "/index/hostlist?vkey=" + vkey
-    }
 
     $(document).ready(function () {
         var table = $('#sampleTable').DataTable({
+            responsive: {
+                details: {
+                    display: $.fn.dataTable.Responsive.display.childRowImmediate
+                }
+            },
             dom: 'Bfrtip',
             processing: true,
             serverSide: true,
             autoWidth: false,
             ordering: false,
             ajax: {
-                url: '/index/getserverconfig?type={{.type}}',
+                url: '/index/getserverconfig?type={{.type}}' + "&client_id=" +{{.client_id}},
                 type: 'POST'
             },
             dom: '<"top"fl><"toolbar">rt<"bottom"ip><"clear">',
             columns: [ //这个是显示到界面上的个数据 格式为 {data:'显示的字段名'}
-                // {data: 'Mode'},
+                {data: 'Id'},
+                {data: 'Remark'},
+                {data: 'ClientId'},
                 {data: 'TcpPort'},
                 {data: 'Target'},
-                {data: 'VerifyKey'},
                 {data: 'Compress'},
                 {data: 'Crypt'},
                 {data: 'Mux'},
@@ -114,6 +119,8 @@
                 {data: 'P'},
                 {data: 'ClientStatus'},
                 {data: 'IsRun'},
+                {data: 'ExportFlow'},
+                {data: 'InletFlow'},
                 {data: "Id"}
             ],
             bFilter: false,
@@ -121,27 +128,18 @@
                 targets: -1,
                 render: function (data, type, row, meta) {
                     if (row.IsRun == 1) {
-                        btn = "<button onclick=\"stop('" + row.VerifyKey + "')\" class=\"btn btn-secondary btn-sm\" type=\"button\">关闭</button>"
+                        btn = "<button onclick=\"stop('" + row.Id + "')\" class=\"btn btn-secondary btn-sm\" type=\"button\">关闭</button>"
                     } else {
-                        btn = "<button onclick=\"start('" + row.VerifyKey + "')\" class=\"btn btn-success btn-sm\" type=\"button\">打开</button>"
-                    }
-                    btn_edit = '<button onclick="edit(\'' + row.VerifyKey + '\')" type="button"   class="btn btn-primary btn-sm">查看编辑</button> '
-                    if ({{.type}} == "hostServer"
-                )
-                    {
-                        btn_host = '<button onclick="hostList(\'' + row.VerifyKey + '\')" type="button"   class="btn btn-info btn-sm">域名管理</button> '
-                    }
-                else
-                    {
-                        btn_host = ""
+                        btn = "<button onclick=\"start('" + row.Id + "')\" class=\"btn btn-success btn-sm\" type=\"button\">打开</button>"
                     }
+                    btn_edit = '<button onclick="edit(\'' + row.Id + '\')" type="button"   class="btn btn-primary btn-sm">查看编辑</button> '
                     return '<div class="btn-group" role="group" aria-label="..."> ' +
-                            '<button onclick="del(\'' + row.VerifyKey + '\')" type="button"   class="btn btn-danger btn-sm">删除</button>' +
-                            btn_edit + btn + btn_host + ' </div>'
+                            '<button onclick="del(\'' + row.Id + '\')" type="button"   class="btn btn-danger btn-sm">删除</button>' +
+                            btn_edit + btn + ' </div>'
                 }
             },
                 {
-                    targets: -2,
+                    targets: -4,
                     render: function (data, type, row, meta) {
                         if (data == 0) {
                             return "<span class=\"badge badge-pill badge-secondary\">暂停</span>"
@@ -151,7 +149,7 @@
                     }
                 },
                 {
-                    targets: -7,
+                    targets: -9,
                     render: function (data, type, row, meta) {
                         if (data == "0") {
                             return "不加密"
@@ -161,7 +159,7 @@
                     }
                 },
                 {
-                    targets: -6,
+                    targets: -8,
                     render: function (data, type, row, meta) {
                         if (data == "0") {
                             return "不启用"
@@ -169,17 +167,9 @@
                             return "启用"
                         }
                     }
-                }
-                ,
-                {
-                    targets: 2,
-                    render: function (data, type, row, meta) {
-                        return "./proxy_client -server={{.ip}}:{{.p}} -vkey=" + data
-                        // return data
-                    }
                 },
                 {
-                    targets: -3,
+                    targets: -5,
                     render: function (data, type, row, meta) {
                         if (data == 0) {
                             return "<span class=\"badge badge-pill badge-secondary\">离线</span>"
@@ -187,6 +177,18 @@
                             return "<span class=\"badge badge-pill badge-success\">在线</span>"
                         }
                     }
+                },
+                {
+                    targets: -2,
+                    render: function (data, type, row, meta) {
+                        return change(row.Flow.InletFlow)
+                    }
+                },
+                {
+                    targets: -3,
+                    render: function (data, type, row, meta) {
+                        return change(row.Flow.ExportFlow)
+                    }
                 }
             ],
             buttons: []

+ 7 - 3
web/views/public/layout.html

@@ -41,8 +41,10 @@
     </div>
     <ul class="app-menu">
         <li><a class="app-menu__item {{if eq "index" .menu}}active{{end}}" href="/"><i
-                class="app-menu__icon fa fa-dashboard"></i><span class="app-menu__label">使用说明</span></a></li>
-        <li><a class="app-menu__item {{if eq "host" .menu}}active{{end}}" href="/index/host"><i
+                class="app-menu__icon fa fa-dashboard"></i><span class="app-menu__label">dashboard</span></a></li>
+        <li><a class="app-menu__item {{if eq "client" .menu}}active{{end}}" href="/client/client"><i
+                class="app-menu__icon fa fa-dot-circle-o"></i><span class="app-menu__label">客户端管理</span></a></li>
+        <li><a class="app-menu__item {{if eq "host" .menu}}active{{end}}" href="/index/hostlist"><i
                 class="app-menu__icon fa fa-lemon-o"></i><span class="app-menu__label">域名代理</span></a></li>
         <li><a class="app-menu__item {{if eq "tcp" .menu}}active{{end}}" href="/index/tcp"><i
                 class="app-menu__icon fa fa-life-buoy"></i><span class="app-menu__label">tcp隧道管理</span></a></li>
@@ -52,12 +54,14 @@
                 class="app-menu__icon fa fa-lightbulb-o"></i><span class="app-menu__label">socks5代理</span></a></li>
         <li><a class="app-menu__item {{if eq "http" .menu}}active{{end}}" href="/index/http"><i
                 class="app-menu__icon fa fa-magic"></i><span class="app-menu__label">http代理</span></a></li>
-
+        <li><a class="app-menu__item {{if eq "help" .menu}}active{{end}}" href="/index/help"><i
+                class="app-menu__icon fa fa-question-circle"></i><span class="app-menu__label">使用说明</span></a></li>
     </ul>
 </aside>
 
 <script type="text/javascript" src="/static/js/datatables.min.js"></script>
 <script src="/static/js/main.js"></script>
+<script src="/static/js/chart.js"></script>
 <main class="app-content">
     <div class="app-title">
         <div>