Files
2026-06-15 06:16:16 +08:00

207 lines
5.9 KiB
Go

package controller
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const hhhlMisskeyHost = "https://dc.hhhl.cc"
// HHHLAuthorize adapts Misskey MiAuth to the OAuth authorization endpoint shape.
func HHHLAuthorize(c *gin.Context) {
redirectURI := strings.TrimSpace(c.Query("redirect_uri"))
if redirectURI == "" {
c.String(http.StatusBadRequest, "missing redirect_uri")
return
}
state := c.Query("state")
sessionID := uuid.NewString()
callbackURL := fmt.Sprintf(
"%s/api/hhhl/callback?r=%s&s=%s&sid=%s",
strings.TrimRight(system_setting.ServerAddress, "/"),
url.QueryEscape(redirectURI),
url.QueryEscape(state),
url.QueryEscape(sessionID),
)
miAuthURL := fmt.Sprintf(
"%s/miauth/%s?name=NewAPI%%E7%%99%%BB%%E5%%BD%%95&callback=%s&permission=read:account",
hhhlMisskeyHost,
url.PathEscape(sessionID),
url.QueryEscape(callbackURL),
)
c.Redirect(http.StatusFound, miAuthURL)
}
// HHHLCallback returns the MiAuth session id as an OAuth authorization code.
// Wrap in pkce.{base64json} format so the generic OAuth provider forwards it correctly.
func HHHLCallback(c *gin.Context) {
redirectURI := strings.TrimSpace(c.Query("r"))
sessionID := strings.TrimSpace(c.Query("sid"))
if redirectURI == "" || sessionID == "" {
c.String(http.StatusBadRequest, "invalid callback")
return
}
targetURL, err := url.Parse(redirectURI)
if err != nil || targetURL.Scheme == "" || targetURL.Host == "" {
c.String(http.StatusBadRequest, "invalid redirect_uri")
return
}
codePayload, _ := common.Marshal(map[string]string{"token": sessionID})
code := "pkce." + base64.RawURLEncoding.EncodeToString(codePayload)
query := targetURL.Query()
query.Set("code", code)
query.Set("state", c.Query("s"))
targetURL.RawQuery = query.Encode()
c.Redirect(http.StatusFound, targetURL.String())
}
// HHHLToken exchanges a MiAuth session id for a Misskey access token.
func HHHLToken(c *gin.Context) {
code := strings.TrimSpace(c.Query("code"))
if code == "" {
if err := c.Request.ParseForm(); err == nil {
code = strings.TrimSpace(c.Request.Form.Get("code"))
}
}
if code == "" {
var payload struct {
Code string `json:"code"`
}
if err := c.ShouldBindJSON(&payload); err == nil {
code = strings.TrimSpace(payload.Code)
}
}
if code == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "Missing code"})
return
}
sessionID := code
if strings.HasPrefix(code, "pkce.") {
decoded, err := base64.RawURLEncoding.DecodeString(code[5:])
if err == nil {
var pkceData struct {
Token string `json:"token"`
}
if jsonErr := common.Unmarshal(decoded, &pkceData); jsonErr == nil && pkceData.Token != "" {
sessionID = pkceData.Token
}
}
}
body, err := common.Marshal(gin.H{})
if err != nil {
common.ApiError(c, err)
return
}
req, err := http.NewRequestWithContext(
c.Request.Context(),
http.MethodPost,
fmt.Sprintf("%s/api/miauth/%s/check", hhhlMisskeyHost, url.PathEscape(sessionID)),
bytes.NewReader(body),
)
if err != nil {
common.ApiError(c, err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
client := http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "MiAuth check request failed"})
return
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
common.ApiError(c, err)
return
}
var tokenData struct {
OK bool `json:"ok"`
Token string `json:"token"`
}
if err := common.Unmarshal(respBody, &tokenData); err != nil || !tokenData.OK || tokenData.Token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "Failed to validate MiAuth session"})
return
}
c.JSON(http.StatusOK, gin.H{"access_token": tokenData.Token, "token_type": "Bearer"})
}
// HHHLUserInfo adapts Misskey /api/i to an OIDC-like userinfo response.
func HHHLUserInfo(c *gin.Context) {
token := strings.TrimSpace(c.Query("access_token"))
if token == "" {
token = strings.TrimSpace(strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer "))
}
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_request", "error_description": "Missing token"})
return
}
body, err := common.Marshal(gin.H{"i": token})
if err != nil {
common.ApiError(c, err)
return
}
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, hhhlMisskeyHost+"/api/i", bytes.NewReader(body))
if err != nil {
common.ApiError(c, err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
client := http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token", "error_description": "Failed to fetch user info"})
return
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
common.ApiError(c, err)
return
}
var userData struct {
Id string `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
}
if err := common.Unmarshal(respBody, &userData); err != nil || userData.Id == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
return
}
if userData.Name == "" {
userData.Name = userData.Username
}
c.JSON(http.StatusOK, gin.H{
"sub": userData.Id,
"preferred_username": userData.Username,
"name": userData.Name,
})
}