package spnego

import (
	
	
	
	
	
	
	
	
	
	

	
	
	
	
	
	
	
	
	
	
)

// Client side functionality //

// Client will negotiate authentication with a server using SPNEGO.
type Client struct {
	*http.Client
	krb5Client *client.Client
	spn        string
	reqs       []*http.Request
}

type redirectErr struct {
	reqTarget *http.Request
}

func ( redirectErr) () string {
	return fmt.Sprintf("redirect to %v", .reqTarget.URL)
}

type teeReadCloser struct {
	io.Reader
	io.Closer
}

// NewClient returns a SPNEGO enabled HTTP client.
// Be careful when passing in the *http.Client if it is beginning reused in multiple calls to this function.
// Ensure reuse of the provided *http.Client is for the same user as a session cookie may have been added to
// http.Client's cookie jar.
// Incorrect reuse of the provided *http.Client could lead to access to the wrong user's session.
func ( *client.Client,  *http.Client,  string) *Client {
	if  == nil {
		 = &http.Client{}
	}
	// Add a cookie jar if there isn't one
	if .Jar == nil {
		.Jar, _ = cookiejar.New(nil)
	}
	// Add a CheckRedirect function that will execute any functional already defined and then error with a redirectErr
	 := .CheckRedirect
	.CheckRedirect = func( *http.Request,  []*http.Request) error {
		if  != nil {
			 := (, )
			if  != nil {
				return 
			}
		}
		return redirectErr{reqTarget: }
	}
	return &Client{
		Client:     ,
		krb5Client: ,
		spn:        ,
	}
}

// Do is the SPNEGO enabled HTTP client's equivalent of the http.Client's Do method.
func ( *Client) ( *http.Request) ( *http.Response,  error) {
	var  bytes.Buffer
	if .Body != nil {
		// Use a tee reader to capture any body sent in case we have to replay it again
		 := io.TeeReader(.Body, &)
		 := teeReadCloser{, .Body}
		.Body = 
	}
	,  = .Client.Do()
	if  != nil {
		if ,  := .(*url.Error);  {
			if ,  := .Err.(redirectErr);  {
				// Picked up a redirect
				.reqTarget.Header.Del(HTTPHeaderAuthRequest)
				.reqs = append(.reqs, .reqTarget)
				if len(.reqs) >= 10 {
					return , errors.New("stopped after 10 redirects")
				}
				if .Body != nil {
					// Refresh the body reader so the body can be sent again
					.reqTarget.Body = io.NopCloser(&)
				}
				return .(.reqTarget)
			}
		}
		return , 
	}
	if respUnauthorizedNegotiate() {
		 := SetSPNEGOHeader(.krb5Client, , .spn)
		if  != nil {
			return , 
		}
		if .Body != nil {
			// Refresh the body reader so the body can be sent again
			.Body = io.NopCloser(&)
		}
		io.Copy(io.Discard, .Body)
		.Body.Close()
		return .()
	}
	return , 
}

// Get is the SPNEGO enabled HTTP client's equivalent of the http.Client's Get method.
func ( *Client) ( string) ( *http.Response,  error) {
	,  := http.NewRequest("GET", , nil)
	if  != nil {
		return nil, 
	}
	return .Do()
}

// Post is the SPNEGO enabled HTTP client's equivalent of the http.Client's Post method.
func ( *Client) (,  string,  io.Reader) ( *http.Response,  error) {
	,  := http.NewRequest("POST", , )
	if  != nil {
		return nil, 
	}
	.Header.Set("Content-Type", )
	return .Do()
}

// PostForm is the SPNEGO enabled HTTP client's equivalent of the http.Client's PostForm method.
func ( *Client) ( string,  url.Values) ( *http.Response,  error) {
	return .Post(, "application/x-www-form-urlencoded", strings.NewReader(.Encode()))
}

// Head is the SPNEGO enabled HTTP client's equivalent of the http.Client's Head method.
func ( *Client) ( string) ( *http.Response,  error) {
	,  := http.NewRequest("HEAD", , nil)
	if  != nil {
		return nil, 
	}
	return .Do()
}

func respUnauthorizedNegotiate( *http.Response) bool {
	if .StatusCode == http.StatusUnauthorized {
		if .Header.Get(HTTPHeaderAuthResponse) == HTTPHeaderAuthResponseValueKey {
			return true
		}
	}
	return false
}

