// Package oauthutil provides OAuth utilities.
package oauthutil

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config"
	"github.com/rclone/rclone/fs/config/configmap"
	"github.com/rclone/rclone/fs/fserrors"
	"github.com/rclone/rclone/fs/fshttp"
	"github.com/rclone/rclone/fs/rc"
	"github.com/rclone/rclone/lib/random"
	"github.com/skratchdot/open-golang/open"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

var (
	// templateString is the template used in the authorization webserver
	templateString string

	// oauthCancelFn stores the cancel function for the currently active OAuth flow
	oauthCancelFn context.CancelFunc
	// oauthCancelMu protects oauthCancelFn and oauthURL
	oauthCancelMu sync.Mutex
	// oauthURL stores the URL for the currently active OAuth flow
	oauthURL string
)

const (
	// TitleBarRedirectURL is the OAuth2 redirect URL to use when the authorization
	// code should be returned in the title bar of the browser, with the page text
	// prompting the user to copy the code and paste it in the application.
	TitleBarRedirectURL = "urn:ietf:wg:oauth:2.0:oob"

	// bindPort is the port that we bind the local webserver to
	bindPort = "53682"

	// bindAddress is binding for local webserver when active
	bindAddress = "127.0.0.1:" + bindPort

	// RedirectURL is redirect to local webserver when active
	RedirectURL = "http://" + bindAddress + "/"

	// RedirectPublicURL is redirect to local webserver when active with public name
	RedirectPublicURL = "http://localhost.rclone.org:" + bindPort + "/"

	// RedirectLocalhostURL is redirect to local webserver when active with localhost
	RedirectLocalhostURL = "http://localhost:" + bindPort + "/"

	// RedirectPublicSecureURL is a public https URL which
	// redirects to the local webserver
	RedirectPublicSecureURL = "https://oauth.rclone.org/"

	// DefaultAuthResponseTemplate is the default template used in the authorization webserver
	DefaultAuthResponseTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ if .OK }}Success!{{ else }}Failure!{{ end }}</title>
