// +build go1.13

/*
 *
 * Copyright 2020 gRPC authors.
 *
 * 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 meshca

import (
	"crypto/x509"
	"encoding/json"
	"fmt"
	"sync"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/sts"
	"google.golang.org/grpc/credentials/tls/certprovider"
	"google.golang.org/grpc/internal/backoff"
)

const pluginName = "mesh_ca"

// For overriding in unit tests.
var (
	grpcDialFunc = grpc.Dial
	backoffFunc  = backoff.DefaultExponential.Backoff
)

func init() {
	certprovider.Register(newPluginBuilder())
}

func newPluginBuilder() *pluginBuilder {
	return &pluginBuilder{clients: make(map[ccMapKey]*refCountedCC)}
}

// Key for the map containing ClientConns to the MeshCA server. Only the server
// name and the STS options (which is used to create call creds) from the plugin
// configuration determine if two configs can share the same ClientConn. Hence
// only those form the key to this map.
type ccMapKey struct {
	name    string
	stsOpts sts.Options
}

// refCountedCC wraps a grpc.ClientConn to MeshCA along with a reference count.
type refCountedCC struct {
	cc     *grpc.ClientConn
	refCnt int
}

// pluginBuilder is an implementation of the certprovider.Builder interface,
// which build certificate provider instances which get certificates signed from
// the MeshCA.
type pluginBuilder struct {
	// A collection of ClientConns to the MeshCA server along with a reference
	// count. Provider instances whose config point to the same server name will
	// end up sharing the ClientConn.
	mu      sync.Mutex
	clients map[ccMapKey]*refCountedCC
}

// Build returns a MeshCA certificate provider for the passed in configuration
// and options.
//
// This builder takes care of sharing the ClientConn to the MeshCA server among
// different plugin instantiations.
func (b *pluginBuilder) Build(c certprovider.StableConfig, opts certprovider.Options) certprovider.Provider {
	cfg, ok := c.(*pluginConfig)
	if !ok {
		// This is not expected when passing config returned by ParseConfig().
		// This could indicate a bug in the certprovider.Store implementation or
		// in cases where the user is directly using these APIs, could be a user
		// error.
		logger.Errorf("unsupported config type: %T", c)
		return nil
	}

	b.mu.Lock()
	defer b.mu.Unlock()

	ccmk := ccMapKey{
		name:    cfg.serverURI,
		stsOpts: cfg.stsOpts,
	}
	rcc, ok := b.clients[ccmk]
	if !ok {
		// STS call credentials take care of exchanging a locally provisioned
		// JWT token for an access token which will be accepted by the MeshCA.
		callCreds, err := sts.NewCredentials(cfg.stsOpts)
		if err != nil {
			logger.Errorf("sts.NewCredentials() failed: %v", err)
			return nil
		}

		// MeshCA is a public endpoint whose certificate is Web-PKI compliant.
		// So, we just need to use the system roots to authenticate the MeshCA.
		cp, err := x509.SystemCertPool()
		if err != nil {
			logger.Errorf("x509.SystemCertPool() failed: %v", err)
			return nil
		}
		transportCreds := credentials.NewClientTLSFromCert(cp, "")

		cc, err := grpcDialFunc(cfg.serverURI, grpc.WithTransportCredentials(transportCreds), grpc.WithPerRPCCredentials(callCreds))
		if err != nil {
			logger.Errorf("grpc.Dial(%s) failed: %v", cfg.serverURI, err)
			return nil
		}

		rcc = &refCountedCC{cc: cc}
		b.clients[ccmk] = rcc
	}
	rcc.refCnt++

	p := newProviderPlugin(providerParams{
		cc:      rcc.cc,
		cfg:     cfg,
		opts:    opts,
		backoff: backoffFunc,
		doneFunc: func() {
			// The plugin implementation will invoke this function when it is
			// being closed, and here we take care of closing the ClientConn
			// when there are no more plugins using it. We need to acquire the
			// lock before accessing the rcc from the enclosing function.
			b.mu.Lock()
			defer b.mu.Unlock()
			rcc.refCnt--
			if rcc.refCnt == 0 {
				logger.Infof("Closing grpc.ClientConn to %s", ccmk.name)
				rcc.cc.Close()
				delete(b.clients, ccmk)
			}
		},
	})
	return p
}

// ParseConfig parses the configuration to be passed to the MeshCA plugin
// implementation. Expects the config to be a json.RawMessage which contains a
// serialized JSON representation of the meshca_experimental.GoogleMeshCaConfig
// proto message.
func (b *pluginBuilder) ParseConfig(c interface{}) (certprovider.StableConfig, error) {
	data, ok := c.(json.RawMessage)
	if !ok {
		return nil, fmt.Errorf("meshca: unsupported config type: %T", c)
	}
	return pluginConfigFromJSON(data)
}

// Name returns the MeshCA plugin name.
func (b *pluginBuilder) Name() string {
	return pluginName
}
