package api import ( "bytes" "encoding/json" "fmt" "io" "os" "strings" "time" http "github.com/bogdanfinn/fhttp" tls_client "github.com/bogdanfinn/tls-client" "github.com/bogdanfinn/tls-client/profiles" "github.com/gin-gonic/gin" "github.com/xqdoo00o/OpenAIAuth/auth" "github.com/xqdoo00o/funcaptcha" "github.com/linweiyuan/go-logger/logger" ) const ( ChatGPTApiPrefix = "/chatgpt" ImitateApiPrefix = "/imitate/v1" ChatGPTApiUrlPrefix = "https://chat.openai.com" PlatformApiPrefix = "/platform" PlatformApiUrlPrefix = "https://api.openai.com" defaultErrorMessageKey = "errorMessage" AuthorizationHeader = "Authorization" XAuthorizationHeader = "X-Authorization" ContentType = "application/x-www-form-urlencoded" UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" Auth0Url = "https://auth0.openai.com" LoginUsernameUrl = Auth0Url + "/u/login/identifier?state=" LoginPasswordUrl = Auth0Url + "/u/login/password?state=" ParseUserInfoErrorMessage = "failed to parse user login info" GetAuthorizedUrlErrorMessage = "failed to get authorized url" EmailInvalidErrorMessage = "email is not valid" EmailOrPasswordInvalidErrorMessage = "email or password is not correct" GetAccessTokenErrorMessage = "failed to get access token" defaultTimeoutSeconds = 600 // 10 minutes EmailKey = "email" AccountDeactivatedErrorMessage = "account %s is deactivated" ReadyHint = "service go-chatgpt-api is ready" refreshPuidErrorMessage = "failed to refresh PUID" ) var ( Client tls_client.HttpClient ArkoseClient tls_client.HttpClient PUID string ProxyUrl string ) type LoginInfo struct { Username string `json:"username"` Password string `json:"password"` } type AuthLogin interface { GetAuthorizedUrl(csrfToken string) (string, int, error) GetState(authorizedUrl string) (string, int, error) CheckUsername(state string, username string) (int, error) CheckPassword(state string, username string, password string) (string, int, error) GetAccessToken(code string) (string, int, error) GetAccessTokenFromHeader(c *gin.Context) (string, int, error) } func init() { Client, _ = tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{ tls_client.WithCookieJar(tls_client.NewCookieJar()), tls_client.WithTimeoutSeconds(defaultTimeoutSeconds), tls_client.WithClientProfile(profiles.Okhttp4Android13), }...) ArkoseClient = getHttpClient() setupPUID() } func NewHttpClient() tls_client.HttpClient { client := getHttpClient() ProxyUrl = os.Getenv("PROXY") if ProxyUrl != "" { client.SetProxy(ProxyUrl) } return client } func getHttpClient() tls_client.HttpClient { client, _ := tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{ tls_client.WithCookieJar(tls_client.NewCookieJar()), tls_client.WithClientProfile(profiles.Okhttp4Android13), }...) return client } func Proxy(c *gin.Context) { url := c.Request.URL.Path if strings.Contains(url, ChatGPTApiPrefix) { url = strings.ReplaceAll(url, ChatGPTApiPrefix, ChatGPTApiUrlPrefix) } else if strings.Contains(url, ImitateApiPrefix) { url = strings.ReplaceAll(url, ImitateApiPrefix, ChatGPTApiUrlPrefix+"/backend-api") } else { url = strings.ReplaceAll(url, PlatformApiPrefix, PlatformApiUrlPrefix) } method := c.Request.Method queryParams := c.Request.URL.Query().Encode() if queryParams != "" { url += "?" + queryParams } // if not set, will return 404 c.Status(http.StatusOK) var req *http.Request if method == http.MethodGet { req, _ = http.NewRequest(http.MethodGet, url, nil) } else { body, _ := io.ReadAll(c.Request.Body) req, _ = http.NewRequest(method, url, bytes.NewReader(body)) } req.Header.Set("User-Agent", UserAgent) req.Header.Set(AuthorizationHeader, GetAccessToken(c)) resp, err := Client.Do(req) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, ReturnMessage(err.Error())) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { if resp.StatusCode == http.StatusUnauthorized { logger.Error(fmt.Sprintf(AccountDeactivatedErrorMessage, c.GetString(EmailKey))) } responseMap := make(map[string]interface{}) json.NewDecoder(resp.Body).Decode(&responseMap) c.AbortWithStatusJSON(resp.StatusCode, responseMap) return } io.Copy(c.Writer, resp.Body) } func ReturnMessage(msg string) gin.H { logger.Warn(msg) return gin.H{ defaultErrorMessageKey: msg, } } func GetAccessToken(c *gin.Context) string { accessToken := c.GetString(AuthorizationHeader) if !strings.HasPrefix(accessToken, "Bearer") { return "Bearer " + accessToken } return accessToken } func GetArkoseToken() (string, error) { return funcaptcha.GetOpenAIToken(PUID, ProxyUrl) } func setupPUID() { username := os.Getenv("OPENAI_EMAIL") password := os.Getenv("OPENAI_PASSWORD") if username != "" && password != "" { go func() { for { authenticator := auth.NewAuthenticator(username, password, ProxyUrl) if err := authenticator.Begin(); err != nil { logger.Warn(fmt.Sprintf("%s: %s", refreshPuidErrorMessage, err.Details)) return } accessToken := authenticator.GetAccessToken() if accessToken == "" { logger.Error(refreshPuidErrorMessage) return } puid, err := authenticator.GetPUID() if err != nil { logger.Error(refreshPuidErrorMessage) return } PUID = puid time.Sleep(time.Hour * 24 * 7) } }() } }