207 lines
5.9 KiB
Go
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,
|
|
})
|
|
}
|