|
|
|
/*
|
|
|
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/rpc"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Attempt to retry only this many number of times before
|
|
|
|
// giving up on the remote RPC entirely.
|
|
|
|
const globalAuthRPCRetryThreshold = 1
|
|
|
|
|
|
|
|
// authConfig requires to make new AuthRPCClient.
|
|
|
|
type authConfig struct {
|
|
|
|
accessKey string // Access key (like username) for authentication.
|
|
|
|
secretKey string // Secret key (like Password) for authentication.
|
|
|
|
serverAddr string // RPC server address.
|
|
|
|
serviceEndpoint string // Endpoint on the server to make any RPC call.
|
|
|
|
secureConn bool // Make TLS connection to RPC server or not.
|
|
|
|
serviceName string // Service name of auth server.
|
|
|
|
disableReconnect bool // Disable reconnect on failure or not.
|
|
|
|
|
|
|
|
/// Retry configurable values.
|
|
|
|
|
|
|
|
// Each retry unit multiplicative, measured in time.Duration.
|
|
|
|
// This is the basic unit used for calculating backoffs.
|
|
|
|
retryUnit time.Duration
|
|
|
|
// Maximum retry duration i.e A caller would wait no more than this
|
|
|
|
// duration to continue their loop.
|
|
|
|
retryCap time.Duration
|
|
|
|
|
|
|
|
// Maximum retries an call authRPC client would do for a failed
|
|
|
|
// RPC call.
|
|
|
|
retryAttemptThreshold int
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuthRPCClient is a authenticated RPC client which does authentication before doing Call().
|
|
|
|
type AuthRPCClient struct {
|
|
|
|
sync.RWMutex // Mutex to lock this object.
|
|
|
|
rpcClient *rpc.Client // RPC client to make any RPC call.
|
|
|
|
config authConfig // Authentication configuration information.
|
|
|
|
authToken string // Authentication token.
|
|
|
|
version semVersion // RPC version.
|
|
|
|
}
|
|
|
|
|
|
|
|
// newAuthRPCClient - returns a JWT based authenticated (go) rpc client, which does automatic reconnect.
|
|
|
|
func newAuthRPCClient(config authConfig) *AuthRPCClient {
|
|
|
|
// Check if retry params are set properly if not default them.
|
|
|
|
emptyDuration := time.Duration(int64(0))
|
|
|
|
if config.retryUnit == emptyDuration {
|
|
|
|
config.retryUnit = defaultRetryUnit
|
|
|
|
}
|
|
|
|
if config.retryCap == emptyDuration {
|
|
|
|
config.retryCap = defaultRetryCap
|
|
|
|
}
|
|
|
|
if config.retryAttemptThreshold == 0 {
|
|
|
|
config.retryAttemptThreshold = globalAuthRPCRetryThreshold
|
|
|
|
}
|
|
|
|
|
|
|
|
return &AuthRPCClient{
|
|
|
|
config: config,
|
|
|
|
version: globalRPCAPIVersion,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login a JWT based authentication is performed with rpc server.
|
|
|
|
func (authClient *AuthRPCClient) Login() (err error) {
|
|
|
|
// Login should be attempted one at a time.
|
|
|
|
//
|
|
|
|
// The reason for large region lock here is
|
|
|
|
// to avoid two simultaneous login attempts
|
|
|
|
// racing over each other.
|
|
|
|
//
|
|
|
|
// #1 Login() gets the lock proceeds to login.
|
|
|
|
// #2 Login() waits for the unlock to happen
|
|
|
|
// after login in #1.
|
|
|
|
// #1 Successfully completes login saves the
|
|
|
|
// newly acquired token.
|
|
|
|
// #2 Successfully gets the lock and proceeds,
|
|
|
|
// but since we have acquired the token
|
|
|
|
// already the call quickly returns.
|
|
|
|
authClient.Lock()
|
|
|
|
defer authClient.Unlock()
|
|
|
|
|
|
|
|
// Attempt to login if not logged in already.
|
|
|
|
if authClient.authToken == "" {
|
|
|
|
var authToken string
|
|
|
|
authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login to authenticate your token.
|
|
|
|
var (
|
|
|
|
loginMethod = authClient.config.serviceName + loginMethodName
|
|
|
|
loginArgs = LoginRPCArgs{
|
|
|
|
AuthToken: authToken,
|
|
|
|
Version: globalRPCAPIVersion,
|
|
|
|
RequestTime: UTCNow(),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Re-dial after we have disconnected or if its a fresh run.
|
|
|
|
var rpcClient *rpc.Client
|
|
|
|
rpcClient, err = rpcDial(authClient.config.serverAddr, authClient.config.serviceEndpoint, authClient.config.secureConn)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
|
|
|
|
// gob doesn't provide any typed errors for us to reflect
|
|
|
|
// upon, this is the only way to return proper error.
|
|
|
|
if strings.Contains(err.Error(), "gob: wrong type") {
|
|
|
|
return errRPCAPIVersionUnsupported
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize rpc client and auth token after a successful login.
|
|
|
|
authClient.authToken = authToken
|
|
|
|
authClient.rpcClient = rpcClient
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// call makes a RPC call after logs into the server.
|
|
|
|
func (authClient *AuthRPCClient) call(serviceMethod string, args interface {
|
|
|
|
SetAuthToken(authToken string)
|
|
|
|
SetRPCAPIVersion(version semVersion)
|
|
|
|
}, reply interface{}) (err error) {
|
|
|
|
if err = authClient.Login(); err != nil {
|
|
|
|
return err
|
|
|
|
} // On successful login, execute RPC call.
|
|
|
|
|
|
|
|
// Set token before the rpc call.
|
|
|
|
authClient.RLock()
|
|
|
|
defer authClient.RUnlock()
|
|
|
|
args.SetAuthToken(authClient.authToken)
|
|
|
|
args.SetRPCAPIVersion(authClient.version)
|
|
|
|
|
|
|
|
// Do an RPC call.
|
|
|
|
return authClient.rpcClient.Call(serviceMethod, args, reply)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call executes RPC call till success or globalAuthRPCRetryThreshold on ErrShutdown.
|
|
|
|
func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
|
|
|
|
SetAuthToken(authToken string)
|
|
|
|
SetRPCAPIVersion(version semVersion)
|
|
|
|
}, reply interface{}) (err error) {
|
|
|
|
|
|
|
|
// Done channel is used to close any lingering retry routine, as soon
|
|
|
|
// as this function returns.
|
|
|
|
doneCh := make(chan struct{})
|
|
|
|
defer close(doneCh)
|
|
|
|
|
|
|
|
for i := range newRetryTimer(authClient.config.retryUnit, authClient.config.retryCap, doneCh) {
|
|
|
|
if err = authClient.call(serviceMethod, args, reply); err == rpc.ErrShutdown {
|
|
|
|
// As connection at server side is closed, close the rpc client.
|
|
|
|
authClient.Close()
|
|
|
|
|
|
|
|
// Retry if reconnect is not disabled.
|
|
|
|
if !authClient.config.disableReconnect {
|
|
|
|
// Retry until threshold reaches.
|
|
|
|
if i < authClient.config.retryAttemptThreshold {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// gob doesn't provide any typed errors for us to reflect
|
|
|
|
// upon, this is the only way to return proper error.
|
|
|
|
if err != nil && strings.Contains(err.Error(), "gob: wrong type") {
|
|
|
|
err = errRPCAPIVersionUnsupported
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes underlying RPC Client.
|
|
|
|
func (authClient *AuthRPCClient) Close() error {
|
|
|
|
authClient.Lock()
|
|
|
|
defer authClient.Unlock()
|
|
|
|
|
|
|
|
if authClient.rpcClient == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
authClient.authToken = ""
|
|
|
|
return authClient.rpcClient.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServerAddr returns the serverAddr (network address) of the connection.
|
|
|
|
func (authClient *AuthRPCClient) ServerAddr() string {
|
|
|
|
return authClient.config.serverAddr
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServiceEndpoint returns the RPC service endpoint of the connection.
|
|
|
|
func (authClient *AuthRPCClient) ServiceEndpoint() string {
|
|
|
|
return authClient.config.serviceEndpoint
|
|
|
|
}
|
|
|
|
|
|
|
|
// default Dial timeout for RPC connections.
|
|
|
|
const defaultDialTimeout = 3 * time.Second
|
|
|
|
|
|
|
|
// Connect success message required from rpc server.
|
|
|
|
const connectSuccessMessage = "200 Connected to Go RPC"
|
|
|
|
|
|
|
|
// dial tries to establish a connection to serverAddr in a safe manner.
|
|
|
|
// If there is a valid rpc.Cliemt, it returns that else creates a new one.
|
|
|
|
func rpcDial(serverAddr, serviceEndpoint string, secureConn bool) (netRPCClient *rpc.Client, err error) {
|
|
|
|
if serverAddr == "" || serviceEndpoint == "" {
|
|
|
|
return nil, errInvalidArgument
|
|
|
|
}
|
|
|
|
d := &net.Dialer{
|
|
|
|
Timeout: defaultDialTimeout,
|
|
|
|
}
|
|
|
|
var conn net.Conn
|
|
|
|
if secureConn {
|
|
|
|
var hostname string
|
|
|
|
if hostname, _, err = net.SplitHostPort(serverAddr); err != nil {
|
|
|
|
return nil, &net.OpError{
|
|
|
|
Op: "dial-http",
|
|
|
|
Net: serverAddr + serviceEndpoint,
|
|
|
|
Addr: nil,
|
|
|
|
Err: fmt.Errorf("Unable to parse server address <%s>: %s", serverAddr, err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// ServerName in tls.Config needs to be specified to support SNI certificates.
|
|
|
|
conn, err = tls.DialWithDialer(d, "tcp", serverAddr, &tls.Config{
|
|
|
|
ServerName: hostname,
|
|
|
|
RootCAs: globalRootCAs,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
conn, err = d.Dial("tcp", serverAddr)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
// Print RPC connection errors that are worthy to display in log.
|
|
|
|
switch err.(type) {
|
|
|
|
case x509.HostnameError:
|
|
|
|
errorIf(err, "Unable to establish secure connection to %s", serverAddr)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, &net.OpError{
|
|
|
|
Op: "dial-http",
|
|
|
|
Net: serverAddr + serviceEndpoint,
|
|
|
|
Addr: nil,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for network errors writing over the dialed conn.
|
|
|
|
if _, err = io.WriteString(conn, "CONNECT "+serviceEndpoint+" HTTP/1.0\n\n"); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, &net.OpError{
|
|
|
|
Op: "dial-http",
|
|
|
|
Net: serverAddr + serviceEndpoint,
|
|
|
|
Addr: nil,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to read the HTTP response for the HTTP method CONNECT, upon
|
|
|
|
// success return the RPC connection instance.
|
|
|
|
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{
|
|
|
|
Method: http.MethodConnect,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, &net.OpError{
|
|
|
|
Op: "dial-http",
|
|
|
|
Net: serverAddr + serviceEndpoint,
|
|
|
|
Addr: nil,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if resp.Status != connectSuccessMessage {
|
|
|
|
conn.Close()
|
|
|
|
return nil, errors.New("unexpected HTTP response: " + resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize rpc client.
|
|
|
|
return rpc.NewClient(conn), nil
|
|
|
|
}
|