♻️ Auth support more format

This commit is contained in:
OpenWaygate committed 2025-05-31 22:59:10 +08:00
1 parent 899619bec9
commit 806985fcb2
7 files changed
+207 -122

No files matched your search

+1 -2
View File
@@ -8,16 +8,15 @@ go_sdk.from_file(go_mod = "//:go.mod")
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
go_deps.config(
go_env = {
"CGO_ENABLED": "0",
"GO111MODULE": "on",
},
)
use_repo(
go_deps,
"com_github_mark3labs_mcp_go",
"com_github_savioxavier_termlink",
"com_github_spf13_cobra",
"com_github_spf13_viper",
+2
View File
@@ -4,6 +4,7 @@ go_library(
name = "cmd",
srcs = [
"auth.go",
"mcp.go",
"root.go",
"version.go",
],
@@ -18,6 +19,7 @@ go_library(
},
deps = [
"//pkg/auth",
"@com_github_mark3labs_mcp_go//server",
"@com_github_savioxavier_termlink//:termlink",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_viper//:viper",
+9 -2
View File
@@ -18,6 +18,7 @@ var authCmd = &cobra.Command{
auth.NewY2BService(
auth.WithCredential(credential),
auth.WithCacheToken(cacheToken),
auth.WithCacheable(true),
)
},
}
@@ -25,6 +26,12 @@ var authCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(authCmd)
authCmd.Flags().StringVarP(&credential, "credential", "c", "client_secret.json", "Path to client secret file")
authCmd.Flags().StringVarP(&cacheToken, "cacheToken", "t", "youtube.token.json", "Path to token cache file")
authCmd.Flags().StringVarP(
&credential, "credential", "c", "client_secret.json",
"Path to client secret file, or base64 encoded string, or json string",
)
authCmd.Flags().StringVarP(
&cacheToken, "cacheToken", "t", "youtube.token.json",
"Path to token cache file, or base64 encoded string, or json string",
)
}
+4 -1
View File
@@ -2,7 +2,10 @@ load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "auth",
srcs = ["auth.go"],
srcs = [
"auth.go",
"service.go",
],
importpath = "github.com/eat-pray-ai/yutu/pkg/auth",
visibility = ["//visibility:public"],
deps = [
+63 -116
View File
@@ -8,7 +8,6 @@ import (
"github.com/eat-pray-ai/yutu/pkg/utils"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
"log"
"net"
@@ -18,89 +17,80 @@ import (
"strings"
)
const (
cacheTokenFile = "youtube.token.json"
)
var (
state = utils.RandomStage()
credential = "client_secret.json"
cacheToken = "youtube.token.json"
errStateMismatch = errors.New("state doesn't match, possible CSRF attack")
errCreateSvc = errors.New("unable to create YouTube service")
errReadPrompt = errors.New("unable to read prompt")
errExchange = errors.New("unable retrieve token from web or prompt")
errStartWeb = errors.New("unable to start web server")
errCacheToken = errors.New("unable to cache token")
errReadSecret = errors.New("unable to read client secret file")
errParseSecret = errors.New("unable to parse client secret to config")
)
errParseToken = errors.New("unable to parse token")
errRefreshToken = errors.New("unable to refresh token, please re-authenticate in cli")
errParseSecret = errors.New("unable to parse client secret")
const missingClientSecretsMessage string = `
Please configure OAuth 2.0
To make this sample run, you need to populate the client_secrets.json file
found at:
%v
with information from the {{ Google Cloud Console }}
{{ https://cloud.google.com/console }}
For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
`
const manualInputMessage = `
After completing the authorization flow, enter the authorization code on command line.
If you end up in an error page after completing the authorization flow, and the url in the address bar is in the form of
'localhost:8216/?state=DONOT-COPY&code=COPY-THIS&scope=DONOT-COPY'
ONLY 'COPY-THIS' part is the code you need to enter on command line.
`
type Option func()
func NewY2BService(opts ...Option) *youtube.Service {
for _, opt := range opts {
opt()
}
ctx := context.Background()
scope := []string{
scope = []string{
youtube.YoutubeScope,
youtube.YoutubeForceSslScope,
youtube.YoutubeChannelMembershipsCreatorScope,
}
client := getClient(ctx, scope...)
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
)
const manualInputHint = `
After completing the authorization flow, enter the authorization code on command line.
If you end up in an error page after completing the authorization flow,
and the url in the address bar is in the form of
'localhost:8216/?state=DONOT-COPY&code=COPY-THIS&scope=DONOT-COPY'
ONLY 'COPY-THIS' part is the code you need to enter on command line.
`
func InitClient(
ctx context.Context, cred, token string, cacheable bool,
) *http.Client {
config := getConfig(cred, scope...)
authedToken := &oauth2.Token{}
err := json.Unmarshal([]byte(token), authedToken)
if err != nil {
log.Fatalln(errors.Join(errCreateSvc, err))
log.Fatalln(errors.Join(errParseToken, err))
}
return service
if !authedToken.Valid() {
tokenSource := config.TokenSource(ctx, authedToken)
authedToken, err = tokenSource.Token()
if err != nil && cacheable {
client, authedToken := newClient(ctx, config)
saveToken(cacheTokenFile, authedToken)
return client
} else if err != nil {
log.Fatalln(errors.Join(errRefreshToken, err))
}
if authedToken != nil && cacheable {
saveToken(cacheTokenFile, authedToken)
}
return config.Client(ctx, authedToken)
}
return config.Client(ctx, authedToken)
}
func getClient(ctx context.Context, scope ...string) *http.Client {
config := getConfig(scope...)
func newClient(
ctx context.Context, config *oauth2.Config,
) (client *http.Client, token *oauth2.Token) {
authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline)
token = getTokenFromWeb(config, authURL)
client = config.Client(ctx, token)
token, err := tokenFromCache(cacheToken)
if err != nil {
return newClient(ctx, config, cacheToken)
} else if !token.Valid() {
tokenSource := config.TokenSource(ctx, token)
token, err = tokenSource.Token()
if token != nil {
saveToken(cacheToken, token)
}
if err != nil {
return newClient(ctx, config, cacheToken)
}
}
return config.Client(ctx, token)
return
}
func getConfig(scope ...string) *oauth2.Config {
cred, err := os.ReadFile(credential)
if err != nil {
fmt.Printf(missingClientSecretsMessage, credential)
log.Fatalln(errors.Join(errReadSecret, err))
}
config, err := google.ConfigFromJSON(cred, scope...)
func getConfig(cred string, scope ...string) *oauth2.Config {
config, err := google.ConfigFromJSON([]byte(cred), scope...)
if err != nil {
log.Fatalln(errors.Join(errParseSecret, err))
}
@@ -108,15 +98,6 @@ func getConfig(scope ...string) *oauth2.Config {
return config
}
func newClient(ctx context.Context, config *oauth2.Config, cacheToken string) *http.Client {
var token *oauth2.Token
authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline)
token = getTokenFromWeb(config, authURL)
saveToken(cacheToken, token)
return config.Client(ctx, token)
}
func startWebServer(redirectURL string) chan string {
u, err := url.Parse(redirectURL)
if err != nil {
@@ -141,9 +122,9 @@ func startWebServer(redirectURL string) chan string {
}
code := r.FormValue("code")
codeCh <- code
listener.Close()
_ = listener.Close()
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(
_, _ = fmt.Fprintf(
w, "Received code: %v\r\nYou can now safely close this window.", code,
)
},
@@ -153,13 +134,12 @@ func startWebServer(redirectURL string) chan string {
return codeCh
}
func getCodeFromPrompt(authURL string) string {
var code string
func getCodeFromPrompt(authURL string) (code string) {
fmt.Printf(
"It seems that your browser is not open. Go to the following "+
"link in your browser:\n%v\n", authURL,
)
fmt.Print(manualInputMessage)
fmt.Print(manualInputHint)
_, err := fmt.Scan(&code)
if err != nil {
log.Fatalln(errors.Join(errReadPrompt, err))
@@ -197,48 +177,15 @@ func getTokenFromWeb(config *oauth2.Config, authURL string) *oauth2.Token {
return token
}
func tokenFromCache(cacheToken string) (*oauth2.Token, error) {
f, err := os.Open(cacheToken)
if err != nil {
return nil, err
}
t := &oauth2.Token{}
err = json.NewDecoder(f).Decode(t)
defer f.Close()
return t, err
}
func saveToken(cacheToken string, token *oauth2.Token) {
f, err := os.OpenFile(cacheToken, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
func saveToken(file string, token *oauth2.Token) {
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalln(errors.Join(errCacheToken, err))
}
defer f.Close()
json.NewEncoder(f).Encode(token)
}
func WithCredential(cred string) Option {
return func() {
if cred != "" {
credential = cred
return
}
if cred, ok := os.LookupEnv("YUTU_CREDENTIAL"); ok {
credential = cred
}
}
}
func WithCacheToken(token string) Option {
return func() {
if token != "" {
cacheToken = token
return
}
if token, ok := os.LookupEnv("YUTU_CACHE_TOKEN"); ok {
cacheToken = token
}
err = json.NewEncoder(f).Encode(token)
if err != nil {
log.Fatalln(errors.Join(errCacheToken, err))
}
}
+122
View File
@@ -0,0 +1,122 @@
package auth
import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/eat-pray-ai/yutu/pkg/utils"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
"log"
"os"
)
var (
cacheable bool
credential string
cacheToken string
errCreateSvc = errors.New("unable to create YouTube service")
errReadToken = errors.New("unable to read token")
errReadSecret = errors.New("unable to read client secret")
)
const missingClientSecretsHint string = `
Please configure OAuth 2.0
To make this sample run, you need to populate the client_secrets.json file
found at:
%v
with information from the {{ Google Cloud Console }}
{{ https://cloud.google.com/console }}
For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
`
type Option func()
func NewY2BService(opts ...Option) *youtube.Service {
for _, opt := range opts {
opt()
}
ctx := context.Background()
client := InitClient(ctx, credential, cacheToken, cacheable)
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
log.Fatalln(errors.Join(errCreateSvc, err))
}
return service
}
func WithCredential(cred string) Option {
return func() {
// cred > YUTU_CREDENTIAL
envCred, ok := os.LookupEnv("YUTU_CREDENTIAL")
if cred == "" && ok {
cred = envCred
} else if cred == "" {
cred = "client_secret.json"
}
// 1. cred is a file path
// 2. cred is a base64 encoded string
// 3. cred is a json string
if _, err := os.Stat(cred); err == nil {
credBytes, err := os.ReadFile(cred)
if err != nil {
fmt.Printf(missingClientSecretsHint, credential)
log.Fatalln(errors.Join(errReadSecret, err))
}
credential = string(credBytes)
return
}
if credB64, err := base64.StdEncoding.DecodeString(cred); err == nil {
credential = string(credB64)
} else if utils.IsJson(cred) {
credential = cred
} else {
fmt.Printf(missingClientSecretsHint, credential)
log.Fatalln(errors.Join(errReadSecret, err))
}
}
}
func WithCacheToken(token string) Option {
return func() {
// token > YUTU_CACHE_TOKEN
envToken, ok := os.LookupEnv("YUTU_CACHE_TOKEN")
if token == "" && ok {
token = envToken
} else if token == "" {
token = "youtube.token.json"
}
// 1. token is a file path
// 2. token is a base64 encoded string
// 3. token is a json string
if _, err := os.Stat(token); err == nil {
tokenBytes, err := os.ReadFile(token)
if err != nil {
log.Fatalln(errors.Join(errReadToken, err))
}
cacheToken = string(tokenBytes)
return
}
if tokenB64, err := base64.StdEncoding.DecodeString(token); err == nil {
cacheToken = string(tokenB64)
} else if utils.IsJson(token) {
cacheToken = token
} else {
log.Fatalln(errors.Join(errReadToken, err))
}
}
}
func WithCacheable(c bool) Option {
return func() {
cacheable = c
}
}
+6 -1
View File
@@ -40,7 +40,7 @@ func OpenURL(url string) error {
func RandomStage() string {
b := make([]byte, 128)
rand.Read(b)
_, _ = rand.Read(b)
state := base64.URLEncoding.EncodeToString(b)
return state
}
@@ -50,3 +50,8 @@ func GetFileName(file string) string {
fileName := base[:len(base)-len(filepath.Ext(base))]
return fileName
}
func IsJson(s string) bool {
var js json.RawMessage
return json.Unmarshal([]byte(s), &js) == nil
}