package handlers import ( "io" "log" "net" "net/http" "net/url" "strings" "alpine-router/mihomo" ) 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" upgradeReq += "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" 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() }() }