</head>
<body>
<h1>{{ if .OK }}Success!{{ else }}Failure!{{ end }}</h1>
<hr>
<pre style="width: 750px; white-space: pre-wrap;">
{{ if eq .OK false }}
Error: {{ .Name }}<br>
{{ if .Description }}Description: {{ .Description }}<br>{{ end }}
{{ if .Code }}Code: {{ .Code }}<br>{{ end }}
{{ if .HelpURL }}Look here for help: <a href="{{ .HelpURL }}">{{ .HelpURL }}</a><br>{{ end }}
{{ else }}
All done. Please go back to rclone.
{{ end }}
</pre>
</body>
</html>
`
)

// OpenURL is used when rclone wants to open a browser window
// for user authentication. It defaults to something which
// should work for most uses, but may be overridden.
var OpenURL = open.Start

// Config - structure that we will use to store the OAuth configuration
// settings. This is based on the union of the configuration structures for the two
// OAuth modules that we are using (oauth2 and oauth2.clientcrentials), along with a
// flag indicating if we are going to use the client credential flow
type Config struct {
	ClientID             string
	ClientSecret         string
	TokenURL             string
	AuthURL              string
	Scopes               []string
	EndpointParams       url.Values
	RedirectURL          string
	ClientCredentialFlow bool
	AuthStyle            oauth2.AuthStyle
}

// MakeOauth2Config makes an oauth2.Config from our config
func (conf *Config) MakeOauth2Config() *oauth2.Config {
	return &oauth2.Config{
		ClientID:     conf.ClientID,
		ClientSecret: conf.ClientSecret,
		RedirectURL:  conf.RedirectURL,
		Scopes:       conf.Scopes,
		Endpoint: oauth2.Endpoint{
			AuthURL:   conf.AuthURL,
			TokenURL:  conf.TokenURL,
			AuthStyle: conf.AuthStyle,
		},
	}
}

// MakeClientCredentialsConfig makes a clientcredentials.Config from our config
func (conf *Config) MakeClientCredentialsConfig() *clientcredentials.Config {
	return &clientcredentials.Config{
		ClientID:     conf.ClientID,
		ClientSecret: conf.ClientSecret,
		Scopes:       conf.Scopes,
		TokenURL:     conf.TokenURL,
		AuthStyle:    conf.AuthStyle,
		// EndpointParams url.Values
	}
}

// SharedOptions are shared between backends the utilize an OAuth flow
var SharedOptions = []fs.Option{{
	Name:      config.ConfigClientID,
	Help:      "OAuth Client Id.\n\nLeave blank normally.",
	Sensitive: true,
}, {
	Name:      config.ConfigClientSecret,
	Help:      "OAuth Client Secret.\n\nLeave blank normally.",
	Sensitive: true,
}, {
	Name:      config.ConfigToken,
	Help:      "OAuth Access Token as a JSON blob.",
	Advanced:  true,
	Sensitive: true,
}, {
	Name:     config.ConfigAuthURL,
	Help:     "Auth server URL.\n\nLeave blank to use the provider defaults.",
	Advanced: true,
}, {
	Name:     config.ConfigTokenURL,
	Help:     "Token server url.\n\nLeave blank to use the provider defaults.",
	Advanced: true,
}, {
	Name:     config.ConfigClientCredentials,
	Default:  false,
	Help:     "Use client credentials OAuth flow.\n\nThis will use the OAUTH2 client Credentials Flow as described in RFC 6749.\n\nNote that this option is NOT supported by all backends.",
	Advanced: true,
}}

// oldToken contains an end-user's tokens.
// This is the data you must store to persist authentication.
//
// From the original code.google.com/p/goauth2/oauth package - used
// for backwards compatibility in the rclone config file
type oldToken struct {
	AccessToken  string
	RefreshToken string
	Expiry       time.Time
}

// GetToken returns the token saved in the config file under
// section name.
func GetToken(name string, m configmap.Mapper) (*oauth2.Token, error) {
	tokenString, ok := m.Get(config.ConfigToken)
	if !ok || tokenString == "" {
		return nil, fmt.Errorf("empty token found - please run \"rclone config reconnect %s:\"", name)
	}
	token := new(oauth2.Token)
	err := json.Unmarshal([]byte(tokenString), token)
	if err != nil {
		return nil, err
	}
	// if has data then return it
	if token.AccessToken != "" {
		return token, nil
	}
	// otherwise try parsing as oldToken
	oldtoken := new(oldToken)
	err = json.Unmarshal([]byte(tokenString), oldtoken)
	if err != nil {
		return nil, err
	}
	// Fill in result into new token
	token.AccessToken = oldtoken.AccessToken
	token.RefreshToken = oldtoken.RefreshToken
	token.Expiry = oldtoken.Expiry
	// Save new format in config file
	err = PutToken(name, m, token, false)
	if err != nil {
		return nil, err
	}
	return token, nil
}

// PutToken stores the token in the config file
//
// This saves the config file if it changes
func PutToken(name string, m configmap.Mapper, token *oauth2.Token, newSection bool) error {
	tokenBytes, err := json.Marshal(token)
	if err != nil {
		return err
	}
	tokenString := string(tokenBytes)
	old, ok := m.Get(config.ConfigToken)
	if !ok || tokenString != old {
		m.Set(config.ConfigToken, tokenString)
		fs.Debugf(name, "Saved new token in config file")
	}
	return nil
}

// TokenSource stores updated tokens in the config file
type TokenSource struct {
	mu          sync.Mutex
	name        string
	m           configmap.Mapper
	tokenSource oauth2.TokenSource
	token       *oauth2.Token
	config      *Config
	ctx         context.Context
	expiryTimer *time.Timer // signals whenever the token expires
}

// If token has expired then first try re-reading it (and its refresh token)
// from the config file in case a concurrently running rclone has updated them
// already.
// Returns whether either of the two tokens has been reread.
func (ts *TokenSource) reReadToken() (changed bool) {
	tokenString, found := ts.m.Get(config.ConfigToken)
	if !found || tokenString == "" {
		fs.Debugf(ts.name, "Failed to read token out of config file")
		return false
	}
	newToken := new(oauth2.Token)
	err := json.Unmarshal([]byte(tokenString), newToken)
	if err != nil {
		fs.Debugf(ts.name, "Failed to parse token out of config file: %v", err)
		return false
	}

	if newToken.Valid() {
		fs.Debugf(ts.name, "Loaded fresh token from config file")
		changed = true
	}
	if newToken.RefreshToken != "" && newToken.RefreshToken != ts.token.RefreshToken {
		fs.Debugf(ts.name, "Loaded new refresh token from config file")
		changed = true
	}

	if changed {
		ts.token = newToken
		ts.tokenSource = nil // invalidate since we changed the token
	} else {
		fs.Debugf(ts.name, "No updated token found in the config file")
	}
	return changed
}

type retrieveErrResponse struct {
	Error string `json:"error"`
}

// If err is nil or an error other than fatal OAuth errors, returns err itself.
// Otherwise returns a more user-friendly error.
func maybeWrapOAuthError(err error, remoteName string) (newErr error) {
	newErr = err
	if rErr, ok := err.(*oauth2.RetrieveError); ok {
		if rErr.Response.StatusCode == 400 || rErr.Response.StatusCode == 401 {
			fs.Debugf(remoteName, "got fatal oauth error: %v", rErr)
			var resp retrieveErrResponse
			if err = json.Unmarshal(rErr.Body, &resp); err != nil {
				newErr = fmt.Errorf("(can't decode error info) - try refreshing token with \"rclone config reconnect %s:\"", remoteName)
				return
			}
			var suggestion string
			switch resp.Error {
			case "invalid_client", "unauthorized_client", "unsupported_grant_type", "invalid_scope":
				suggestion = "if you're using your own client id/secret, make sure they're properly set up following the docs"
			case "invalid_grant":
				fallthrough
			default:
				suggestion = fmt.Sprintf("maybe token expired? - try refreshing with \"rclone config reconnect %s:\"", remoteName)
			}
			newErr = fmt.Errorf("%s: %s", resp.Error, suggestion)
		}
	}
	return
}

// Token returns a token or an error.
// Token must be safe for concurrent use by multiple goroutines.
// The returned Token must not be modified.
//
// This saves the token in the config file if it has changed
func (ts *TokenSource) Token() (*oauth2.Token, error) {
	ts.mu.Lock()
	defer ts.mu.Unlock()
	var (
		token   *oauth2.Token
		err     error
		changed = false
	)
	const maxTries = 5

	// If we have a cached valid token, use that
	if ts.token.Valid() {
		return ts.token, nil
	}

	fs.Debug(ts.name, "Token expired")

	// Try getting the token a few times
	for i := 1; i <= maxTries; i++ {
		// Try reading the token from the config file in case it has
		// been updated by a concurrent rclone process
		if !ts.token.Valid() {
			if ts.reReadToken() {
				changed = true
			} else if !ts.config.ClientCredentialFlow && ts.token.RefreshToken == "" {
				return nil, fserrors.FatalError(
					fmt.Errorf("token expired and there's no refresh token - manually refresh with \"rclone config reconnect %s:\"", ts.name),
				)
			}
		}

		// Make a new token source if required
		if ts.tokenSource == nil {
			if ts.config.ClientCredentialFlow {
				ts.tokenSource = ts.config.MakeClientCredentialsConfig().TokenSource(ts.ctx)
			} else {
				ts.tokenSource = ts.config.MakeOauth2Config().TokenSource(ts.ctx, ts.token)
			}
		}

		token, err = ts.tokenSource.Token()
		if err == nil {
			fs.Debug(ts.name, "Token refresh successful")
			break
		}
		if newErr := maybeWrapOAuthError(err, ts.name); newErr != err {
			err = newErr // Fatal OAuth error
			break
		}
		fs.Debugf(ts.name, "Token refresh failed try %d/%d: %v", i, maxTries, err)
		time.Sleep(1 * time.Second)
	}
	if err != nil {
		return nil, fmt.Errorf("couldn't fetch token: %w", err)
	}
	changed = changed || ts.token == nil || token.AccessToken != ts.token.AccessToken || token.RefreshToken != ts.token.RefreshToken || token.Expiry != ts.token.Expiry
	ts.token = token
	if changed {
		// Bump on the expiry timer if it is set
		if ts.expiryTimer != nil {
			ts.expiryTimer.Reset(ts.timeToExpiry())
		}
		err = PutToken(ts.name, ts.m, token, false)
		if err != nil {
			return nil, fmt.Errorf("couldn't store token: %w", err)
		}
	}
	return token, nil
}

// Invalidate invalidates the token
func (ts *TokenSource) Invalidate() {
	ts.mu.Lock()
	ts.token.AccessToken = ""
	ts.mu.Unlock()
}

// Expire marks the token as expired
//
// This also marks the token in the config file as expired, if it is the same one
func (ts *TokenSource) Expire() error {
	ts.mu.Lock()
	defer ts.mu.Unlock()
	ts.token.Expiry = time.Now().Add(time.Hour * (-1)) // expire token
	t, err := GetToken(ts.name, ts.m)
	if err != nil {
		return err
	}
	if t.AccessToken == ts.token.AccessToken {
		err = PutToken(ts.name, ts.m, ts.token, false)
	}
	return err
}

// timeToExpiry returns how long until the token expires
//
// Call with the lock held
func (ts *TokenSource) timeToExpiry() time.Duration {
	t := ts.token
	if t == nil {
		return 0
	}
	if t.Expiry.IsZero() {
		return 3e9 * time.Second // ~95 years
	}
	return time.Until(t.Expiry)
}

// OnExpiry returns a channel which has the time written to it when
// the token expires.  Note that there is only one channel so if
// attaching multiple go routines it will only signal to one of them.
func (ts *TokenSource) OnExpiry() <-chan time.Time {
	ts.mu.Lock()
	defer ts.mu.Unlock()
	if ts.expiryTimer == nil {
		ts.expiryTimer = time.NewTimer(ts.timeToExpiry())
	}
	return ts.expiryTimer.C
}

// Check interface satisfied
var _ oauth2.TokenSource = (*TokenSource)(nil)

// Context returns a context with our HTTP Client baked in for oauth2
func Context(ctx context.Context, client *http.Client) context.Context {
	return context.WithValue(ctx, oauth2.HTTPClient, client)
}

// OverrideCredentials sets the ClientID and ClientSecret from the
// config file if they are not blank.
// If any value is overridden, true is returned.
// the origConfig is copied
func OverrideCredentials(name string, m configmap.Mapper, origConfig *Config) (newConfig *Config, changed bool) {
	newConfig = new(Config)
	*newConfig = *origConfig
	changed = false
	ClientID, ok := m.Get(config.ConfigClientID)
	if ok && ClientID != "" {
		newConfig.ClientID = ClientID
		// Clear out any existing client secret since the ID changed.
		// (otherwise it's impossible for a config to clear the secret)
		newConfig.ClientSecret = ""
		changed = true
	}
	ClientSecret, ok := m.Get(config.ConfigClientSecret)
	if ok && ClientSecret != "" {
		newConfig.ClientSecret = ClientSecret
		changed = true
	}
	AuthURL, ok := m.Get(config.ConfigAuthURL)
	if ok && AuthURL != "" {
		newConfig.AuthURL = AuthURL
		changed = true
	}
	TokenURL, ok := m.Get(config.ConfigTokenURL)
	if ok && TokenURL != "" {
		newConfig.TokenURL = TokenURL
		changed = true
	}
	ClientCredentialStr, ok := m.Get(config.ConfigClientCredentials)
	if ok && ClientCredentialStr != "" {
		ClientCredential, err := strconv.ParseBool(ClientCredentialStr)
		if err != nil {
			fs.Errorf(nil, "Invalid setting for %q: %v", config.ConfigClientCredentials, err)
		} else {
			newConfig.ClientCredentialFlow = ClientCredential
		}
		changed = true
	}
	return newConfig, changed
}

// SharedClientIDWarning warns the user that this remote is relying on
// rclone's built-in shared OAuth client_id.
//
// It only warns when the user has not configured their own client_id
// (config.ConfigClientID is blank in the config), so it must only be
// called from the code path that actually uses the shared client_id -
// not for service account, environment or anonymous auth which don't.
//
// name is the remote's name, service is the human readable name of the
// service (eg "Google Drive") and helpURL points at the docs describing
// how to make your own client_id.
func SharedClientIDWarning(name, service, helpURL string, m configmap.Mapper) {
	if clientID, _ := m.Get(config.ConfigClientID); clientID != "" {
		return
	}
	fs.Logf(name, "This remote uses rclone's shared %s client_id, which is being retired and will stop working during 2026. Create your own client_id to avoid interruption: %s", service, helpURL)
}

// SharedClientIDConfigConfirm returns a config wizard Yes/No step
// warning that this remote would use rclone's shared client_id (which
// is being retired) and asking whether to continue with it anyway. It
// should only be used when the user has left client_id blank on the
// OAuth path.
//
// state is the config state to go to next, service is the human
// readable name of the service (eg "Google Drive") and helpURL points
// at the docs describing how to make your own client_id.
//
// The question defaults to No to steer the user towards making their
// own client_id.
func SharedClientIDConfigConfirm(state, service, helpURL string) (*fs.ConfigOut, error) {
	return fs.ConfigConfirm(state, false, "config_shared_client_id", fmt.Sprintf(`rclone's shared %s client_id is being retired and will stop working during 2026.
