diff --git a/adapters/outbound/direct.go b/adapters/outbound/direct.go index 70a3f9eb5..1a6a1144c 100644 --- a/adapters/outbound/direct.go +++ b/adapters/outbound/direct.go @@ -1,6 +1,7 @@ package adapters import ( + "encoding/json" "net" C "github.com/Dreamacro/clash/constant" @@ -40,6 +41,12 @@ func (d *Direct) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err er return &DirectAdapter{conn: c}, nil } +func (d *Direct) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": d.Type().String(), + }) +} + func NewDirect() *Direct { return &Direct{} } diff --git a/adapters/outbound/fallback.go b/adapters/outbound/fallback.go index e858d0ab0..0acaabbb7 100644 --- a/adapters/outbound/fallback.go +++ b/adapters/outbound/fallback.go @@ -1,6 +1,7 @@ package adapters import ( + "encoding/json" "errors" "sync" "time" @@ -63,6 +64,18 @@ func (f *Fallback) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err return f.proxies[0].RawProxy.Generator(metadata) } +func (f *Fallback) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range f.proxies { + all = append(all, proxy.RawProxy.Name()) + } + return json.Marshal(map[string]interface{}{ + "type": f.Type().String(), + "now": f.Now(), + "all": all, + }) +} + func (f *Fallback) Close() { f.done <- struct{}{} } diff --git a/adapters/outbound/reject.go b/adapters/outbound/reject.go index deb36e714..ae26022dc 100644 --- a/adapters/outbound/reject.go +++ b/adapters/outbound/reject.go @@ -1,6 +1,7 @@ package adapters import ( + "encoding/json" "io" "net" "time" @@ -36,6 +37,12 @@ func (r *Reject) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err er return &RejectAdapter{conn: &NopConn{}}, nil } +func (r *Reject) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": r.Type().String(), + }) +} + func NewReject() *Reject { return &Reject{} } diff --git a/adapters/outbound/selector.go b/adapters/outbound/selector.go index 1f920ae1c..e4dbd6d92 100644 --- a/adapters/outbound/selector.go +++ b/adapters/outbound/selector.go @@ -1,6 +1,7 @@ package adapters import ( + "encoding/json" "errors" "sort" @@ -30,17 +31,21 @@ func (s *Selector) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err return s.selected.Generator(metadata) } -func (s *Selector) Now() string { - return s.selected.Name() -} - -func (s *Selector) All() []string { +func (s *Selector) MarshalJSON() ([]byte, error) { var all []string for k := range s.proxies { all = append(all, k) } sort.Strings(all) - return all + return json.Marshal(map[string]interface{}{ + "type": s.Type().String(), + "now": s.Now(), + "all": all, + }) +} + +func (s *Selector) Now() string { + return s.selected.Name() } func (s *Selector) Set(name string) error { diff --git a/adapters/outbound/shadowsocks.go b/adapters/outbound/shadowsocks.go index f743f4df6..313560842 100644 --- a/adapters/outbound/shadowsocks.go +++ b/adapters/outbound/shadowsocks.go @@ -2,6 +2,7 @@ package adapters import ( "bytes" + "encoding/json" "fmt" "net" "strconv" @@ -71,6 +72,12 @@ func (ss *ShadowSocks) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, return &ShadowsocksAdapter{conn: c}, err } +func (ss *ShadowSocks) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": ss.Type().String(), + }) +} + func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) cipher := option.Cipher diff --git a/adapters/outbound/socks5.go b/adapters/outbound/socks5.go index b814efb3e..dc958235d 100644 --- a/adapters/outbound/socks5.go +++ b/adapters/outbound/socks5.go @@ -3,6 +3,7 @@ package adapters import ( "bytes" "crypto/tls" + "encoding/json" "errors" "fmt" "io" @@ -75,6 +76,12 @@ func (ss *Socks5) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e return &Socks5Adapter{conn: c}, nil } +func (ss *Socks5) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": ss.Type().String(), + }) +} + func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { buf := make([]byte, socks.MaxAddrLen) var err error diff --git a/adapters/outbound/urltest.go b/adapters/outbound/urltest.go index 74402e48f..1466c2771 100644 --- a/adapters/outbound/urltest.go +++ b/adapters/outbound/urltest.go @@ -1,7 +1,9 @@ package adapters import ( + "encoding/json" "errors" + "sort" "sync" "sync/atomic" "time" @@ -46,6 +48,19 @@ func (u *URLTest) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err e return a, err } +func (u *URLTest) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range u.proxies { + all = append(all, proxy.Name()) + } + sort.Strings(all) + return json.Marshal(map[string]interface{}{ + "type": u.Type().String(), + "now": u.Now(), + "all": all, + }) +} + func (u *URLTest) Close() { u.done <- struct{}{} } diff --git a/adapters/outbound/vmess.go b/adapters/outbound/vmess.go index 6969c3c4a..04f4b110f 100644 --- a/adapters/outbound/vmess.go +++ b/adapters/outbound/vmess.go @@ -1,6 +1,7 @@ package adapters import ( + "encoding/json" "fmt" "net" "strconv" @@ -43,24 +44,30 @@ type VmessOption struct { SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` } -func (ss *Vmess) Name() string { - return ss.name +func (v *Vmess) Name() string { + return v.name } -func (ss *Vmess) Type() C.AdapterType { +func (v *Vmess) Type() C.AdapterType { return C.Vmess } -func (ss *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { - c, err := net.DialTimeout("tcp", ss.server, tcpTimeout) +func (v *Vmess) Generator(metadata *C.Metadata) (adapter C.ProxyAdapter, err error) { + c, err := net.DialTimeout("tcp", v.server, tcpTimeout) if err != nil { - return nil, fmt.Errorf("%s connect error", ss.server) + return nil, fmt.Errorf("%s connect error", v.server) } tcpKeepAlive(c) - c, err = ss.client.New(c, parseVmessAddr(metadata)) + c, err = v.client.New(c, parseVmessAddr(metadata)) return &VmessAdapter{conn: c}, err } +func (v *Vmess) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "type": v.Type().String(), + }) +} + func NewVmess(option VmessOption) (*Vmess, error) { security := strings.ToLower(option.Cipher) client, err := vmess.NewClient(vmess.Config{ diff --git a/config/config.go b/config/config.go index ff784fef7..b62ba9d07 100644 --- a/config/config.go +++ b/config/config.go @@ -5,44 +5,30 @@ import ( "io/ioutil" "os" "strings" - "sync" - "time" adapters "github.com/Dreamacro/clash/adapters/outbound" - "github.com/Dreamacro/clash/common/observable" "github.com/Dreamacro/clash/common/structure" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" R "github.com/Dreamacro/clash/rules" + T "github.com/Dreamacro/clash/tunnel" - log "github.com/sirupsen/logrus" yaml "gopkg.in/yaml.v2" ) -var ( - config *Config - once sync.Once -) - // General config type General struct { - Port int - SocksPort int - RedirPort int - AllowLan bool - Mode Mode - LogLevel C.LogLevel + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` + AllowLan bool `json:"allow-lan"` + Mode T.Mode `json:"mode"` + LogLevel log.LogLevel `json:"log-level"` + ExternalController string `json:"external-controller,omitempty"` + Secret string `json:"secret,omitempty"` } -// ProxyConfig is update proxy schema -type ProxyConfig struct { - Port *int - SocksPort *int - RedirPort *int - AllowLan *bool -} - -// RawConfig is raw config struct -type RawConfig struct { +type rawConfig struct { Port int `yaml:"port"` SocksPort int `yaml:"socks-port"` RedirPort int `yaml:"redir-port"` @@ -59,38 +45,16 @@ type RawConfig struct { // Config is clash config manager type Config struct { - general *General - rules []C.Rule - proxies map[string]C.Proxy - lastUpdate time.Time - - event chan<- interface{} - reportCh chan interface{} - observable *observable.Observable + General *General + Rules []C.Rule + Proxies map[string]C.Proxy } -// Event is event of clash config -type Event struct { - Type string - Payload interface{} -} - -// Subscribe config stream -func (c *Config) Subscribe() observable.Subscription { - sub, _ := c.observable.Subscribe() - return sub -} - -// Report return a channel for collecting report message -func (c *Config) Report() chan<- interface{} { - return c.reportCh -} - -func (c *Config) readConfig() (*RawConfig, error) { - if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) { +func readConfig(path string) (*rawConfig, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } - data, err := ioutil.ReadFile(C.Path.Config()) + data, err := ioutil.ReadFile(path) if err != nil { return nil, err } @@ -100,10 +64,10 @@ func (c *Config) readConfig() (*RawConfig, error) { } // config with some default value - rawConfig := &RawConfig{ + rawConfig := &rawConfig{ AllowLan: false, - Mode: Rule.String(), - LogLevel: C.INFO.String(), + Mode: T.Rule.String(), + LogLevel: log.INFO.String(), Rule: []string{}, Proxy: []map[string]interface{}{}, ProxyGroup: []map[string]interface{}{}, @@ -113,131 +77,69 @@ func (c *Config) readConfig() (*RawConfig, error) { } // Parse config -func (c *Config) Parse() error { - cfg, err := c.readConfig() +func Parse(path string) (*Config, error) { + config := &Config{} + + rawCfg, err := readConfig(path) if err != nil { - return err + return nil, err } - if err := c.parseGeneral(cfg); err != nil { - return err - } - - if err := c.parseProxies(cfg); err != nil { - return err - } - - return c.parseRules(cfg) -} - -// Proxies return proxies of clash -func (c *Config) Proxies() map[string]C.Proxy { - return c.proxies -} - -// Rules return rules of clash -func (c *Config) Rules() []C.Rule { - return c.rules -} - -// SetMode change mode of clash -func (c *Config) SetMode(mode Mode) { - c.general.Mode = mode - c.event <- &Event{Type: "mode", Payload: mode} -} - -// SetLogLevel change log level of clash -func (c *Config) SetLogLevel(level C.LogLevel) { - c.general.LogLevel = level - c.event <- &Event{Type: "log-level", Payload: level} -} - -// General return clash general config -func (c *Config) General() General { - return *c.general -} - -// UpdateRules is a function for hot reload rules -func (c *Config) UpdateRules() error { - cfg, err := c.readConfig() + general, err := parseGeneral(rawCfg) if err != nil { - return err + return nil, err } + config.General = general - return c.parseRules(cfg) + proxies, err := parseProxies(rawCfg) + if err != nil { + return nil, err + } + config.Proxies = proxies + + rules, err := parseRules(rawCfg) + if err != nil { + return nil, err + } + config.Rules = rules + + return config, nil } -func (c *Config) parseGeneral(cfg *RawConfig) error { +func parseGeneral(cfg *rawConfig) (*General, error) { port := cfg.Port socksPort := cfg.SocksPort redirPort := cfg.RedirPort allowLan := cfg.AllowLan logLevelString := cfg.LogLevel modeString := cfg.Mode + externalController := cfg.ExternalController + secret := cfg.Secret - mode, exist := ModeMapping[modeString] + mode, exist := T.ModeMapping[modeString] if !exist { - return fmt.Errorf("General.mode value invalid") + return nil, fmt.Errorf("General.mode value invalid") } - logLevel, exist := C.LogLevelMapping[logLevelString] + logLevel, exist := log.LogLevelMapping[logLevelString] if !exist { - return fmt.Errorf("General.log-level value invalid") + return nil, fmt.Errorf("General.log-level value invalid") } - c.general = &General{ - Port: port, - SocksPort: socksPort, - RedirPort: redirPort, - AllowLan: allowLan, - Mode: mode, - LogLevel: logLevel, + general := &General{ + Port: port, + SocksPort: socksPort, + RedirPort: redirPort, + AllowLan: allowLan, + Mode: mode, + LogLevel: logLevel, + ExternalController: externalController, + Secret: secret, } - - if restAddr := cfg.ExternalController; restAddr != "" { - c.event <- &Event{Type: "external-controller", Payload: restAddr} - c.event <- &Event{Type: "secret", Payload: cfg.Secret} - } - - c.UpdateGeneral(*c.general) - return nil + return general, nil } -// UpdateGeneral dispatch update event -func (c *Config) UpdateGeneral(general General) { - c.UpdateProxy(ProxyConfig{ - Port: &general.Port, - SocksPort: &general.SocksPort, - RedirPort: &general.RedirPort, - AllowLan: &general.AllowLan, - }) - c.event <- &Event{Type: "mode", Payload: general.Mode} - c.event <- &Event{Type: "log-level", Payload: general.LogLevel} -} - -// UpdateProxy dispatch update proxy event -func (c *Config) UpdateProxy(pc ProxyConfig) { - if pc.AllowLan != nil { - c.general.AllowLan = *pc.AllowLan - } - - c.general.Port = *or(pc.Port, &c.general.Port) - if c.general.Port != 0 && (pc.AllowLan != nil || pc.Port != nil) { - c.event <- &Event{Type: "http-addr", Payload: genAddr(c.general.Port, c.general.AllowLan)} - } - - c.general.SocksPort = *or(pc.SocksPort, &c.general.SocksPort) - if c.general.SocksPort != 0 && (pc.AllowLan != nil || pc.SocksPort != nil) { - c.event <- &Event{Type: "socks-addr", Payload: genAddr(c.general.SocksPort, c.general.AllowLan)} - } - - c.general.RedirPort = *or(pc.RedirPort, &c.general.RedirPort) - if c.general.RedirPort != 0 && (pc.AllowLan != nil || pc.RedirPort != nil) { - c.event <- &Event{Type: "redir-addr", Payload: genAddr(c.general.RedirPort, c.general.AllowLan)} - } -} - -func (c *Config) parseProxies(cfg *RawConfig) error { +func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) { proxies := make(map[string]C.Proxy) proxiesConfig := cfg.Proxy groupsConfig := cfg.ProxyGroup @@ -251,7 +153,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error { for idx, mapping := range proxiesConfig { proxyType, existType := mapping["type"].(string) if !existType { - return fmt.Errorf("Proxy %d missing type", idx) + return nil, fmt.Errorf("Proxy %d missing type", idx) } var proxy C.Proxy @@ -279,15 +181,15 @@ func (c *Config) parseProxies(cfg *RawConfig) error { } proxy, err = adapters.NewVmess(*vmessOption) default: - return fmt.Errorf("Unsupport proxy type: %s", proxyType) + return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType) } if err != nil { - return fmt.Errorf("Proxy [%d]: %s", idx, err.Error()) + return nil, fmt.Errorf("Proxy [%d]: %s", idx, err.Error()) } if _, exist := proxies[proxy.Name()]; exist { - return fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) + return nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name()) } proxies[proxy.Name()] = proxy } @@ -297,11 +199,11 @@ func (c *Config) parseProxies(cfg *RawConfig) error { groupType, existType := mapping["type"].(string) groupName, existName := mapping["name"].(string) if !existType && existName { - return fmt.Errorf("ProxyGroup %d: missing type or name", idx) + return nil, fmt.Errorf("ProxyGroup %d: missing type or name", idx) } if _, exist := proxies[groupName]; exist { - return fmt.Errorf("ProxyGroup %s: the duplicate name", groupName) + return nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName) } var group C.Proxy var err error @@ -315,7 +217,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error { ps, err := getProxies(proxies, urlTestOption.Proxies) if err != nil { - return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) + return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) } group, err = adapters.NewURLTest(*urlTestOption, ps) case "select": @@ -327,7 +229,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error { ps, err := getProxies(proxies, selectorOption.Proxies) if err != nil { - return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) + return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) } group, err = adapters.NewSelector(selectorOption.Name, ps) case "fallback": @@ -339,12 +241,12 @@ func (c *Config) parseProxies(cfg *RawConfig) error { ps, err := getProxies(proxies, fallbackOption.Proxies) if err != nil { - return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) + return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error()) } group, err = adapters.NewFallback(*fallbackOption, ps) } if err != nil { - return fmt.Errorf("Proxy %s: %s", groupName, err.Error()) + return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error()) } proxies[groupName] = group } @@ -357,7 +259,7 @@ func (c *Config) parseProxies(cfg *RawConfig) error { proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", ps) // close old goroutine - for _, proxy := range c.proxies { + for _, proxy := range proxies { switch raw := proxy.(type) { case *adapters.URLTest: raw.Close() @@ -365,12 +267,10 @@ func (c *Config) parseProxies(cfg *RawConfig) error { raw.Close() } } - c.proxies = proxies - c.event <- &Event{Type: "proxies", Payload: proxies} - return nil + return proxies, nil } -func (c *Config) parseRules(cfg *RawConfig) error { +func parseRules(cfg *rawConfig) ([]C.Rule, error) { rules := []C.Rule{} rulesConfig := cfg.Rule @@ -397,55 +297,5 @@ func (c *Config) parseRules(cfg *RawConfig) error { } } - c.rules = rules - c.event <- &Event{Type: "rules", Payload: rules} - return nil -} - -func (c *Config) handleResponseMessage() { - for elm := range c.reportCh { - event := elm.(*Event) - switch event.Type { - case "http-addr": - if event.Payload.(bool) == false { - log.Errorf("Listening HTTP proxy at %d error", c.general.Port) - c.general.Port = 0 - } - case "socks-addr": - if event.Payload.(bool) == false { - log.Errorf("Listening SOCKS proxy at %d error", c.general.SocksPort) - c.general.SocksPort = 0 - } - case "redir-addr": - if event.Payload.(bool) == false { - log.Errorf("Listening Redir proxy at %d error", c.general.RedirPort) - c.general.RedirPort = 0 - } - } - } -} - -func newConfig() *Config { - event := make(chan interface{}) - reportCh := make(chan interface{}) - config := &Config{ - general: &General{}, - proxies: make(map[string]C.Proxy), - rules: []C.Rule{}, - lastUpdate: time.Now(), - - event: event, - reportCh: reportCh, - observable: observable.NewObservable(event), - } - go config.handleResponseMessage() - return config -} - -// Instance return singleton instance of Config -func Instance() *Config { - once.Do(func() { - config = newConfig() - }) - return config + return rules, nil } diff --git a/config/initial.go b/config/initial.go index 62f635700..fe13c66da 100644 --- a/config/initial.go +++ b/config/initial.go @@ -3,6 +3,7 @@ package config import ( "archive/tar" "compress/gzip" + "fmt" "io" "net/http" "os" @@ -54,15 +55,15 @@ func downloadMMDB(path string) (err error) { } // Init prepare necessary files -func Init() { +func Init(dir string) error { // initial homedir - if _, err := os.Stat(C.Path.HomeDir()); os.IsNotExist(err) { - if err := os.MkdirAll(C.Path.HomeDir(), 0777); err != nil { - log.Fatalf("Can't create config directory %s: %s", C.Path.HomeDir(), err.Error()) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0777); err != nil { + return fmt.Errorf("Can't create config directory %s: %s", dir, err.Error()) } } - // initial config.ini + // initial config.yml if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) { log.Info("Can't find config, create a empty file") os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644) @@ -73,7 +74,8 @@ func Init() { log.Info("Can't find MMDB, start download") err := downloadMMDB(C.Path.MMDB()) if err != nil { - log.Fatalf("Can't download MMDB: %s", err.Error()) + return fmt.Errorf("Can't download MMDB: %s", err.Error()) } } + return nil } diff --git a/config/mode.go b/config/mode.go deleted file mode 100644 index b910eba18..000000000 --- a/config/mode.go +++ /dev/null @@ -1,31 +0,0 @@ -package config - -type Mode int - -var ( - // ModeMapping is a mapping for Mode enum - ModeMapping = map[string]Mode{ - "Global": Global, - "Rule": Rule, - "Direct": Direct, - } -) - -const ( - Global Mode = iota - Rule - Direct -) - -func (m Mode) String() string { - switch m { - case Global: - return "Global" - case Rule: - return "Rule" - case Direct: - return "Direct" - default: - return "Unknow" - } -} diff --git a/config/utils.go b/config/utils.go index ca5a77233..54e205f8f 100644 --- a/config/utils.go +++ b/config/utils.go @@ -14,13 +14,6 @@ func trimArr(arr []string) (r []string) { return } -func genAddr(port int, allowLan bool) string { - if allowLan { - return fmt.Sprintf(":%d", port) - } - return fmt.Sprintf("127.0.0.1:%d", port) -} - func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) { var ps []C.Proxy for _, name := range list { diff --git a/constant/adapters.go b/constant/adapters.go index 69a00ba17..fa0526d1b 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -30,6 +30,7 @@ type Proxy interface { Name() string Type() AdapterType Generator(metadata *Metadata) (ProxyAdapter, error) + MarshalJSON() ([]byte, error) } // AdapterType is enum of adapter type diff --git a/constant/config.go b/constant/config.go deleted file mode 100644 index f0b93fe23..000000000 --- a/constant/config.go +++ /dev/null @@ -1,10 +0,0 @@ -package constant - -type General struct { - Mode *string `json:"mode,omitempty"` - AllowLan *bool `json:"allow-lan,omitempty"` - Port *int `json:"port,omitempty"` - SocksPort *int `json:"socks-port,omitempty"` - RedirPort *int `json:"redir-port,omitempty"` - LogLevel *string `json:"log-level,omitempty"` -} diff --git a/constant/log.go b/constant/log.go deleted file mode 100644 index 4423322e9..000000000 --- a/constant/log.go +++ /dev/null @@ -1,35 +0,0 @@ -package constant - -var ( - // LogLevelMapping is a mapping for LogLevel enum - LogLevelMapping = map[string]LogLevel{ - "error": ERROR, - "warning": WARNING, - "info": INFO, - "debug": DEBUG, - } -) - -const ( - ERROR LogLevel = iota - WARNING - INFO - DEBUG -) - -type LogLevel int - -func (l LogLevel) String() string { - switch l { - case INFO: - return "info" - case WARNING: - return "warning" - case ERROR: - return "error" - case DEBUG: - return "debug" - default: - return "unknow" - } -} diff --git a/constant/proxy.go b/constant/proxy.go deleted file mode 100644 index 302e92c32..000000000 --- a/constant/proxy.go +++ /dev/null @@ -1,7 +0,0 @@ -package constant - -// ProxySignal is used to handle graceful shutdown of proxy -type ProxySignal struct { - Done chan<- struct{} - Closed <-chan struct{} -} diff --git a/hub/common.go b/hub/common.go deleted file mode 100644 index 4898d2260..000000000 --- a/hub/common.go +++ /dev/null @@ -1,33 +0,0 @@ -package hub - -import ( - "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/proxy" - T "github.com/Dreamacro/clash/tunnel" -) - -var ( - tunnel = T.Instance() - cfg = config.Instance() - listener = proxy.Instance() -) - -type Error struct { - Error string `json:"error"` -} - -type Errors struct { - Errors map[string]string `json:"errors"` -} - -func formatErrors(errorsMap map[string]error) (bool, Errors) { - errors := make(map[string]string) - hasError := false - for key, err := range errorsMap { - if err != nil { - errors[key] = err.Error() - hasError = true - } - } - return hasError, Errors{Errors: errors} -} diff --git a/hub/configs.go b/hub/configs.go deleted file mode 100644 index 499648371..000000000 --- a/hub/configs.go +++ /dev/null @@ -1,96 +0,0 @@ -package hub - -import ( - "fmt" - "net/http" - - "github.com/Dreamacro/clash/config" - C "github.com/Dreamacro/clash/constant" - - "github.com/go-chi/chi" - "github.com/go-chi/render" -) - -func configRouter() http.Handler { - r := chi.NewRouter() - r.Get("/", getConfigs) - r.Put("/", updateConfigs) - return r -} - -type configSchema struct { - Port int `json:"port"` - SocksPort int `json:"socket-port"` - RedirPort int `json:"redir-port"` - AllowLan bool `json:"allow-lan"` - Mode string `json:"mode"` - LogLevel string `json:"log-level"` -} - -func getConfigs(w http.ResponseWriter, r *http.Request) { - general := cfg.General() - render.JSON(w, r, configSchema{ - Port: general.Port, - SocksPort: general.SocksPort, - RedirPort: general.RedirPort, - AllowLan: general.AllowLan, - Mode: general.Mode.String(), - LogLevel: general.LogLevel.String(), - }) -} - -func updateConfigs(w http.ResponseWriter, r *http.Request) { - general := &C.General{} - err := render.DecodeJSON(r.Body, general) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: "Format error", - }) - return - } - - // update errors - var modeErr, logLevelErr error - - // update mode - if general.Mode != nil { - mode, ok := config.ModeMapping[*general.Mode] - if !ok { - modeErr = fmt.Errorf("Mode error") - } else { - cfg.SetMode(mode) - } - } - - // update log-level - if general.LogLevel != nil { - level, ok := C.LogLevelMapping[*general.LogLevel] - if !ok { - logLevelErr = fmt.Errorf("Log Level error") - } else { - cfg.SetLogLevel(level) - } - } - - hasError, errors := formatErrors(map[string]error{ - "mode": modeErr, - "log-level": logLevelErr, - }) - - if hasError { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, errors) - return - } - - // update proxy - cfg.UpdateProxy(config.ProxyConfig{ - AllowLan: general.AllowLan, - Port: general.Port, - SocksPort: general.SocksPort, - RedirPort: general.RedirPort, - }) - - w.WriteHeader(http.StatusNoContent) -} diff --git a/hub/executor/executor.go b/hub/executor/executor.go new file mode 100644 index 000000000..541c782d8 --- /dev/null +++ b/hub/executor/executor.go @@ -0,0 +1,66 @@ +package executor + +import ( + "github.com/Dreamacro/clash/config" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" + P "github.com/Dreamacro/clash/proxy" + T "github.com/Dreamacro/clash/tunnel" +) + +// Parse config with default config path +func Parse() (*config.Config, error) { + return ParseWithPath(C.Path.Config()) +} + +// ParseWithPath parse config with custom config path +func ParseWithPath(path string) (*config.Config, error) { + return config.Parse(path) +} + +// ApplyConfig dispatch configure to all parts +func ApplyConfig(cfg *config.Config) { + updateProxies(cfg.Proxies) + updateRules(cfg.Rules) + updateGeneral(cfg.General) +} + +func GetGeneral() *config.General { + ports := P.GetPorts() + return &config.General{ + Port: ports.Port, + SocksPort: ports.SocksPort, + RedirPort: ports.RedirPort, + AllowLan: P.AllowLan(), + Mode: T.Instance().Mode(), + LogLevel: log.Level(), + } +} + +func updateProxies(proxies map[string]C.Proxy) { + T.Instance().UpdateProxies(proxies) +} + +func updateRules(rules []C.Rule) { + T.Instance().UpdateRules(rules) +} + +func updateGeneral(general *config.General) { + allowLan := general.AllowLan + + P.SetAllowLan(allowLan) + if err := P.ReCreateHTTP(general.Port); err != nil { + log.Errorln("Start HTTP server error: %s", err.Error()) + } + + if err := P.ReCreateSocks(general.SocksPort); err != nil { + log.Errorln("Start SOCKS5 server error: %s", err.Error()) + } + + if err := P.ReCreateRedir(general.RedirPort); err != nil { + log.Errorln("Start Redir server error: %s", err.Error()) + } + + log.SetLevel(general.LogLevel) + T.Instance().SetMode(general.Mode) +} diff --git a/hub/hub.go b/hub/hub.go new file mode 100644 index 000000000..4a3715e5f --- /dev/null +++ b/hub/hub.go @@ -0,0 +1,21 @@ +package hub + +import ( + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/hub/route" +) + +// Parse call at the beginning of clash +func Parse() error { + cfg, err := executor.Parse() + if err != nil { + return err + } + + if cfg.General.ExternalController != "" { + go route.Start(cfg.General.ExternalController, cfg.General.Secret) + } + + executor.ApplyConfig(cfg) + return nil +} diff --git a/hub/proxies.go b/hub/proxies.go deleted file mode 100644 index 3a54ed8bf..000000000 --- a/hub/proxies.go +++ /dev/null @@ -1,217 +0,0 @@ -package hub - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strconv" - "time" - - A "github.com/Dreamacro/clash/adapters/outbound" - C "github.com/Dreamacro/clash/constant" - - "github.com/go-chi/chi" - "github.com/go-chi/render" -) - -func proxyRouter() http.Handler { - r := chi.NewRouter() - r.Get("/", getProxies) - r.With(parseProxyName).Get("/{name}", getProxy) - r.With(parseProxyName).Get("/{name}/delay", getProxyDelay) - r.With(parseProxyName).Put("/{name}", updateProxy) - return r -} - -// When name is composed of a partial escape string, Golang does not unescape it -func parseProxyName(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - name := chi.URLParam(r, "name") - if newName, err := url.PathUnescape(name); err == nil { - name = newName - } - ctx := context.WithValue(r.Context(), contextKey("proxy name"), name) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -type SampleProxy struct { - Type string `json:"type"` -} - -type Selector struct { - Type string `json:"type"` - Now string `json:"now"` - All []string `json:"all"` -} - -type URLTest struct { - Type string `json:"type"` - Now string `json:"now"` -} - -type Fallback struct { - Type string `json:"type"` - Now string `json:"now"` -} - -func transformProxy(proxy C.Proxy) interface{} { - t := proxy.Type() - switch t { - case C.Selector: - selector := proxy.(*A.Selector) - return Selector{ - Type: t.String(), - Now: selector.Now(), - All: selector.All(), - } - case C.URLTest: - return URLTest{ - Type: t.String(), - Now: proxy.(*A.URLTest).Now(), - } - case C.Fallback: - return Fallback{ - Type: t.String(), - Now: proxy.(*A.Fallback).Now(), - } - default: - return SampleProxy{ - Type: proxy.Type().String(), - } - } -} - -type GetProxiesResponse struct { - Proxies map[string]interface{} `json:"proxies"` -} - -func getProxies(w http.ResponseWriter, r *http.Request) { - rawProxies := cfg.Proxies() - proxies := make(map[string]interface{}) - for name, proxy := range rawProxies { - proxies[name] = transformProxy(proxy) - } - render.JSON(w, r, GetProxiesResponse{Proxies: proxies}) -} - -func getProxy(w http.ResponseWriter, r *http.Request) { - name := r.Context().Value(contextKey("proxy name")).(string) - proxies := cfg.Proxies() - proxy, exist := proxies[name] - if !exist { - w.WriteHeader(http.StatusNotFound) - render.JSON(w, r, Error{ - Error: "Proxy not found", - }) - return - } - render.JSON(w, r, transformProxy(proxy)) -} - -type UpdateProxyRequest struct { - Name string `json:"name"` -} - -func updateProxy(w http.ResponseWriter, r *http.Request) { - req := UpdateProxyRequest{} - if err := render.DecodeJSON(r.Body, &req); err != nil { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: "Format error", - }) - return - } - - name := r.Context().Value(contextKey("proxy name")).(string) - proxies := cfg.Proxies() - proxy, exist := proxies[name] - if !exist { - w.WriteHeader(http.StatusNotFound) - render.JSON(w, r, Error{ - Error: "Proxy not found", - }) - return - } - - selector, ok := proxy.(*A.Selector) - if !ok { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: "Proxy can't update", - }) - return - } - - if err := selector.Set(req.Name); err != nil { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: fmt.Sprintf("Selector update error: %s", err.Error()), - }) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -type GetProxyDelayRequest struct { - URL string `json:"url"` - Timeout int16 `json:"timeout"` -} - -type GetProxyDelayResponse struct { - Delay int16 `json:"delay"` -} - -func getProxyDelay(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - url := query.Get("url") - timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: "Format error", - }) - return - } - - name := r.Context().Value(contextKey("proxy name")).(string) - proxies := cfg.Proxies() - proxy, exist := proxies[name] - if !exist { - w.WriteHeader(http.StatusNotFound) - render.JSON(w, r, Error{ - Error: "Proxy not found", - }) - return - } - - sigCh := make(chan int16) - go func() { - t, err := A.DelayTest(proxy, url) - if err != nil { - sigCh <- 0 - } - sigCh <- t - }() - - select { - case <-time.After(time.Millisecond * time.Duration(timeout)): - w.WriteHeader(http.StatusRequestTimeout) - render.JSON(w, r, Error{ - Error: "Proxy delay test timeout", - }) - case t := <-sigCh: - if t == 0 { - w.WriteHeader(http.StatusServiceUnavailable) - render.JSON(w, r, Error{ - Error: "An error occurred in the delay test", - }) - } else { - render.JSON(w, r, GetProxyDelayResponse{ - Delay: t, - }) - } - } -} diff --git a/hub/route/configs.go b/hub/route/configs.go new file mode 100644 index 000000000..66d6cb433 --- /dev/null +++ b/hub/route/configs.go @@ -0,0 +1,69 @@ +package route + +import ( + "net/http" + + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/log" + T "github.com/Dreamacro/clash/tunnel" + P "github.com/Dreamacro/clash/proxy" + + "github.com/go-chi/chi" + "github.com/go-chi/render" +) + +func configRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getConfigs) + return r +} + +type configSchema struct { + Port *int `json:"port"` + SocksPort *int `json:"socket-port"` + RedirPort *int `json:"redir-port"` + AllowLan *bool `json:"allow-lan"` + Mode *T.Mode `json:"mode"` + LogLevel *log.LogLevel `json:"log-level"` +} + +func getConfigs(w http.ResponseWriter, r *http.Request) { + general := executor.GetGeneral() + render.Respond(w, r, general) +} + +func pointerOrDefault (p *int, def int) int { + if p != nil { + return *p + } + + return def +} + +func updateConfigs(w http.ResponseWriter, r *http.Request) { + general := &configSchema{} + if err := render.DecodeJSON(r.Body, general); err != nil { + w.WriteHeader(http.StatusBadRequest) + render.Respond(w, r, ErrBadRequest) + return + } + + if general.AllowLan != nil { + P.SetAllowLan(*general.AllowLan) + } + + ports := P.GetPorts() + P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port)) + P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort)) + P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort)) + + if general.Mode != nil { + T.Instance().SetMode(*general.Mode) + } + + if general.LogLevel != nil { + log.SetLevel(*general.LogLevel) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/hub/route/ctxkeys.go b/hub/route/ctxkeys.go new file mode 100644 index 000000000..079329718 --- /dev/null +++ b/hub/route/ctxkeys.go @@ -0,0 +1,12 @@ +package route + +var ( + CtxKeyProxyName = contextKey("proxy name") + CtxKeyProxy = contextKey("proxy") +) + +type contextKey string + +func (c contextKey) String() string { + return "clash context key " + string(c) +} diff --git a/hub/route/errors.go b/hub/route/errors.go new file mode 100644 index 000000000..b469e107f --- /dev/null +++ b/hub/route/errors.go @@ -0,0 +1,22 @@ +package route + +var ( + ErrUnauthorized = newError("Unauthorized") + ErrBadRequest = newError("Body invalid") + ErrForbidden = newError("Forbidden") + ErrNotFound = newError("Resource not found") + ErrRequestTimeout = newError("Timeout") +) + +// HTTPError is custom HTTP error for API +type HTTPError struct { + Message string `json:"message"` +} + +func (e *HTTPError) Error() string { + return e.Message +} + +func newError(msg string) *HTTPError { + return &HTTPError{Message: msg} +} diff --git a/hub/route/proxies.go b/hub/route/proxies.go new file mode 100644 index 000000000..65beac1ec --- /dev/null +++ b/hub/route/proxies.go @@ -0,0 +1,137 @@ +package route + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + A "github.com/Dreamacro/clash/adapters/outbound" + C "github.com/Dreamacro/clash/constant" + T "github.com/Dreamacro/clash/tunnel" + + "github.com/go-chi/chi" + "github.com/go-chi/render" +) + +func proxyRouter() http.Handler { + r := chi.NewRouter() + r.Get("/", getProxies) + + r.Route("/{name}", func(r chi.Router) { + r.Use(parseProxyName, findProxyByName) + r.Get("/", getProxy) + r.Get("/delay", getProxyDelay) + r.Put("/", updateProxy) + }) + return r +} + +// When name is composed of a partial escape string, Golang does not unescape it +func parseProxyName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + if newName, err := url.PathUnescape(name); err == nil { + name = newName + } + ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func findProxyByName(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + name := r.Context().Value(CtxKeyProxyName).(string) + proxies := T.Instance().Proxies() + proxy, exist := proxies[name] + if !exist { + w.WriteHeader(http.StatusNotFound) + render.Respond(w, r, ErrNotFound) + return + } + + ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getProxies(w http.ResponseWriter, r *http.Request) { + proxies := T.Instance().Proxies() + render.Respond(w, r, map[string]map[string]C.Proxy{ + "proxies": proxies, + }) +} + +func getProxy(w http.ResponseWriter, r *http.Request) { + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + render.Respond(w, r, proxy) +} + +type UpdateProxyRequest struct { + Name string `json:"name"` +} + +func updateProxy(w http.ResponseWriter, r *http.Request) { + req := UpdateProxyRequest{} + if err := render.DecodeJSON(r.Body, &req); err != nil { + w.WriteHeader(http.StatusBadRequest) + render.Respond(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + + selector, ok := proxy.(*A.Selector) + if !ok { + w.WriteHeader(http.StatusBadRequest) + render.Respond(w, r, ErrBadRequest) + return + } + + if err := selector.Set(req.Name); err != nil { + w.WriteHeader(http.StatusBadRequest) + render.Respond(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error()))) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func getProxyDelay(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + url := query.Get("url") + timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + render.Respond(w, r, ErrBadRequest) + return + } + + proxy := r.Context().Value(CtxKeyProxy).(C.Proxy) + + sigCh := make(chan int16) + go func() { + t, err := A.DelayTest(proxy, url) + if err != nil { + sigCh <- 0 + } + sigCh <- t + }() + + select { + case <-time.After(time.Millisecond * time.Duration(timeout)): + w.WriteHeader(http.StatusRequestTimeout) + render.Respond(w, r, ErrRequestTimeout) + case t := <-sigCh: + if t == 0 { + w.WriteHeader(http.StatusServiceUnavailable) + render.Respond(w, r, newError("An error occurred in the delay test")) + } else { + render.Respond(w, r, map[string]int16{ + "delay": t, + }) + } + } +} diff --git a/hub/rules.go b/hub/route/rules.go similarity index 56% rename from hub/rules.go rename to hub/route/rules.go index c91120e8f..02e19b9bd 100644 --- a/hub/rules.go +++ b/hub/route/rules.go @@ -1,8 +1,10 @@ -package hub +package route import ( "net/http" + T "github.com/Dreamacro/clash/tunnel" + "github.com/go-chi/chi" "github.com/go-chi/render" ) @@ -10,7 +12,6 @@ import ( func ruleRouter() http.Handler { r := chi.NewRouter() r.Get("/", getRules) - r.Put("/", updateRules) return r } @@ -20,12 +21,8 @@ type Rule struct { Proxy string `json:"proxy"` } -type GetRulesResponse struct { - Rules []Rule `json:"rules"` -} - func getRules(w http.ResponseWriter, r *http.Request) { - rawRules := cfg.Rules() + rawRules := T.Instance().Rules() var rules []Rule for _, rule := range rawRules { @@ -37,19 +34,7 @@ func getRules(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - render.JSON(w, r, GetRulesResponse{ - Rules: rules, + render.Respond(w, r, map[string][]Rule{ + "rules": rules, }) } - -func updateRules(w http.ResponseWriter, r *http.Request) { - err := cfg.UpdateRules() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - render.JSON(w, r, Error{ - Error: err.Error(), - }) - return - } - w.WriteHeader(http.StatusNoContent) -} diff --git a/hub/server.go b/hub/route/server.go similarity index 62% rename from hub/server.go rename to hub/route/server.go index 304f991e8..72fe96556 100644 --- a/hub/server.go +++ b/hub/route/server.go @@ -1,4 +1,4 @@ -package hub +package route import ( "encoding/json" @@ -6,44 +6,32 @@ import ( "strings" "time" - "github.com/Dreamacro/clash/config" - C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" T "github.com/Dreamacro/clash/tunnel" "github.com/go-chi/chi" "github.com/go-chi/cors" "github.com/go-chi/render" - log "github.com/sirupsen/logrus" ) -var secret = "" +var ( + serverSecret = "" + serverAddr = "" +) type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` } -func newHub(signal chan struct{}) { - var addr string - ch := config.Instance().Subscribe() - signal <- struct{}{} - count := 0 - for { - elm := <-ch - event := elm.(*config.Event) - switch event.Type { - case "external-controller": - addr = event.Payload.(string) - count++ - case "secret": - secret = event.Payload.(string) - count++ - } - if count == 2 { - break - } +func Start(addr string, secret string) { + if serverAddr != "" { + return } + serverAddr = addr + serverSecret = secret + r := chi.NewRouter() cors := cors.New(cors.Options{ @@ -59,12 +47,12 @@ func newHub(signal chan struct{}) { r.With(jsonContentType).Get("/logs", getLogs) r.Mount("/configs", configRouter()) r.Mount("/proxies", proxyRouter()) - r.Mount("/rules", ruleRouter()) + // r.Mount("/rules", ruleRouter()) - log.Infof("RESTful API listening at: %s", addr) + log.Infoln("RESTful API listening at: %s", addr) err := http.ListenAndServe(addr, r) if err != nil { - log.Errorf("External controller error: %s", err.Error()) + log.Errorln("External controller error: %s", err.Error()) } } @@ -81,18 +69,16 @@ func authentication(next http.Handler) http.Handler { header := r.Header.Get("Authorization") text := strings.SplitN(header, " ", 2) - if secret == "" { + if serverSecret == "" { next.ServeHTTP(w, r) return } hasUnvalidHeader := text[0] != "Bearer" - hasUnvalidSecret := len(text) == 2 && text[1] != secret + hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret if hasUnvalidHeader || hasUnvalidSecret { w.WriteHeader(http.StatusUnauthorized) - render.JSON(w, r, Error{ - Error: "Authentication failed", - }) + render.Respond(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) @@ -100,17 +86,11 @@ func authentication(next http.Handler) http.Handler { return http.HandlerFunc(fn) } -type contextKey string - -func (c contextKey) String() string { - return "clash context key " + string(c) -} - func traffic(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) tick := time.NewTicker(time.Second) - t := tunnel.Traffic() + t := T.Instance().Traffic() for range tick.C { up, down := t.Now() if err := json.NewEncoder(w).Encode(Traffic{ @@ -134,29 +114,18 @@ func getLogs(w http.ResponseWriter, r *http.Request) { levelText = "info" } - level, ok := C.LogLevelMapping[levelText] + level, ok := log.LogLevelMapping[levelText] if !ok { w.WriteHeader(http.StatusBadRequest) - render.JSON(w, r, Error{ - Error: "Level error", - }) + render.Respond(w, r, ErrBadRequest) return } - src := tunnel.Log() - sub, err := src.Subscribe() - defer src.UnSubscribe(sub) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - render.JSON(w, r, Error{ - Error: err.Error(), - }) - return - } + sub := log.Subscribe() render.Status(r, http.StatusOK) for elm := range sub { - log := elm.(T.Log) - if log.LogLevel > level { + log := elm.(*log.Event) + if log.LogLevel < level { continue } @@ -169,10 +138,3 @@ func getLogs(w http.ResponseWriter, r *http.Request) { w.(http.Flusher).Flush() } } - -// Run initial hub -func Run() { - signal := make(chan struct{}) - go newHub(signal) - <-signal -} diff --git a/log/level.go b/log/level.go new file mode 100644 index 000000000..4e28e7143 --- /dev/null +++ b/log/level.go @@ -0,0 +1,76 @@ +package log + +import ( + "encoding/json" + "errors" + + yaml "gopkg.in/yaml.v2" +) + +var ( + // LogLevelMapping is a mapping for LogLevel enum + LogLevelMapping = map[string]LogLevel{ + "error": ERROR, + "warning": WARNING, + "info": INFO, + "debug": DEBUG, + } +) + +const ( + DEBUG LogLevel = iota + INFO + WARNING + ERROR +) + +type LogLevel int + +// UnmarshalYAML unserialize Mode with yaml +func (l *LogLevel) UnmarshalYAML(data []byte) error { + var tp string + yaml.Unmarshal(data, &tp) + level, exist := LogLevelMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *l = level + return nil +} + +// MarshalYAML serialize Mode with yaml +func (l LogLevel) MarshalYAML() ([]byte, error) { + return yaml.Marshal(l.String()) +} + +// UnmarshalJSON unserialize Mode with json +func (l *LogLevel) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, tp) + level, exist := LogLevelMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *l = level + return nil +} + +// MarshalJSON serialize Mode with json +func (l LogLevel) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +func (l LogLevel) String() string { + switch l { + case INFO: + return "info" + case WARNING: + return "warning" + case ERROR: + return "error" + case DEBUG: + return "debug" + default: + return "unknow" + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 000000000..c80606610 --- /dev/null +++ b/log/log.go @@ -0,0 +1,85 @@ +package log + +import ( + "fmt" + + "github.com/Dreamacro/clash/common/observable" + + log "github.com/sirupsen/logrus" +) + +var ( + logCh = make(chan interface{}) + source = observable.NewObservable(logCh) + level = INFO +) + +type Event struct { + LogLevel LogLevel + Payload string +} + +func (e *Event) Type() string { + return e.LogLevel.String() +} + +func Infoln(format string, v ...interface{}) { + event := newLog(INFO, format, v...) + logCh <- event + print(event) +} + +func Warnln(format string, v ...interface{}) { + event := newLog(WARNING, format, v...) + logCh <- event + print(event) +} + +func Errorln(format string, v ...interface{}) { + event := newLog(ERROR, format, v...) + logCh <- event + print(event) +} + +func Debugln(format string, v ...interface{}) { + event := newLog(DEBUG, format, v...) + logCh <- event + print(event) +} + +func Subscribe() observable.Subscription { + sub, _ := source.Subscribe() + return sub +} + +func Level() LogLevel { + return level +} + +func SetLevel(newLevel LogLevel) { + level = newLevel +} + +func print(data *Event) { + if data.LogLevel < level { + return + } + + switch data.LogLevel { + case INFO: + log.Infoln(data.Payload) + case WARNING: + log.Warnln(data.Payload) + case ERROR: + log.Errorln(data.Payload) + case DEBUG: + log.Debugln(data.Payload) + } +} + +func newLog(logLevel LogLevel, format string, v ...interface{}) *Event { + return &Event{ + LogLevel: logLevel, + Payload: fmt.Sprintf(format, v...), + } +} diff --git a/main.go b/main.go index b48106251..be91250ac 100644 --- a/main.go +++ b/main.go @@ -10,8 +10,6 @@ import ( "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/hub" - "github.com/Dreamacro/clash/proxy" - "github.com/Dreamacro/clash/tunnel" log "github.com/sirupsen/logrus" ) @@ -26,10 +24,6 @@ func init() { } func main() { - tunnel.Instance().Run() - proxy.Instance().Run() - hub.Run() - if homedir != "" { if !filepath.IsAbs(homedir) { currentDir, _ := os.Getwd() @@ -38,9 +32,11 @@ func main() { C.SetHomeDir(homedir) } - config.Init() - err := config.Instance().Parse() - if err != nil { + if err := config.Init(C.Path.HomeDir()); err != nil { + log.Fatalf("Initial configuration directory error: %s", err.Error()) + } + + if err := hub.Parse(); err != nil { log.Fatalf("Parse config error: %s", err.Error()) } diff --git a/proxy/http/server.go b/proxy/http/server.go index 598300589..db57b2499 100644 --- a/proxy/http/server.go +++ b/proxy/http/server.go @@ -6,31 +6,25 @@ import ( "net/http" "github.com/Dreamacro/clash/adapters/inbound" - C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" - - log "github.com/sirupsen/logrus" ) var ( tun = tunnel.Instance() ) -func NewHttpProxy(addr string) (*C.ProxySignal, error) { +func NewHttpProxy(addr string) (chan<- struct{}, <-chan struct{}, error) { l, err := net.Listen("tcp", addr) if err != nil { - return nil, err + return nil, nil, err } done := make(chan struct{}) closed := make(chan struct{}) - signal := &C.ProxySignal{ - Done: done, - Closed: closed, - } go func() { - log.Infof("HTTP proxy listening at: %s", addr) + log.Infoln("HTTP proxy listening at: %s", addr) for { c, err := l.Accept() if err != nil { @@ -45,12 +39,11 @@ func NewHttpProxy(addr string) (*C.ProxySignal, error) { go func() { <-done - close(done) l.Close() closed <- struct{}{} }() - return signal, nil + return done, closed, nil } func handleConn(conn net.Conn) { diff --git a/proxy/listener.go b/proxy/listener.go index 06e658a32..5e635ba55 100644 --- a/proxy/listener.go +++ b/proxy/listener.go @@ -1,116 +1,166 @@ package proxy import ( - "sync" + "fmt" + "net" + "strconv" - "github.com/Dreamacro/clash/config" - C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/proxy/http" "github.com/Dreamacro/clash/proxy/redir" "github.com/Dreamacro/clash/proxy/socks" ) var ( - listener *Listener - once sync.Once + allowLan = false + + socksListener *listener + httpListener *listener + redirListener *listener ) -type Listener struct { - // signal for update - httpSignal *C.ProxySignal - socksSignal *C.ProxySignal - redirSignal *C.ProxySignal +type listener struct { + Address string + Done chan<- struct{} + Closed <-chan struct{} } -func (l *Listener) updateHTTP(addr string) error { - if l.httpSignal != nil { - signal := l.httpSignal - signal.Done <- struct{}{} - <-signal.Closed - l.httpSignal = nil - } - - signal, err := http.NewHttpProxy(addr) - if err != nil { - return err - } - - l.httpSignal = signal - return nil +type Ports struct { + Port int `json:"port"` + SocksPort int `json:"socks-port"` + RedirPort int `json:"redir-port"` } -func (l *Listener) updateSocks(addr string) error { - if l.socksSignal != nil { - signal := l.socksSignal - signal.Done <- struct{}{} - <-signal.Closed - l.socksSignal = nil - } - - signal, err := socks.NewSocksProxy(addr) - if err != nil { - return err - } - - l.socksSignal = signal - return nil +func AllowLan() bool { + return allowLan } -func (l *Listener) updateRedir(addr string) error { - if l.redirSignal != nil { - signal := l.redirSignal - signal.Done <- struct{}{} - <-signal.Closed - l.redirSignal = nil - } - - signal, err := redir.NewRedirProxy(addr) - if err != nil { - return err - } - - l.redirSignal = signal - return nil +func SetAllowLan(al bool) { + allowLan = al } -func (l *Listener) process(signal chan<- struct{}) { - sub := config.Instance().Subscribe() - signal <- struct{}{} - reportCH := config.Instance().Report() - for elm := range sub { - event := elm.(*config.Event) - switch event.Type { - case "http-addr": - addr := event.Payload.(string) - err := l.updateHTTP(addr) - reportCH <- &config.Event{Type: "http-addr", Payload: err == nil} - case "socks-addr": - addr := event.Payload.(string) - err := l.updateSocks(addr) - reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil} - case "redir-addr": - addr := event.Payload.(string) - err := l.updateRedir(addr) - reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil} +func ReCreateHTTP(port int) error { + addr := genAddr(port, allowLan) + + if httpListener != nil { + if httpListener.Address == addr { + return nil } + httpListener.Done <- struct{}{} + <-httpListener.Closed + httpListener = nil } + + if portIsZero(addr) { + return nil + } + + done, closed, err := http.NewHttpProxy(addr) + if err != nil { + return err + } + + httpListener = &listener{ + Address: addr, + Done: done, + Closed: closed, + } + return nil } -// Run ensure config monitoring -func (l *Listener) Run() { - signal := make(chan struct{}) - go l.process(signal) - <-signal +func ReCreateSocks(port int) error { + addr := genAddr(port, allowLan) + + if socksListener != nil { + if socksListener.Address == addr { + return nil + } + socksListener.Done <- struct{}{} + <-socksListener.Closed + socksListener = nil + } + + if portIsZero(addr) { + return nil + } + + done, closed, err := socks.NewSocksProxy(addr) + if err != nil { + return err + } + + socksListener = &listener{ + Address: addr, + Done: done, + Closed: closed, + } + return nil } -func newListener() *Listener { - return &Listener{} +func ReCreateRedir(port int) error { + addr := genAddr(port, allowLan) + + if redirListener != nil { + if redirListener.Address == addr { + return nil + } + redirListener.Done <- struct{}{} + <-redirListener.Closed + redirListener = nil + } + + if portIsZero(addr) { + return nil + } + + done, closed, err := redir.NewRedirProxy(addr) + if err != nil { + return err + } + + redirListener = &listener{ + Address: addr, + Done: done, + Closed: closed, + } + return nil } -// Instance return singleton instance of Listener -func Instance() *Listener { - once.Do(func() { - listener = newListener() - }) - return listener +// GetPorts return the ports of proxy servers +func GetPorts() *Ports { + ports := &Ports{} + + if httpListener != nil { + _, portStr, _ := net.SplitHostPort(httpListener.Address) + port, _ := strconv.Atoi(portStr) + ports.Port = port + } + + if socksListener != nil { + _, portStr, _ := net.SplitHostPort(socksListener.Address) + port, _ := strconv.Atoi(portStr) + ports.SocksPort = port + } + + if redirListener != nil { + _, portStr, _ := net.SplitHostPort(redirListener.Address) + port, _ := strconv.Atoi(portStr) + ports.RedirPort = port + } + + return ports +} + +func portIsZero(addr string) bool { + _, port, err := net.SplitHostPort(addr) + if port == "0" || port == "" || err != nil { + return true + } + return false +} + +func genAddr(port int, allowLan bool) string { + if allowLan { + return fmt.Sprintf(":%d", port) + } + return fmt.Sprintf("127.0.0.1:%d", port) } diff --git a/proxy/redir/tcp.go b/proxy/redir/tcp.go index f4b47f4b7..bb6d3c552 100644 --- a/proxy/redir/tcp.go +++ b/proxy/redir/tcp.go @@ -4,7 +4,6 @@ import ( "net" "github.com/Dreamacro/clash/adapters/inbound" - C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/tunnel" log "github.com/sirupsen/logrus" @@ -14,18 +13,14 @@ var ( tun = tunnel.Instance() ) -func NewRedirProxy(addr string) (*C.ProxySignal, error) { +func NewRedirProxy(addr string) (chan<- struct{}, <-chan struct{}, error) { l, err := net.Listen("tcp", addr) if err != nil { - return nil, err + return nil, nil, err } done := make(chan struct{}) closed := make(chan struct{}) - signal := &C.ProxySignal{ - Done: done, - Closed: closed, - } go func() { log.Infof("Redir proxy listening at: %s", addr) @@ -43,12 +38,11 @@ func NewRedirProxy(addr string) (*C.ProxySignal, error) { go func() { <-done - close(done) l.Close() closed <- struct{}{} }() - return signal, nil + return done, closed, nil } func handleRedir(conn net.Conn) { diff --git a/proxy/socks/tcp.go b/proxy/socks/tcp.go index 57645dc31..646050be1 100644 --- a/proxy/socks/tcp.go +++ b/proxy/socks/tcp.go @@ -4,7 +4,6 @@ import ( "net" "github.com/Dreamacro/clash/adapters/inbound" - C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/go-shadowsocks2/socks" @@ -15,18 +14,14 @@ var ( tun = tunnel.Instance() ) -func NewSocksProxy(addr string) (*C.ProxySignal, error) { +func NewSocksProxy(addr string) (chan<- struct{}, <-chan struct{}, error) { l, err := net.Listen("tcp", addr) if err != nil { - return nil, err + return nil, nil, err } done := make(chan struct{}) closed := make(chan struct{}) - signal := &C.ProxySignal{ - Done: done, - Closed: closed, - } go func() { log.Infof("SOCKS proxy listening at: %s", addr) @@ -44,12 +39,11 @@ func NewSocksProxy(addr string) (*C.ProxySignal, error) { go func() { <-done - close(done) l.Close() closed <- struct{}{} }() - return signal, nil + return done, closed, nil } func handleSocks(conn net.Conn) { diff --git a/tunnel/log.go b/tunnel/log.go deleted file mode 100644 index f703d62c4..000000000 --- a/tunnel/log.go +++ /dev/null @@ -1,51 +0,0 @@ -package tunnel - -import ( - "fmt" - - C "github.com/Dreamacro/clash/constant" - - log "github.com/sirupsen/logrus" -) - -type Log struct { - LogLevel C.LogLevel - Payload string -} - -func (l *Log) Type() string { - return l.LogLevel.String() -} - -func print(data Log) { - switch data.LogLevel { - case C.INFO: - log.Infoln(data.Payload) - case C.WARNING: - log.Warnln(data.Payload) - case C.ERROR: - log.Errorln(data.Payload) - case C.DEBUG: - log.Debugln(data.Payload) - } -} - -func (t *Tunnel) subscribeLogs() { - sub, err := t.observable.Subscribe() - if err != nil { - log.Fatalf("Can't subscribe tunnel log: %s", err.Error()) - } - for elm := range sub { - data := elm.(Log) - if data.LogLevel <= t.logLevel { - print(data) - } - } -} - -func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log { - return Log{ - LogLevel: logLevel, - Payload: fmt.Sprintf(format, v...), - } -} diff --git a/tunnel/mode.go b/tunnel/mode.go new file mode 100644 index 000000000..b367ed3ad --- /dev/null +++ b/tunnel/mode.go @@ -0,0 +1,53 @@ +package tunnel + +import ( + "encoding/json" + "errors" +) + +type Mode int + +var ( + // ModeMapping is a mapping for Mode enum + ModeMapping = map[string]Mode{ + "Global": Global, + "Rule": Rule, + "Direct": Direct, + } +) + +const ( + Global Mode = iota + Rule + Direct +) + +// UnmarshalJSON unserialize Mode +func (m *Mode) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, tp) + mode, exist := ModeMapping[tp] + if !exist { + return errors.New("invalid mode") + } + *m = mode + return nil +} + +// MarshalJSON serialize Mode +func (m Mode) MarshalJSON() ([]byte, error) { + return json.Marshal(m.String()) +} + +func (m Mode) String() string { + switch m { + case Global: + return "Global" + case Rule: + return "Rule" + case Direct: + return "Direct" + default: + return "Unknow" + } +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index fb4325540..c574adadd 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -5,9 +5,8 @@ import ( "time" InboundAdapter "github.com/Dreamacro/clash/adapters/inbound" - "github.com/Dreamacro/clash/common/observable" - cfg "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "gopkg.in/eapache/channels.v1" ) @@ -26,12 +25,7 @@ type Tunnel struct { traffic *C.Traffic // Outbound Rule - mode cfg.Mode - - // Log - logCh chan interface{} - observable *observable.Observable - logLevel C.LogLevel + mode Mode } // Add request to queue @@ -44,33 +38,38 @@ func (t *Tunnel) Traffic() *C.Traffic { return t.traffic } -// Log return clash log stream -func (t *Tunnel) Log() *observable.Observable { - return t.observable +// Rules return all rules +func (t *Tunnel) Rules() []C.Rule { + return t.rules } -func (t *Tunnel) configMonitor(signal chan<- struct{}) { - sub := cfg.Instance().Subscribe() - signal <- struct{}{} - for elm := range sub { - event := elm.(*cfg.Event) - switch event.Type { - case "proxies": - proxies := event.Payload.(map[string]C.Proxy) - t.configLock.Lock() - t.proxies = proxies - t.configLock.Unlock() - case "rules": - rules := event.Payload.([]C.Rule) - t.configLock.Lock() - t.rules = rules - t.configLock.Unlock() - case "mode": - t.mode = event.Payload.(cfg.Mode) - case "log-level": - t.logLevel = event.Payload.(C.LogLevel) - } - } +// UpdateRules handle update rules +func (t *Tunnel) UpdateRules(rules []C.Rule) { + t.configLock.Lock() + t.rules = rules + t.configLock.Unlock() +} + +// Proxies return all proxies +func (t *Tunnel) Proxies() map[string]C.Proxy { + return t.proxies +} + +// UpdateProxies handle update proxies +func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy) { + t.configLock.Lock() + t.proxies = proxies + t.configLock.Unlock() +} + +// Mode return current mode +func (t *Tunnel) Mode() Mode { + return t.mode +} + +// SetMode change the mode of tunnel +func (t *Tunnel) SetMode(mode Mode) { + t.mode = mode } func (t *Tunnel) process() { @@ -88,9 +87,9 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) { var proxy C.Proxy switch t.mode { - case cfg.Direct: + case Direct: proxy = t.proxies["DIRECT"] - case cfg.Global: + case Global: proxy = t.proxies["GLOBAL"] // Rule default: @@ -98,7 +97,7 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) { } remoConn, err := proxy.Generator(metadata) if err != nil { - t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error()) + log.Warnln("Proxy connect error: %s", err.Error()) return } defer remoConn.Close() @@ -121,34 +120,21 @@ func (t *Tunnel) match(metadata *C.Metadata) C.Proxy { if !ok { continue } - t.logCh <- newLog(C.INFO, "%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter()) + log.Infoln("%v match %s using %s", metadata.String(), rule.RuleType().String(), rule.Adapter()) return a } } - t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", metadata.String()) + log.Infoln("%v doesn't match any rule using DIRECT", metadata.String()) return t.proxies["DIRECT"] } -// Run initial task -func (t *Tunnel) Run() { - go t.process() - go t.subscribeLogs() - signal := make(chan struct{}) - go t.configMonitor(signal) - <-signal -} - func newTunnel() *Tunnel { - logCh := make(chan interface{}) return &Tunnel{ queue: channels.NewInfiniteChannel(), proxies: make(map[string]C.Proxy), - observable: observable.NewObservable(logCh), - logCh: logCh, configLock: &sync.RWMutex{}, traffic: C.NewTraffic(time.Second), - mode: cfg.Rule, - logLevel: C.INFO, + mode: Rule, } } @@ -156,6 +142,7 @@ func newTunnel() *Tunnel { func Instance() *Tunnel { once.Do(func() { tunnel = newTunnel() + go tunnel.process() }) return tunnel }