// Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

package common

import (
	"bytes"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"strings"
)

// HTTPRequestSigner the interface to sign a request
type HTTPRequestSigner interface {
	Sign(r *http.Request) error
}

// KeyProvider interface that wraps information about the key's account owner
type KeyProvider interface {
	PrivateRSAKey() (*rsa.PrivateKey, error)
	KeyID() (string, error)
}

const signerVersion = "1"

// SignerBodyHashPredicate a function that allows to disable/enable body hashing
// of requests and headers associated with body content
type SignerBodyHashPredicate func(r *http.Request) bool

// ociRequestSigner implements the http-signatures-draft spec
// as described in https://tools.ietf.org/html/draft-cavage-http-signatures-08
type ociRequestSigner struct {
	KeyProvider    KeyProvider
	GenericHeaders []string
	BodyHeaders    []string
	ShouldHashBody SignerBodyHashPredicate
}

var (
	defaultGenericHeaders    = []string{"date", "(request-target)", "host"}
	defaultBodyHeaders       = []string{"content-length", "content-type", "x-content-sha256"}
	defaultBodyHashPredicate = func(r *http.Request) bool {
		return r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch
	}
)

// DefaultGenericHeaders list of default generic headers that is used in signing
func DefaultGenericHeaders() []string {
	return makeACopy(defaultGenericHeaders)
}

// DefaultBodyHeaders list of default body headers that is used in signing
func DefaultBodyHeaders() []string {
	return makeACopy(defaultBodyHeaders)
}

// DefaultRequestSigner creates a signer with default parameters.
func DefaultRequestSigner(provider KeyProvider) HTTPRequestSigner {
	return RequestSigner(provider, defaultGenericHeaders, defaultBodyHeaders)
}

// RequestSignerExcludeBody creates a signer without hash the body.
func RequestSignerExcludeBody(provider KeyProvider) HTTPRequestSigner {
	bodyHashPredicate := func(r *http.Request) bool {
		// week request signer will not hash the body
		return false
	}
	return RequestSignerWithBodyHashingPredicate(provider, defaultGenericHeaders, defaultBodyHeaders, bodyHashPredicate)
}

// NewSignerFromOCIRequestSigner creates a copy of the request signer and attaches the new SignerBodyHashPredicate
// returns an error if the passed signer is not of type ociRequestSigner
func NewSignerFromOCIRequestSigner(oldSigner HTTPRequestSigner, predicate SignerBodyHashPredicate) (HTTPRequestSigner, error) {
	if oldS, ok := oldSigner.(ociRequestSigner); ok {
		s := ociRequestSigner{
			KeyProvider:    oldS.KeyProvider,
			GenericHeaders: oldS.GenericHeaders,
			BodyHeaders:    oldS.BodyHeaders,
			ShouldHashBody: predicate,
		}
		return s, nil

	}
	return nil, fmt.Errorf("can not create a signer, input signer needs to be of type ociRequestSigner")
}

// RequestSigner creates a signer that utilizes the specified headers for signing
// and the default predicate for using the body of the request as part of the signature
func RequestSigner(provider KeyProvider, genericHeaders, bodyHeaders []string) HTTPRequestSigner {
	return ociRequestSigner{
		KeyProvider:    provider,
		GenericHeaders: genericHeaders,
		BodyHeaders:    bodyHeaders,
		ShouldHashBody: defaultBodyHashPredicate}
}

// RequestSignerWithBodyHashingPredicate creates a signer that utilizes the specified headers for signing, as well as a predicate for using
// the body of the request and bodyHeaders parameter as part of the signature
func RequestSignerWithBodyHashingPredicate(provider KeyProvider, genericHeaders, bodyHeaders []string, shouldHashBody SignerBodyHashPredicate) HTTPRequestSigner {
	return ociRequestSigner{
		KeyProvider:    provider,
		GenericHeaders: genericHeaders,
		BodyHeaders:    bodyHeaders,
		ShouldHashBody: shouldHashBody}
}

func (signer ociRequestSigner) getSigningHeaders(r *http.Request) []string {
	var result []string
	result = append(result, signer.GenericHeaders...)

	if signer.ShouldHashBody(r) {
		result = append(result, signer.BodyHeaders...)
	}

	return result
}

