Skip to content

Commit

Permalink
chore: add route.ApplyConfig for CMFA
Browse files Browse the repository at this point in the history
  • Loading branch information
wwqgtxx committed Aug 31, 2024
1 parent f6164ac commit 6306c6b
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 112 deletions.
2 changes: 1 addition & 1 deletion hub/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func ParseWithBytes(buf []byte) (*config.Config, error) {
return config.Parse(buf)
}

// ApplyConfig dispatch configure to all parts
// ApplyConfig dispatch configure to all parts without ExternalController
func ApplyConfig(cfg *config.Config, force bool) {
mux.Lock()
defer mux.Unlock()
Expand Down
46 changes: 31 additions & 15 deletions hub/hub.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package hub

import (
"strings"

"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/log"
Expand Down Expand Up @@ -33,6 +36,33 @@ func WithSecret(secret string) Option {
}
}

// ApplyConfig dispatch configure to all parts include ExternalController
func ApplyConfig(cfg *config.Config) {
applyRoute(cfg)
executor.ApplyConfig(cfg, true)
}

func applyRoute(cfg *config.Config) {
if features.CMFA && strings.HasSuffix(cfg.Controller.ExternalUI, ":0") {
// CMFA have set its default override value to end with ":0" for security.
// so we direct return at here
return
}
if cfg.Controller.ExternalUI != "" {
route.SetUIPath(cfg.Controller.ExternalUI)
}
route.ReCreateServer(&route.Config{
Addr: cfg.Controller.ExternalController,
TLSAddr: cfg.Controller.ExternalControllerTLS,
UnixAddr: cfg.Controller.ExternalControllerUnix,
Secret: cfg.Controller.Secret,
Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey,
DohServer: cfg.Controller.ExternalDohServer,
IsDebug: cfg.General.LogLevel == log.DEBUG,
})
}

// Parse call at the beginning of mihomo
func Parse(options ...Option) error {
cfg, err := executor.Parse()
Expand All @@ -44,20 +74,6 @@ func Parse(options ...Option) error {
option(cfg)
}

if cfg.Controller.ExternalUI != "" {
route.SetUIPath(cfg.Controller.ExternalUI)
}

if cfg.Controller.ExternalController != "" {
go route.Start(cfg.Controller.ExternalController, cfg.Controller.ExternalControllerTLS,
cfg.Controller.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.Controller.ExternalDohServer,
cfg.General.LogLevel == log.DEBUG)
}

if cfg.Controller.ExternalControllerUnix != "" {
go route.StartUnix(cfg.Controller.ExternalControllerUnix, cfg.Controller.ExternalDohServer, cfg.General.LogLevel == log.DEBUG)
}

executor.ApplyConfig(cfg, true)
ApplyConfig(cfg)
return nil
}
226 changes: 132 additions & 94 deletions hub/route/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ import (
)

var (
serverSecret = ""
serverAddr = ""

uiPath = ""

httpServer *http.Server
tlsServer *http.Server
unixServer *http.Server
)

type Traffic struct {
Expand All @@ -46,11 +47,28 @@ type Memory struct {
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
}

type Config struct {
Addr string
TLSAddr string
UnixAddr string
Secret string
Certificate string
PrivateKey string
DohServer string
IsDebug bool
}

func ReCreateServer(cfg *Config) {
go start(cfg)
go startTLS(cfg)
go startUnix(cfg)
}

func SetUIPath(path string) {
uiPath = C.Path.Resolve(path)
}

func router(isDebug bool, withAuth bool, dohServer string) *chi.Mux {
func router(isDebug bool, secret string, dohServer string) *chi.Mux {
r := chi.NewRouter()
corsM := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
Expand All @@ -72,8 +90,8 @@ func router(isDebug bool, withAuth bool, dohServer string) *chi.Mux {
}())
}
r.Group(func(r chi.Router) {
if withAuth {
r.Use(authentication)
if secret != "" {
r.Use(authentication(secret))
}
r.Get("/", hello)
r.Get("/logs", getLogs)
Expand Down Expand Up @@ -111,88 +129,111 @@ func router(isDebug bool, withAuth bool, dohServer string) *chi.Mux {
return r
}

func Start(addr string, tlsAddr string, secret string,
certificate, privateKey string, dohServer string, isDebug bool) {
if serverAddr != "" {
return
func start(cfg *Config) {
// first stop existing server
if httpServer != nil {
_ = httpServer.Close()
httpServer = nil
}

serverAddr = addr
serverSecret = secret

if len(tlsAddr) > 0 {
go func() {
c, err := CN.ParseCert(certificate, privateKey, C.Path)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
}
// handle addr
if len(cfg.Addr) > 0 {
l, err := inbound.Listen("tcp", cfg.Addr)
if err != nil {
log.Errorln("External controller listen error: %s", err)
return
}
log.Infoln("RESTful API listening at: %s", l.Addr().String())

l, err := inbound.Listen("tcp", tlsAddr)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
}
server := &http.Server{
Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer),
}
if err = server.Serve(l); err != nil {
log.Errorln("External controller serve error: %s", err)
}
httpServer = server
}
}

serverAddr = l.Addr().String()
log.Infoln("RESTful API tls listening at: %s", serverAddr)
tlsServe := &http.Server{
Handler: router(isDebug, true, dohServer),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c},
},
}
if err = tlsServe.ServeTLS(l, "", ""); err != nil {
log.Errorln("External controller tls serve error: %s", err)
}
}()
func startTLS(cfg *Config) {
// first stop existing server
if tlsServer != nil {
_ = tlsServer.Close()
tlsServer = nil
}

l, err := inbound.Listen("tcp", addr)
if err != nil {
log.Errorln("External controller listen error: %s", err)
return
// handle tlsAddr
if len(cfg.TLSAddr) > 0 {
c, err := CN.ParseCert(cfg.Certificate, cfg.PrivateKey, C.Path)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
}

l, err := inbound.Listen("tcp", cfg.TLSAddr)
if err != nil {
log.Errorln("External controller tls listen error: %s", err)
return
}

log.Infoln("RESTful API tls listening at: %s", l.Addr().String())
server := &http.Server{
Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c},
},
}
if err = server.ServeTLS(l, "", ""); err != nil {
log.Errorln("External controller tls serve error: %s", err)
}
tlsServer = server
}
serverAddr = l.Addr().String()
log.Infoln("RESTful API listening at: %s", serverAddr)
}

if err = http.Serve(l, router(isDebug, true, dohServer)); err != nil {
log.Errorln("External controller serve error: %s", err)
func startUnix(cfg *Config) {
// first stop existing server
if unixServer != nil {
_ = unixServer.Close()
unixServer = nil
}

}
// handle addr
if len(cfg.UnixAddr) > 0 {
addr := C.Path.Resolve(cfg.UnixAddr)

dir := filepath.Dir(addr)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0o755); err != nil {
log.Errorln("External controller unix listen error: %s", err)
return
}
}

func StartUnix(addr string, dohServer string, isDebug bool) {
addr = C.Path.Resolve(addr)
// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
//
// Note: As mentioned above in the ‘security’ section, when a socket binds a socket to a valid pathname address,
// a socket file is created within the filesystem. On Linux, the application is expected to unlink
// (see the notes section in the man page for AF_UNIX) before any other socket can be bound to the same address.
// The same applies to Windows unix sockets, except that, DeleteFile (or any other file delete API)
// should be used to delete the socket file prior to calling bind with the same path.
_ = syscall.Unlink(addr)

dir := filepath.Dir(addr)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0o755); err != nil {
l, err := inbound.Listen("unix", addr)
if err != nil {
log.Errorln("External controller unix listen error: %s", err)
return
}
}
log.Infoln("RESTful API unix listening at: %s", l.Addr().String())

// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
//
// Note: As mentioned above in the ‘security’ section, when a socket binds a socket to a valid pathname address,
// a socket file is created within the filesystem. On Linux, the application is expected to unlink
// (see the notes section in the man page for AF_UNIX) before any other socket can be bound to the same address.
// The same applies to Windows unix sockets, except that, DeleteFile (or any other file delete API)
// should be used to delete the socket file prior to calling bind with the same path.
_ = syscall.Unlink(addr)

l, err := inbound.Listen("unix", addr)
if err != nil {
log.Errorln("External controller unix listen error: %s", err)
return
server := &http.Server{
Handler: router(cfg.IsDebug, "", cfg.DohServer),
}
if err = server.Serve(l); err != nil {
log.Errorln("External controller unix serve error: %s", err)
}
unixServer = server
}
serverAddr = l.Addr().String()
log.Infoln("RESTful API unix listening at: %s", serverAddr)

if err = http.Serve(l, router(isDebug, false, dohServer)); err != nil {
log.Errorln("External controller unix serve error: %s", err)
}
}

