2026-04-13 18:56:13 +03:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-04-15 11:38:26 +03:00
|
|
|
"nano-router/mihomo"
|
2026-04-13 18:56:13 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func getMihomoAPIBase() string {
|
|
|
|
|
cfg, err := mihomo.LoadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "http://127.0.0.1:9090"
|
|
|
|
|
}
|
|
|
|
|
ec, _ := cfg["external-controller"].(string)
|
|
|
|
|
if ec == "" {
|
|
|
|
|
ec = "0.0.0.0:9090"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(ec, "0.0.0.0:") || strings.HasPrefix(ec, ":") {
|
|
|
|
|
ec = "127.0.0.1" + strings.TrimPrefix(ec, "0.0.0.0")
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(ec, "http") {
|
|
|
|
|
ec = "http://" + ec
|
|
|
|
|
}
|
|
|
|
|
return ec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getMihomoSecret() string {
|
|
|
|
|
cfg, err := mihomo.LoadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
s, _ := cfg["secret"].(string)
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getMihomoHostPort() (string, string) {
|
|
|
|
|
u, err := url.Parse(getMihomoAPIBase())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "127.0.0.1", "9090"
|
|
|
|
|
}
|
|
|
|
|
host := u.Hostname()
|
|
|
|
|
if host == "0.0.0.0" || host == "" {
|
|
|
|
|
host = "127.0.0.1"
|
|
|
|
|
}
|
|
|
|
|
port := u.Port()
|
|
|
|
|
if port == "" {
|
|
|
|
|
if u.Scheme == "https" {
|
|
|
|
|
port = "443"
|
|
|
|
|
} else {
|
|
|
|
|
port = "80"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return host, port
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func HandleMihomoAPIProxy(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
base := getMihomoAPIBase()
|
|
|
|
|
secret := getMihomoSecret()
|
|
|
|
|
|
|
|
|
|
suffix := strings.TrimPrefix(r.URL.Path, "/api/mihomo/api/")
|
|
|
|
|
target, err := url.Parse(base + "/" + suffix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "parse mihomo url: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q := r.URL.Query()
|
|
|
|
|
if r.Method == http.MethodGet {
|
|
|
|
|
q.Set("nonce", "1")
|
|
|
|
|
}
|
|
|
|
|
target.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
proxyReq, err := http.NewRequest(r.Method, target.String(), r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "create proxy request: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for k, vv := range r.Header {
|
|
|
|
|
if strings.EqualFold(k, "Authorization") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, v := range vv {
|
|
|
|
|
proxyReq.Header.Add(k, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if secret != "" {
|
|
|
|
|
proxyReq.Header.Set("Authorization", "Bearer "+secret)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
|
resp, err := client.Do(proxyReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusBadGateway, "mihomo api unreachable: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
for k, vv := range resp.Header {
|
|
|
|
|
if strings.EqualFold(k, "Content-Length") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, v := range vv {
|
|
|
|
|
w.Header().Add(k, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(resp.StatusCode)
|
|
|
|
|
_, err = io.Copy(w, resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("proxy copy error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func HandleMihomoWSProxy(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
secret := getMihomoSecret()
|
|
|
|
|
host, port := getMihomoHostPort()
|
|
|
|
|
|
|
|
|
|
suffix := strings.TrimPrefix(r.URL.Path, "/api/mihomo/ws/")
|
|
|
|
|
path := "/" + suffix
|
|
|
|
|
if r.URL.RawQuery != "" {
|
|
|
|
|
path += "?" + r.URL.RawQuery
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hj, ok := w.(http.Hijacker)
|
|
|
|
|
if !ok {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "websocket hijack not supported")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clientConn, _, err := hj.Hijack()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "hijack failed: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remoteConn, err := net.Dial("tcp", net.JoinHostPort(host, port))
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusBadGateway, "mihomo ws connect failed: "+err.Error())
|
|
|
|
|
clientConn.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
upgradeReq := "GET " + path + " HTTP/1.1\r\n"
|
|
|
|
|
upgradeReq += "Host: " + host + ":" + port + "\r\n"
|
|
|
|
|
upgradeReq += "Upgrade: websocket\r\n"
|
|
|
|
|
upgradeReq += "Connection: Upgrade\r\n"
|
2026-04-15 11:38:26 +03:00
|
|
|
upgradeReq += "Sec-WebSocket-Key: " + r.Header.Get("Sec-Websocket-Key") + "\r\n"
|
2026-04-13 18:56:13 +03:00
|
|
|
upgradeReq += "Sec-WebSocket-Version: 13\r\n"
|
|
|
|
|
if secret != "" {
|
|
|
|
|
upgradeReq += "Authorization: Bearer " + secret + "\r\n"
|
|
|
|
|
}
|
|
|
|
|
for k, vv := range r.Header {
|
|
|
|
|
if strings.EqualFold(k, "Upgrade") || strings.EqualFold(k, "Connection") ||
|
|
|
|
|
strings.EqualFold(k, "Sec-Websocket-Key") || strings.EqualFold(k, "Sec-Websocket-Version") ||
|
|
|
|
|
strings.EqualFold(k, "Sec-Websocket-Extensions") || strings.EqualFold(k, "Sec-Websocket-Protocol") ||
|
|
|
|
|
strings.EqualFold(k, "Authorization") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, v := range vv {
|
|
|
|
|
upgradeReq += k + ": " + v + "\r\n"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k, v := range r.Header {
|
|
|
|
|
if strings.EqualFold(k, "Sec-WebSocket-Protocol") {
|
|
|
|
|
upgradeReq += "Sec-WebSocket-Protocol: " + strings.Join(v, ", ") + "\r\n"
|
|
|
|
|
}
|
|
|
|
|
if strings.EqualFold(k, "Sec-WebSocket-Extensions") {
|
|
|
|
|
upgradeReq += "Sec-WebSocket-Extensions: " + strings.Join(v, ", ") + "\r\n"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
upgradeReq += "\r\n"
|
|
|
|
|
|
|
|
|
|
_, err = remoteConn.Write([]byte(upgradeReq))
|
|
|
|
|
if err != nil {
|
|
|
|
|
clientConn.Close()
|
|
|
|
|
remoteConn.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf := make([]byte, 4096)
|
|
|
|
|
n, err := remoteConn.Read(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
clientConn.Close()
|
|
|
|
|
remoteConn.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
respStr := string(buf[:n])
|
|
|
|
|
if !strings.Contains(respStr, "101") {
|
|
|
|
|
clientConn.Write(buf[:n])
|
|
|
|
|
clientConn.Close()
|
|
|
|
|
remoteConn.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headerEnd := strings.Index(respStr, "\r\n\r\n")
|
|
|
|
|
if headerEnd >= 0 {
|
|
|
|
|
clientConn.Write(buf[:n])
|
|
|
|
|
} else {
|
|
|
|
|
clientConn.Write(buf[:n])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
io.Copy(remoteConn, clientConn)
|
|
|
|
|
remoteConn.Close()
|
|
|
|
|
}()
|
|
|
|
|
go func() {
|
|
|
|
|
io.Copy(clientConn, remoteConn)
|
|
|
|
|
clientConn.Close()
|
|
|
|
|
}()
|
|
|
|
|
}
|