Create your own to avoid interruption: %s

Continue using the shared client_id anyway?`, service, helpURL))
}

// NewClientWithBaseClient gets a token from the config file and
// configures a Client with it.  It returns the client and a
// TokenSource which Invalidate may need to be called on.  It uses the
// httpClient passed in as the base client.
func NewClientWithBaseClient(ctx context.Context, name string, m configmap.Mapper, config *Config, baseClient *http.Client) (*http.Client, *TokenSource, error) {
	config, _ = OverrideCredentials(name, m, config)
	token, err := GetToken(name, m)
	if err != nil && !config.ClientCredentialFlow {
		return nil, nil, err
	}

	// Set our own http client in the context
	ctx = Context(ctx, baseClient)

	// Wrap the TokenSource in our TokenSource which saves changed
	// tokens in the config file
	ts := &TokenSource{
		name:   name,
		m:      m,
		token:  token,
		config: config,
		ctx:    ctx,
	}
	return oauth2.NewClient(ctx, ts), ts, nil
}

// NewClientCredentialsClient creates a new OAuth module using the
// ClientCredential flow
func NewClientCredentialsClient(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, baseClient *http.Client) (*http.Client, *TokenSource, error) {
	oauthConfig, _ = OverrideCredentials(name, m, oauthConfig)
	token, _ := GetToken(name, m)
	// If the token doesn't exist then we will fetch one in the next step as we don't need a refresh token

	// Set our own http client in the context
	ctx = Context(ctx, baseClient)

	// Wrap the TokenSource in our TokenSource which saves changed
	// tokens in the config file
	ts := &TokenSource{
		name:   name,
		m:      m,
		token:  token,
		config: oauthConfig,
		ctx:    ctx,
	}
	return oauth2.NewClient(ctx, ts), ts, nil
}

// NewClient gets a token from the config file and configures a Client
// with it. It returns the client and a TokenSource which Invalidate
// may need to be called on
func NewClient(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config) (*http.Client, *TokenSource, error) {
	// Check whether we are using the client credentials flow
	if oauthConfig.ClientCredentialFlow {

		return NewClientCredentialsClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
	}
	return NewClientWithBaseClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
}

// AuthResult is returned from the web server after authorization
// success or failure
type AuthResult struct {
	OK          bool // Failure or Success?
	Name        string
	Description string
	Code        string
	HelpURL     string
	Form        url.Values // the complete contents of the form
	Err         error      // any underlying error to report
}

// Error satisfies the error interface so AuthResult can be used as an error
func (ar *AuthResult) Error() string {
	status := "Error"
	if ar.OK {
		status = "OK"
	}
	return fmt.Sprintf("%s: %s\nCode: %q\nDescription: %s\nHelp: %s",
		status, ar.Name, ar.Code, ar.Description, ar.HelpURL)
}

// CheckAuthFn is called when a good Auth has been received
type CheckAuthFn func(*Config, *AuthResult) error

// Options for the oauth config
type Options struct {
	OAuth2Config *Config                 // Basic config for oauth2
	NoOffline    bool                    // If set then "access_type=offline" parameter is not passed
	CheckAuth    CheckAuthFn             // When the AuthResult is known the checkAuth function is called if set
	OAuth2Opts   []oauth2.AuthCodeOption // extra oauth2 options
	StateBlankOK bool                    // If set, state returned as "" is deemed to be OK
}

// ConfigOut returns a config item suitable for the backend config
//
// state is the place to return the config to
// oAuth is the config to run the oauth with
func ConfigOut(state string, oAuth *Options) (*fs.ConfigOut, error) {
	return &fs.ConfigOut{
		State: state,
		OAuth: oAuth,
	}, nil
}

// ConfigOAuth does the oauth config specified in the config block
//
// This is called with a state which has pushed on it
//
//	state prefixed with "*oauth"
//	state for oauth to return to
//	state that returned the OAuth when we wish to recall it
//	value that returned the OAuth
func ConfigOAuth(ctx context.Context, name string, m configmap.Mapper, ri *fs.RegInfo, in fs.ConfigIn) (*fs.ConfigOut, error) {
	stateParams, state := fs.StatePop(in.State)

	// Make the next state
	newState := func(state string) string {
		return fs.StatePush(stateParams, state)
	}

	// Recall the Oauth state again by calling the Config with the same input again
	getOAuth := func() (opt *Options, err error) {
		tmpState, _ := fs.StatePop(stateParams)
		tmpState, State := fs.StatePop(tmpState)
		_, Result := fs.StatePop(tmpState)
		out, err := ri.Config(ctx, name, m, fs.ConfigIn{State: State, Result: Result})
		if err != nil {
			return nil, err
		}
		if out.OAuth == nil {
			return nil, errors.New("failed to recall OAuth state")
		}
		opt, ok := out.OAuth.(*Options)
		if !ok {
			return nil, fmt.Errorf("internal error: oauth failed: wrong type in config: %T", out.OAuth)
		}
		if opt.OAuth2Config == nil {
			return nil, errors.New("internal error: oauth failed: OAuth2Config not set")
		}
		return opt, nil
	}

	switch state {
	case "*oauth":
		// See if already have a token
		tokenString, ok := m.Get("token")
		if ok && tokenString != "" {
			return fs.ConfigConfirm(newState("*oauth-confirm"), true, "config_refresh_token", "Token already configured - replace it?")
		}
		return fs.ConfigGoto(newState("*oauth-confirm"))
	case "*oauth-confirm":
		if in.Result == "false" {
			return fs.ConfigGoto(newState("*oauth-done"))
		}
		opt, err := getOAuth()
		if err != nil {
			return nil, err
		}
		oauthConfig, _ := OverrideCredentials(name, m, opt.OAuth2Config)
		if oauthConfig.ClientCredentialFlow {
			// If using client credential flow, skip straight to getting the token since we don't need a browser
			return fs.ConfigGoto(newState("*oauth-do"))
		}
		return fs.ConfigConfirm(newState("*oauth-islocal"), true, "config_is_local", "Use web browser to automatically authenticate rclone with remote?\n * Say Y if the machine running rclone has a web browser you can use\n * Say N if running rclone on a (remote) machine without web browser access\nIf not sure try Y. If Y failed, try N.\n")
	case "*oauth-islocal":
		if in.Result == "true" {
			return fs.ConfigGoto(newState("*oauth-do"))
		}
		return fs.ConfigGoto(newState("*oauth-remote"))
	case "*oauth-remote":
		opt, err := getOAuth()
		if err != nil {
			return nil, err
		}
		if noWebserverNeeded(opt.OAuth2Config) {
			authURL, _, err := getAuthURL(name, m, opt.OAuth2Config, opt)
			if err != nil {
				return nil, err
			}
			return fs.ConfigInput(newState("*oauth-do"), "config_verification_code", fmt.Sprintf("Verification code\n\nGo to this URL, authenticate then paste the code here.\n\n%s\n", authURL))
		}
		var out strings.Builder
		fmt.Fprintf(&out, `For this to work, you will need rclone available on a machine that has