func (signer ociRequestSigner) getSigningString(request *http.Request) string {
	signingHeaders := signer.getSigningHeaders(request)
	signingParts := make([]string, len(signingHeaders))
	for i, part := range signingHeaders {
		var value string
		part = strings.ToLower(part)
		switch part {
		case "(request-target)":
			value = getRequestTarget(request)
		case "host":
			value = request.URL.Host
			if len(value) == 0 {
				value = request.Host
			}
		default:
			value = request.Header.Get(part)
		}
		signingParts[i] = fmt.Sprintf("%s: %s", part, value)
	}

	signingString := strings.Join(signingParts, "\n")
	return signingString

}

func getRequestTarget(request *http.Request) string {
	lowercaseMethod := strings.ToLower(request.Method)
	return fmt.Sprintf("%s %s", lowercaseMethod, request.URL.RequestURI())
}

func calculateHashOfBody(request *http.Request) (err error) {
	var hash string
	hash, err = GetBodyHash(request)
	if err != nil {
		return
	}
	request.Header.Set(requestHeaderXContentSHA256, hash)
	return
}

// drainBody reads all of b to memory and then returns two equivalent
// ReadClosers yielding the same bytes.
//
// It returns an error if the initial slurp of all bytes fails. It does not attempt
// to make the returned ReadClosers have identical error-matching behavior.
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
	if b == http.NoBody {
		// No copying needed. Preserve the magic sentinel meaning of NoBody.
		return http.NoBody, http.NoBody, nil
	}
	var buf bytes.Buffer
	if _, err = buf.ReadFrom(b); err != nil {
		return nil, b, err
	}
	if err = b.Close(); err != nil {
		return nil, b, err
	}
	return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
}

func hashAndEncode(data []byte) string {
	hashedContent := sha256.Sum256(data)
	hash := base64.StdEncoding.EncodeToString(hashedContent[:])
	return hash
}

// GetBodyHash creates a base64 string from the hash of body the request
func GetBodyHash(request *http.Request) (hashString string, err error) {
	if request.Body == nil {
		request.ContentLength = 0
		request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))
		return hashAndEncode([]byte("")), nil
	}

	var data []byte
	bReader := request.Body
	bReader, request.Body, err = drainBody(request.Body)
	if err != nil {
		return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
	}

	data, err = ioutil.ReadAll(bReader)
	if err != nil {
		return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
	}

	// Since the request can be coming from a binary body. Make an attempt to set the body length
	request.ContentLength = int64(len(data))
	request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))

	hashString = hashAndEncode(data)
	return
}

func (signer ociRequestSigner) computeSignature(request *http.Request) (signature string, err error) {
	signingString := signer.getSigningString(request)
	hasher := sha256.New()
	hasher.Write([]byte(signingString))
	hashed := hasher.Sum(nil)

	privateKey, err := signer.KeyProvider.PrivateRSAKey()
	if err != nil {
		return
	}

	var unencodedSig []byte
	unencodedSig, e := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
	if e != nil {
		err = fmt.Errorf("can not compute signature while signing the request %s: ", e.Error())
		return
	}

	signature = base64.StdEncoding.EncodeToString(unencodedSig)
	return
}

// Sign signs the http request, by inspecting the necessary headers. Once signed
// the request will have the proper 'Authorization' header set, otherwise
// and error is returned
func (signer ociRequestSigner) Sign(request *http.Request) (err error) {
	if signer.ShouldHashBody(request) {
		err = calculateHashOfBody(request)
		if err != nil {
			return
		}
	}

	var signature string
	if signature, err = signer.computeSignature(request); err != nil {
		return
	}

	signingHeaders := strings.Join(signer.getSigningHeaders(request), " ")

	var keyID string
	if keyID, err = signer.KeyProvider.KeyID(); err != nil {
		return
	}

	authValue := fmt.Sprintf("Signature version=\"%s\",headers=\"%s\",keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\"",
		signerVersion, signingHeaders, keyID, signature)

	request.Header.Set(requestHeaderAuthorization, authValue)

	return
}