func setRequestSPN( *http.Request) (types.PrincipalName, error) {
	 := strings.TrimSuffix(.URL.Host, ".")
	// This if statement checks if the host includes a port number
	if strings.LastIndex(.URL.Host, ":") > strings.LastIndex(.URL.Host, "]") {
		// There is a port number in the URL
		, ,  := net.SplitHostPort()
		if  != nil {
			return types.PrincipalName{}, 
		}
		,  := net.LookupCNAME()
		if  != "" &&  == nil {
			// Underlyng canonical name should be used for SPN
			 = strings.ToLower()
		}
		 = strings.TrimSuffix(, ".")
		.Host = fmt.Sprintf("%s:%s", , )
		return types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "HTTP/"+), nil
	}
	,  := net.LookupCNAME()
	if  != "" &&  == nil {
		// Underlyng canonical name should be used for SPN
		 = strings.ToLower()
	}
	 = strings.TrimSuffix(, ".")
	.Host = 
	return types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "HTTP/"+), nil
}

// SetSPNEGOHeader gets the service ticket and sets it as the SPNEGO authorization header on HTTP request object.
// To auto generate the SPN from the request object pass a null string "".
func ( *client.Client,  *http.Request,  string) error {
	if  == "" {
		,  := setRequestSPN()
		if  != nil {
			return 
		}
		 = .PrincipalNameString()
	}
	.Log("using SPN %s", )
	 := SPNEGOClient(, )
	 := .AcquireCred()
	if  != nil {
		return fmt.Errorf("could not acquire client credential: %v", )
	}
	,  := .InitSecContext()
	if  != nil {
		return fmt.Errorf("could not initialize context: %v", )
	}
	,  := .Marshal()
	if  != nil {
		return krberror.Errorf(, krberror.EncodingError, "could not marshal SPNEGO")
	}
	 := "Negotiate " + base64.StdEncoding.EncodeToString()
	.Header.Set(HTTPHeaderAuthRequest, )
	return nil
}

// Service side functionality //

const (
	// spnegoNegTokenRespKRBAcceptCompleted - The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
	spnegoNegTokenRespKRBAcceptCompleted = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
	// spnegoNegTokenRespReject - The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
	spnegoNegTokenRespReject = "Negotiate oQcwBaADCgEC"
	// spnegoNegTokenRespIncompleteKRB5 - Response token specifying incomplete context and KRB5 as the supported mechtype.
	spnegoNegTokenRespIncompleteKRB5 = "Negotiate oRQwEqADCgEBoQsGCSqGSIb3EgECAg=="
	// sessionCredentials is the session value key holding the credentials jcmturner/goidentity/Identity object.
	sessionCredentials = "github.com/jcmturner/gokrb5/v8/sessionCredentials"
	// ctxCredentials is the SPNEGO context key holding the credentials jcmturner/goidentity/Identity object.
	ctxCredentials = "github.com/jcmturner/gokrb5/v8/ctxCredentials"
	// HTTPHeaderAuthRequest is the header that will hold authn/z information.
	HTTPHeaderAuthRequest = "Authorization"
	// HTTPHeaderAuthResponse is the header that will hold SPNEGO data from the server.
	HTTPHeaderAuthResponse = "WWW-Authenticate"
	// HTTPHeaderAuthResponseValueKey is the key in the auth header for SPNEGO.
	HTTPHeaderAuthResponseValueKey = "Negotiate"
	// UnauthorizedMsg is the message returned in the body when authentication fails.
	UnauthorizedMsg = "Unauthorised.\n"
)

// SPNEGOKRB5Authenticate is a Kerberos SPNEGO authentication HTTP handler wrapper.
func ( http.Handler,  *keytab.Keytab,  ...func(*service.Settings)) http.Handler {
	return http.HandlerFunc(func( http.ResponseWriter,  *http.Request) {
		// Set up the SPNEGO GSS-API mechanism
		var  *SPNEGO
		,  := types.GetHostAddress(.RemoteAddr)
		if  == nil {
			// put in this order so that if the user provides a ClientAddress it will override the one here.
			 := append([]func(*service.Settings){service.ClientAddress()}, ...)
			 = SPNEGOService(, ...)
		} else {
			 = SPNEGOService(, ...)
			.Log("%s - SPNEGO could not parse client address: %v", .RemoteAddr, )
		}

		// Check if there is a session manager and if there is an already established session for this client
		,  := getSessionCredentials(, )
		if  == nil && .Authenticated() {
			// There is an established session so bypass auth and serve
			.Log("%s - SPNEGO request served under session %s", .RemoteAddr, .SessionID())
			.ServeHTTP(, goidentity.AddToHTTPRequestContext(&, ))
			return
		}

		,  := getAuthorizationNegotiationHeaderAsSPNEGOToken(, , )
		if  == nil ||  != nil {
			// response to client and logging handled in function above so just return
			return
		}

		// Validate the context token
		, ,  := .AcceptSecContext()
		if .Code != gssapi.StatusComplete && .Code != gssapi.StatusContinueNeeded {
			spnegoResponseReject(, , "%s - SPNEGO validation error: %v", .RemoteAddr, )
			return
		}
		if .Code == gssapi.StatusContinueNeeded {
			spnegoNegotiateKRB5MechType(, , "%s - SPNEGO GSS-API continue needed", .RemoteAddr)
			return
		}

		if  {
			// Authentication successful; get user's credentials from the context
			 := .Value(ctxCredentials).(*credentials.Credentials)
			// Create a new session if a session manager has been configured
			 = newSession(, , , )
			if  != nil {
				return
			}
			spnegoResponseAcceptCompleted(, , "%s %s@%s - SPNEGO authentication succeeded", .RemoteAddr, .UserName(), .Domain())
			// Add the identity to the context and serve the inner/wrapped handler
			.ServeHTTP(, goidentity.AddToHTTPRequestContext(, ))
			return
		}
		// If we get to here we have not authenticationed so just reject
		spnegoResponseReject(, , "%s - SPNEGO Kerberos authentication failed", .RemoteAddr)
		return
	})
}