a web browser available.

For more help and alternate methods see: https://rclone.org/remote_setup/

Execute the following on the machine with the web browser (same rclone
version recommended):

`)
		// Find the overridden options
		inM := ri.Options.NonDefault(m)
		delete(inM, fs.ConfigToken) // delete token as we are refreshing it
		ci := fs.GetConfig(ctx)
		for k, v := range inM {
			fs.Debugf(nil, "sending %s = %s", k, fs.RedactOptionValue(ci, ri.Options.Get(k), v))
		}
		// Encode them into a string
		mCopyString, err := inM.Encode()
		if err != nil {
			return nil, fmt.Errorf("oauthutil authorize encode: %w", err)
		}
		// Write what the user has to do
		if len(mCopyString) > 0 {
			fmt.Fprintf(&out, "\trclone authorize %q %q\n", ri.Name, mCopyString)
		} else {
			fmt.Fprintf(&out, "\trclone authorize %q\n", ri.Name)
		}
		fmt.Fprintln(&out, "\nThen paste the result.")
		return fs.ConfigInput(newState("*oauth-authorize"), "config_token", out.String())
	case "*oauth-authorize":
		// Read the updates to the config
		outM := configmap.Simple{}
		token := oauth2.Token{}
		code := in.Result
		newFormat := true
		err := outM.Decode(code)
		if err != nil {
			newFormat = false
			err = json.Unmarshal([]byte(code), &token)
		}
		if err != nil {
			return fs.ConfigError(newState("*oauth-authorize"), fmt.Sprintf("Couldn't decode response - try again (make sure you are using a matching version of rclone on both sides: %v\n", err))
		}
		// Save the config updates
		if newFormat {
			ci := fs.GetConfig(ctx)
			for k, v := range outM {
				m.Set(k, v)
				fs.Debugf(nil, "received %s = %s", k, fs.RedactOptionValue(ci, ri.Options.Get(k), v))
			}
		} else {
			m.Set(fs.ConfigToken, code)
		}
		return fs.ConfigGoto(newState("*oauth-done"))
	case "*oauth-do":
		// Make sure we can read the HTML template file if it was specified.
		configTemplateFile, _ := m.Get("config_template_file")
		configTemplateString, _ := m.Get("config_template")

		if configTemplateFile != "" {
			dat, err := os.ReadFile(configTemplateFile)

			if err != nil {
				return nil, fmt.Errorf("failed to read template file: %w", err)
			}

			templateString = string(dat)
		} else if configTemplateString != "" {
			templateString = configTemplateString
		} else {
			templateString = DefaultAuthResponseTemplate
		}
		code := in.Result
		opt, err := getOAuth()
		if err != nil {
			return nil, err
		}
		oauthConfig, changed := OverrideCredentials(name, m, opt.OAuth2Config)
		if changed {
			fs.Logf(nil, "Make sure your Redirect URL is set to %q in your custom config.\n", oauthConfig.RedirectURL)
		}
		if oauthConfig.ClientCredentialFlow {
			err = clientCredentialsFlowGetToken(ctx, name, m, oauthConfig, opt)
			if err != nil {
				return nil, err
			}
		} else {
			if code == "" {
				oauthConfig = fixRedirect(oauthConfig)
				code, err = configSetup(ctx, ri.Name, name, m, oauthConfig, opt)
				if err != nil {
					return nil, fmt.Errorf("config failed to refresh token: %w", err)
				}
			}
			err = configExchange(ctx, name, m, oauthConfig, code)
			if err != nil {
				return nil, err
			}
		}
		return fs.ConfigGoto(newState("*oauth-done"))
	case "*oauth-done":
		// Return to the state indicated in the State stack
		_, returnState := fs.StatePop(stateParams)
		return fs.ConfigGoto(returnState)
	}
	return nil, fmt.Errorf("unknown internal oauth state %q", state)
}

func init() {
	// Set the function to avoid circular import
	fs.ConfigOAuth = ConfigOAuth
}

func init() {
	rc.Add(rc.Call{
		Path:  "config/oauthstop",
		Fn:    rcOAuthStop,
		Title: "Stop any running OAuth authentication server.",
		Help: `Stops the OAuth authentication server if one is running.

