diff --git a/lib/oauthutil/oauthutil.go b/lib/oauthutil/oauthutil.go index 02b65f244..fdb480540 100644 --- a/lib/oauthutil/oauthutil.go +++ b/lib/oauthutil/oauthutil.go @@ -2,13 +2,12 @@ package oauthutil import ( "context" - "crypto/rand" "encoding/json" "fmt" "html/template" "net" "net/http" - "strings" + "net/url" "sync" "time" @@ -17,6 +16,7 @@ import ( "github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/fshttp" + "github.com/rclone/rclone/lib/random" "github.com/skratchdot/open-golang/open" "golang.org/x/oauth2" ) @@ -46,8 +46,8 @@ const ( // redirects to the local webserver RedirectPublicSecureURL = "https://oauth.rclone.org/" - // AuthResponse is a template to handle the redirect URL for oauth requests - AuthResponse = ` + // AuthResponseTemplate is a template to handle the redirect URL for oauth requests + AuthResponseTemplate = `
@@ -58,17 +58,12 @@ const (
{{ if eq .OK false }}
-Error: {{ .AuthError.Name }}
-{{ if .AuthError.Description }}Description: {{ .AuthError.Description }}
{{ end }}
-{{ if .AuthError.Code }}Code: {{ .AuthError.Code }}
{{ end }}
-{{ if .AuthError.HelpURL }}Look here for help: {{ .AuthError.HelpURL }}
{{ end }}
+Error: {{ .Name }}
+{{ if .Description }}Description: {{ .Description }}
{{ end }}
+{{ if .Code }}Code: {{ .Code }}
{{ end }}
+{{ if .HelpURL }}Look here for help: {{ .HelpURL }}
{{ end }}
{{ else }}
- {{ if .Code }}
-Please copy this code into rclone:
-{{ .Code }}
- {{ else }}
All done. Please go back to rclone.
- {{ end }}
{{ end }}
@@ -340,26 +335,54 @@ func NewClient(name string, m configmap.Mapper, oauthConfig *oauth2.Config) (*ht
return NewClientWithBaseClient(name, m, oauthConfig, fshttp.NewClient(fs.Config))
}
+// 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)
+}
+
// Config does the initial creation of the token
//
// It may run an internal webserver to receive the results
func Config(id, name string, m configmap.Mapper, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
- return doConfig(id, name, m, nil, config, true, opts)
+ return doConfig(id, name, m, config, true, nil, opts)
+}
+
+// CheckAuthFn is called when a good Auth has been received
+type CheckAuthFn func(*oauth2.Config, *AuthResult) error
+
+// ConfigWithCallback does the initial creation of the token
+//
+// It may run an internal webserver to receive the results
+//
+// When the AuthResult is known the checkAuth function is called if set
+func ConfigWithCallback(id, name string, m configmap.Mapper, config *oauth2.Config, checkAuth CheckAuthFn, opts ...oauth2.AuthCodeOption) error {
+ return doConfig(id, name, m, config, true, checkAuth, opts)
}
// ConfigNoOffline does the same as Config but does not pass the
// "access_type=offline" parameter.
func ConfigNoOffline(id, name string, m configmap.Mapper, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
- return doConfig(id, name, m, nil, config, false, opts)
+ return doConfig(id, name, m, config, false, nil, opts)
}
-// ConfigErrorCheck does the same as Config, but allows the backend to pass a error handling function
-// This function gets called with the request made to rclone as a parameter if no code was found
-func ConfigErrorCheck(id, name string, m configmap.Mapper, errorHandler func(*http.Request) AuthError, config *oauth2.Config, opts ...oauth2.AuthCodeOption) error {
- return doConfig(id, name, m, errorHandler, config, true, opts)
-}
-
-func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Request) AuthError, oauthConfig *oauth2.Config, offline bool, opts []oauth2.AuthCodeOption) error {
+func doConfig(id, name string, m configmap.Mapper, oauthConfig *oauth2.Config, offline bool, checkAuth CheckAuthFn, opts []oauth2.AuthCodeOption) error {
oauthConfig, changed := overrideCredentials(name, m, oauthConfig)
authorizeOnlyValue, ok := m.Get(config.ConfigAuthorize)
authorizeOnly := ok && authorizeOnlyValue != "" // set if being run by "rclone authorize"
@@ -384,7 +407,18 @@ func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Reque
// Detect whether we should use internal web server
useWebServer := false
switch oauthConfig.RedirectURL {
- case RedirectURL, RedirectPublicURL, RedirectLocalhostURL, RedirectPublicSecureURL:
+ case TitleBarRedirectURL:
+ useWebServer = authorizeOnly
+ if !authorizeOnly {
+ useWebServer = isLocal()
+ }
+ if useWebServer {
+ // copy the config and set to use the internal webserver
+ configCopy := *oauthConfig
+ oauthConfig = &configCopy
+ oauthConfig.RedirectURL = RedirectURL
+ }
+ default:
if changed {
fmt.Printf("Make sure your Redirect URL is set to %q in your custom config.\n", oauthConfig.RedirectURL)
}
@@ -401,11 +435,7 @@ func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Reque
fmt.Printf("\trclone authorize %q\n", id)
}
fmt.Println("Then paste the result below:")
- code := ""
- for code == "" {
- fmt.Printf("result> ")
- code = strings.TrimSpace(config.ReadLine())
- }
+ code := config.ReadNonEmptyLine("result> ")
token := &oauth2.Token{}
err := json.Unmarshal([]byte(code), token)
if err != nil {
@@ -413,41 +443,24 @@ func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Reque
}
return PutToken(name, m, token, true)
}
- case TitleBarRedirectURL:
- useWebServer = authorizeOnly
- if !authorizeOnly {
- useWebServer = isLocal()
- }
- if useWebServer {
- // copy the config and set to use the internal webserver
- configCopy := *oauthConfig
- oauthConfig = &configCopy
- oauthConfig.RedirectURL = RedirectURL
- }
}
// Make random state
- stateBytes := make([]byte, 16)
- _, err := rand.Read(stateBytes)
+ state, err := random.Password(128)
if err != nil {
return err
}
- state := fmt.Sprintf("%x", stateBytes)
+
+ // Generate oauth URL
if offline {
opts = append(opts, oauth2.AccessTypeOffline)
}
authURL := oauthConfig.AuthCodeURL(state, opts...)
- // Prepare webserver
- server := authServer{
- state: state,
- bindAddress: bindAddress,
- authURL: authURL,
- errorHandler: errorHandler,
- }
+ // Prepare webserver if needed
+ var server *authServer
if useWebServer {
- server.code = make(chan string, 1)
- server.err = make(chan error, 1)
+ server = newAuthServer(bindAddress, state, authURL)
err := server.Init()
if err != nil {
return errors.Wrap(err, "failed to start auth webserver")
@@ -457,36 +470,39 @@ func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Reque
authURL = "http://" + bindAddress + "/auth?state=" + state
}
- // Generate a URL for the user to visit for authorization.
+ // Open the URL for the user to visit
_ = open.Start(authURL)
fmt.Printf("If your browser doesn't open automatically go to the following link: %s\n", authURL)
fmt.Printf("Log in and authorize rclone for access\n")
- var authCode string
+ // Read the code via the webserver or manually
+ var auth *AuthResult
if useWebServer {
- // Read the code, and exchange it for a token.
fmt.Printf("Waiting for code...\n")
- authCode = <-server.code
- authError := <-server.err
- if authCode != "" {
- fmt.Printf("Got code\n")
- } else {
- if authError != nil {
- return authError
+ auth = <-server.result
+ if !auth.OK || auth.Code == "" {
+ return auth
+ }
+ fmt.Printf("Got code\n")
+ if checkAuth != nil {
+ err = checkAuth(oauthConfig, auth)
+ if err != nil {
+ return err
}
- return errors.New("failed to get code")
}
} else {
- // Read the code, and exchange it for a token.
- fmt.Printf("Enter verification code> ")
- authCode = config.ReadLine()
+ auth = &AuthResult{
+ Code: config.ReadNonEmptyLine("Enter verification code> "),
+ }
}
- token, err := oauthConfig.Exchange(oauth2.NoContext, authCode)
+
+ // Exchange the code for a token
+ token, err := oauthConfig.Exchange(oauth2.NoContext, auth.Code)
if err != nil {
return errors.Wrap(err, "failed to get token")
}
- // Print code if we do automatic retrieval
+ // Print code if we are doing a manual auth
if authorizeOnly {
result, err := json.Marshal(token)
if err != nil {
@@ -499,29 +515,75 @@ func doConfig(id, name string, m configmap.Mapper, errorHandler func(*http.Reque
// Local web server for collecting auth
type authServer struct {
- state string
- listener net.Listener
- bindAddress string
- code chan string
- err chan error
- authURL string
- server *http.Server
- errorHandler func(*http.Request) AuthError
+ state string
+ listener net.Listener
+ bindAddress string
+ authURL string
+ server *http.Server
+ result chan *AuthResult
}
-// AuthError gets returned by the backend's errorHandler function
-type AuthError struct {
- Name string
- Description string
- Code string
- HelpURL string
+// newAuthServer makes the webserver for collecting auth
+func newAuthServer(bindAddress, state, authURL string) *authServer {
+ return &authServer{
+ state: state,
+ bindAddress: bindAddress,
+ authURL: authURL, // http://host/auth redirects to here
+ result: make(chan *AuthResult, 1),
+ }
}
-// AuthResponseData can fill the AuthResponse template
-type AuthResponseData struct {
- OK bool // Failure or Success?
- Code string // code to paste into rclone config
- AuthError
+// Receive the auth request
+func (s *authServer) handleAuth(w http.ResponseWriter, req *http.Request) {
+ 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(AuthResponseTemplate))
+ 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 == "" {
+ reply(http.StatusBadRequest, &AuthResult{
+ Name: "Auth Error",
+ Description: "No code returned by remote server",
+ })
+ return
+ }
+
+ // check state
+ state := req.Form.Get("state")
+ if state != s.state {
+ 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
@@ -533,67 +595,22 @@ func (s *authServer) Init() error {
Handler: mux,
}
s.server.SetKeepAlivesEnabled(false)
+
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
- http.Error(w, "", 404)
+ http.Error(w, "", http.StatusNotFound)
return
})
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", 403)
+ http.Error(w, "State did not match - please try again", http.StatusForbidden)
return
}
http.Redirect(w, req, s.authURL, http.StatusTemporaryRedirect)
return
})
- mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
- w.Header().Set("Content-Type", "text/html")
- fs.Debugf(nil, "Received request on auth server")
- code := req.FormValue("code")
- var err error
- var t = template.Must(template.New("authResponse").Parse(AuthResponse))
- resp := AuthResponseData{AuthError: AuthError{}}
- if code != "" {
- state := req.FormValue("state")
- if state != s.state {
- fs.Debugf(nil, "State did not match: want %q got %q", s.state, state)
- resp.OK = false
- resp.AuthError = AuthError{
- Name: "Auth State doesn't match",
- }
- } else {
- fs.Debugf(nil, "Successfully got code")
- resp.OK = true
- if s.code == nil {
- resp.Code = code
- }
- }
- } else {
- fs.Debugf(nil, "No code found on request")
- var authError AuthError
- if s.errorHandler == nil {
- authError = AuthError{
- Name: "Auth Error",
- Description: "No code found returned by remote server.",
- }
- } else {
- authError = s.errorHandler(req)
- }
- err = fmt.Errorf("Error: %s\nCode: %s\nDescription: %s\nHelp: %s",
- authError.Name, authError.Code, authError.Description, authError.HelpURL)
- resp.OK = false
- resp.AuthError = authError
- w.WriteHeader(500)
- }
- if err := t.Execute(w, resp); err != nil {
- fs.Debugf(nil, "Could not execute template for web response.")
- }
- if s.code != nil {
- s.code <- code
- s.err <- err
- }
- })
+ mux.HandleFunc("/", s.handleAuth)
var err error
s.listener, err = net.Listen("tcp", s.bindAddress)
@@ -612,10 +629,7 @@ func (s *authServer) Serve() {
// Stop the auth server by closing its socket
func (s *authServer) Stop() {
fs.Debugf(nil, "Closing auth server")
- if s.code != nil {
- close(s.code)
- s.code = nil
- }
+ close(s.result)
_ = s.listener.Close()
// close the server