func setPrivateNetworkAccess(next http.Handler) http.Handler {
Expand All @@ -210,38 +251,35 @@ func safeEuqal(a, b string) bool {
return subtle.ConstantTimeCompare(aBuf, bBuf) == 1
}

func authentication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if serverSecret == "" {
next.ServeHTTP(w, r)
return
}
func authentication(secret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// Browser websocket not support custom header
if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" {
token := r.URL.Query().Get("token")
if !safeEuqal(token, secret) {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
return
}

// Browser websocket not support custom header
if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" {
token := r.URL.Query().Get("token")
if !safeEuqal(token, serverSecret) {
header := r.Header.Get("Authorization")
bearer, token, found := strings.Cut(header, " ")

hasInvalidHeader := bearer != "Bearer"
hasInvalidSecret := !found || !safeEuqal(token, secret)
if hasInvalidHeader || hasInvalidSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
return
}

header := r.Header.Get("Authorization")
bearer, token, found := strings.Cut(header, " ")

hasInvalidHeader := bearer != "Bearer"
hasInvalidSecret := !found || !safeEuqal(token, serverSecret)
if hasInvalidHeader || hasInvalidSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
return http.HandlerFunc(fn)
}
return http.HandlerFunc(fn)
}

func hello(w http.ResponseWriter, r *http.Request) {
Expand Down
2 changes: 2 additions & 0 deletions listener/sing_tun/server_android.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build android && !cmfa

package sing_tun

import (
Expand Down
2 changes: 1 addition & 1 deletion listener/sing_tun/server_notandroid.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !android
//go:build !android || cmfa

package sing_tun

Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func main() {
return
case <-hupSign:
if cfg, err := executor.ParseWithPath(C.Path.Config()); err == nil {
executor.ApplyConfig(cfg, true)
hub.ApplyConfig(cfg)
} else {
log.Errorln("Parse config error: %s", err.Error())
}
Expand Down

0 comments on commit 6306c6b

Please sign in to comment.