This can be used to recover from an interrupted OAuth flow without
restarting rclone. If no OAuth authentication is in progress, an error
is returned.
`,
	})
}

func rcOAuthStop(ctx context.Context, in rc.Params) (out rc.Params, err error) {
	oauthCancelMu.Lock()
	defer oauthCancelMu.Unlock()
	if oauthCancelFn == nil {
		return nil, errors.New("no oauth authentication is in progress")
	}
	oauthCancelFn()
	oauthCancelFn = nil
	oauthURL = ""
	return nil, nil
}

func init() {
	rc.Add(rc.Call{
		Path:  "config/oauthstatus",
		Fn:    rcOAuthStatus,
		Title: "Get the status of the OAuth authentication server.",
		Help: `Returns the current status of the OAuth authentication server.

Returns a JSON object:
- status - "running" or "stopped"
- authUrl - URL for the authorization (only if status is "running")

Eg

    {
        "status": "running",
        "authUrl": "http://127.0.0.1:53682/auth?state=..."
    }
`,
	})
}

func rcOAuthStatus(ctx context.Context, in rc.Params) (out rc.Params, err error) {
	oauthCancelMu.Lock()
	defer oauthCancelMu.Unlock()
	status := "stopped"
	params := rc.Params{"status": status}
	if oauthCancelFn != nil {
		status = "running"
		params["status"] = status
		params["authUrl"] = oauthURL
	}
	return params, nil
}

// Return true if can run without a webserver and just entering a code
func noWebserverNeeded(oauthConfig *Config) bool {
	return oauthConfig.RedirectURL == TitleBarRedirectURL
}

// get the URL we need to send the user to
func getAuthURL(name string, m configmap.Mapper, oauthConfig *Config, opt *Options) (authURL string, state string, err error) {
	oauthConfig, _ = OverrideCredentials(name, m, oauthConfig)

	// Make random state
	state, err = random.Password(128)
	if err != nil {
		return "", "", err
	}

	// Create the configuration required for the OAuth flow
	oauth2Conf := oauthConfig.MakeOauth2Config()

	// Generate oauth URL
	opts := opt.OAuth2Opts
	if !opt.NoOffline {
		opts = append(opts, oauth2.AccessTypeOffline)
	}
	authURL = oauth2Conf.AuthCodeURL(state, opts...)
	return authURL, state, nil
}

// If TitleBarRedirect is set but we are doing a real oauth, then
// override our redirect URL
func fixRedirect(oauthConfig *Config) *Config {
	switch oauthConfig.RedirectURL {
	case TitleBarRedirectURL:
		// copy the config and set to use the internal webserver
		configCopy := *oauthConfig
		oauthConfig = &configCopy
		oauthConfig.RedirectURL = RedirectURL
	}
	return oauthConfig
}

// configSetup does the initial creation of the token for the client credentials flow
//
// If opt is nil it will use the default Options.
func clientCredentialsFlowGetToken(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, opt *Options) error {
	if opt == nil {
		opt = &Options{}
	}
	_ = opt // not currently using the Options
	fs.Debugf(nil, "Getting token for client credentials flow")
	_, tokenSource, err := NewClientCredentialsClient(ctx, name, m, oauthConfig, fshttp.NewClient(ctx))
	if err != nil {
		return fmt.Errorf("client credentials flow: failed to make client: %w", err)
	}
	// Get the token and save it in the config file
	_, err = tokenSource.Token()
	if err != nil {
		return fmt.Errorf("client credentials flow: failed to get token: %w", err)
	}
	return nil
}

// configSetup does the initial creation of the token
//
// If opt is nil it will use the default Options.
//
// It will run an internal webserver to receive the results
func configSetup(ctx context.Context, id, name string, m configmap.Mapper, oauthConfig *Config, opt *Options) (string, error) {
	if opt == nil {
		opt = &Options{}
	}
	authorizeNoAutoBrowserValue, ok := m.Get(config.ConfigAuthNoBrowser)
	authorizeNoAutoBrowser := ok && authorizeNoAutoBrowserValue != ""

	authURL, state, err := getAuthURL(name, m, oauthConfig, opt)
	if err != nil {
		return "", err
	}

	// Prepare webserver
	server := newAuthServer(opt, bindAddress, state, authURL)
	err = server.Init()
	if err != nil {
		return "", fmt.Errorf("failed to start auth webserver: %w", err)
	}
	authURL = "http://" + bindAddress + "/auth?state=" + state

	oauthCtx, cancel := context.WithCancel(ctx)
	oauthCancelMu.Lock()
	oauthCancelFn = cancel
	oauthURL = authURL
	oauthCancelMu.Unlock()

	go server.Serve()
	defer func() {
		oauthCancelMu.Lock()
		oauthCancelFn = nil
		oauthURL = ""
		oauthCancelMu.Unlock()
		cancel()
		server.Stop()
	}()

	if !authorizeNoAutoBrowser {
		// Open the URL for the user to visit
		err := OpenURL(authURL)
		if err != nil {
			fs.Errorf(nil, "Failed to open browser automatically (%v) - please go to the following link: %s\n", err, authURL)
		} else {
			fs.Logf(nil, "If your browser doesn't open automatically go to the following link: %s\n", authURL)
		}
	} else {
		fs.Logf(nil, "Please go to the following link: %s\n", authURL)
	}
	fs.Logf(nil, "Log in and authorize rclone for access\n")

	// Read the code via the webserver
	fs.Logf(nil, "Waiting for code...\n")
	var auth *AuthResult
	select {
	case auth = <-server.result:
	case <-oauthCtx.Done():
		return "", errors.New("oauth authentication was cancelled")
	}
	if !auth.OK || auth.Code == "" {
		return "", auth
	}
	fs.Logf(nil, "Got code\n")
	if opt.CheckAuth != nil {
		err = opt.CheckAuth(oauthConfig, auth)
		if err != nil {
			return "", err
		}
	}
	return auth.Code, nil
}

// Exchange the code for a token
func configExchange(ctx context.Context, name string, m configmap.Mapper, oauthConfig *Config, code string) error {
	ctx = Context(ctx, fshttp.NewClient(ctx))

	// Create the configuration required for the OAuth flow
	oauth2Conf := oauthConfig.MakeOauth2Config()

	token, err := oauth2Conf.Exchange(ctx, code)
	if err != nil {
		return fmt.Errorf("failed to get token: %w", err)
	}
	return PutToken(name, m, token, true)
}

// Local web server for collecting auth
type authServer struct {
	opt         *Options
	state       string
	listener    net.Listener
	bindAddress string
	authURL     string
	server      *http.Server
	result      chan *AuthResult
}

// newAuthServer makes the webserver for collecting auth
func newAuthServer(opt *Options, bindAddress, state, authURL string) *authServer {
	return &authServer{
		opt:         opt,
		state:       state,
		bindAddress: bindAddress,
		authURL:     authURL, // http://host/auth redirects to here
		result:      make(chan *AuthResult, 1),
	}
}

// Receive the auth request
func (s *authServer) handleAuth(w http.ResponseWriter, req *http.Request) {
	if req.URL.Path != "/" {
		fs.Debugf(nil, "Ignoring %s request on auth server to %q", req.Method, req.URL.Path)
		http.NotFound(w, req)
		return
	}
	fs.Debugf(nil, "Received %s request on auth server to %q", req.Method, req.URL.Path)

	// Reply with the response to the user and to the channel
	reply := func(status int, res *AuthResult) {
		w.WriteHeader(status)
		w.Header().Set("Content-Type", "text/html")
		var t = template.Must(template.New("authResponse").Parse(templateString))
		if err := t.Execute(w, res); err != nil {
			fs.Debugf(nil, "Could not execute template for web response.")
		}
		s.result <- res
	}

	// Parse the form parameters and save them
	err := req.ParseForm()
	if err != nil {
		reply(http.StatusBadRequest, &AuthResult{
			Name:        "Parse form error",
			Description: err.Error(),
		})
		return
	}

	// get code, error if empty
	code := req.Form.Get("code")
	if code == "" {
		err := &AuthResult{
			Name:        "Auth Error",
			Description: "No code returned by remote server",
		}
		if errorCode := req.Form.Get("error"); errorCode != "" {
			err.Description += ": " + errorCode
		}
		if errorMessage := req.Form.Get("error_description"); errorMessage != "" {
			err.Description += ": " + errorMessage
		}
		reply(http.StatusBadRequest, err)
		return
	}

	// check state
	state := req.Form.Get("state")
	if state != s.state && !(state == "" && s.opt.StateBlankOK) {
		reply(http.StatusBadRequest, &AuthResult{
			Name:        "Auth state doesn't match",
			Description: fmt.Sprintf("Expecting %q got %q", s.state, state),
		})
		return
	}

	// code OK
	reply(http.StatusOK, &AuthResult{
		OK:   true,
		Code: code,
		Form: req.Form,
	})
}

// Init gets the internal web server ready to receive config details
func (s *authServer) Init() error {
	fs.Debugf(nil, "Starting auth server on %s", s.bindAddress)
	mux := http.NewServeMux()
	s.server = &http.Server{
		Addr:    s.bindAddress,
		Handler: mux,
	}
	s.server.SetKeepAlivesEnabled(false)

	mux.HandleFunc("/auth", func(w http.ResponseWriter, req *http.Request) {
		state := req.FormValue("state")
		if state != s.state {
			fs.Debugf(nil, "State did not match: want %q got %q", s.state, state)
			http.Error(w, "State did not match - please try again", http.StatusForbidden)
			return
		}
		fs.Debugf(nil, "Redirecting browser to: %s", s.authURL)
		http.Redirect(w, req, s.authURL, http.StatusTemporaryRedirect)
	})
	mux.HandleFunc("/", s.handleAuth)

	var err error
	s.listener, err = net.Listen("tcp", s.bindAddress)
	if err != nil {
		return err
	}
	return nil
}

// Serve the auth server, doesn't return
func (s *authServer) Serve() {
	err := s.server.Serve(s.listener)
	fs.Debugf(nil, "Closed auth server with error: %v", err)
}

// Stop the auth server by closing its socket
func (s *authServer) Stop() {
	fs.Debugf(nil, "Closing auth server")
	close(s.result)
	_ = s.listener.Close()

	// close the server
	_ = s.server.Close()
}