func getAuthorizationNegotiationHeaderAsSPNEGOToken( *SPNEGO,  *http.Request,  http.ResponseWriter) (*SPNEGOToken, error) {
	 := strings.SplitN(.Header.Get(HTTPHeaderAuthRequest), " ", 2)
	if len() != 2 || [0] != HTTPHeaderAuthResponseValueKey {
		// No Authorization header set so return 401 with WWW-Authenticate Negotiate header
		.Header().Set(HTTPHeaderAuthResponse, HTTPHeaderAuthResponseValueKey)
		http.Error(, UnauthorizedMsg, http.StatusUnauthorized)
		return nil, errors.New("client did not provide a negotiation authorization header")
	}

	// Decode the header into an SPNEGO context token
	,  := base64.StdEncoding.DecodeString([1])
	if  != nil {
		 = fmt.Errorf("error in base64 decoding negotiation header: %v", )
		spnegoNegotiateKRB5MechType(, , "%s - SPNEGO %v", .RemoteAddr, )
		return nil, 
	}
	var  SPNEGOToken
	 = .Unmarshal()
	if  != nil {
		// Check if this is a raw KRB5 context token - issue #347.
		var  KRB5Token
		if .Unmarshal() != nil {
			 = fmt.Errorf("error in unmarshaling SPNEGO token: %v", )
			spnegoNegotiateKRB5MechType(, , "%s - SPNEGO %v", .RemoteAddr, )
			return nil, 
		}
		// Wrap it into an SPNEGO context token
		.Init = true
		.NegTokenInit = NegTokenInit{
			MechTypes:      []asn1.ObjectIdentifier{.OID},
			MechTokenBytes: ,
		}
	}
	return &, nil
}

func getSessionCredentials( *SPNEGO,  *http.Request) (credentials.Credentials, error) {
	var  credentials.Credentials
	// Check if there is a session manager and if there is an already established session for this client
	if  := .serviceSettings.SessionManager();  != nil {
		,  := .Get(, sessionCredentials)
		if  != nil ||  == nil || len() < 1 {
			return , fmt.Errorf("%s - SPNEGO error getting session and credentials for request: %v", .RemoteAddr, )
		}
		 = .Unmarshal()
		if  != nil {
			return , fmt.Errorf("%s - SPNEGO credentials malformed in session: %v", .RemoteAddr, )
		}
		return , nil
	}
	return , errors.New("no session manager configured")
}

func newSession( *SPNEGO,  *http.Request,  http.ResponseWriter,  *credentials.Credentials) error {
	if  := .serviceSettings.SessionManager();  != nil {
		// create new session
		,  := .Marshal()
		if  != nil {
			spnegoInternalServerError(, , "SPNEGO could not marshal credentials to add to the session: %v", )
			return 
		}
		 = .New(, , sessionCredentials, )
		if  != nil {
			spnegoInternalServerError(, , "SPNEGO could not create new session: %v", )
			return 
		}
		.Log("%s %s@%s - SPNEGO new session (%s) created", .RemoteAddr, .UserName(), .Domain(), .SessionID())
	}
	return nil
}

// Log and respond to client for error conditions

func spnegoNegotiateKRB5MechType( *SPNEGO,  http.ResponseWriter,  string,  ...interface{}) {
	.Log(, ...)
	.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespIncompleteKRB5)
	http.Error(, UnauthorizedMsg, http.StatusUnauthorized)
}

func spnegoResponseReject( *SPNEGO,  http.ResponseWriter,  string,  ...interface{}) {
	.Log(, ...)
	.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespReject)
	http.Error(, UnauthorizedMsg, http.StatusUnauthorized)
}

func spnegoResponseAcceptCompleted( *SPNEGO,  http.ResponseWriter,  string,  ...interface{}) {
	.Log(, ...)
	.Header().Set(HTTPHeaderAuthResponse, spnegoNegTokenRespKRBAcceptCompleted)
}

func spnegoInternalServerError( *SPNEGO,  http.ResponseWriter,  string,  ...interface{}) {
	.Log(, ...)
	http.Error(, "Internal Server Error", http.StatusInternalServerError)
}