♻️ Auth support more format
This commit is contained in:
1 parent
899619bec9
commit
806985fcb2
7 files changed
+207
-122
No files matched your search
+1
-2
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user