Files
alpine-router/handlers/mihomo_proxy.go

217 lines
4.8 KiB
Go
Raw Normal View History

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()
}()
}