Initial QSfera import
This commit is contained in:
+331
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// negotiatorBody wraps an io.ReadSeeker to allow waiting for its closure
|
||||
// before rewinding and reusing it.
|
||||
type negotiatorBody struct {
|
||||
body io.ReadSeeker
|
||||
closed chan struct{}
|
||||
startPos int64
|
||||
}
|
||||
|
||||
// newNegotiatorBody creates a negotiatorBody from the provided io.Reader.
|
||||
// If the body is nil, it returns nil.
|
||||
// If the body is already an io.ReadSeeker, it uses it directly.
|
||||
// Otherwise, it reads the entire body into memory to allow rewinding.
|
||||
func newNegotiatorBody(body io.Reader) (*negotiatorBody, error) {
|
||||
if body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// Check if body is already seekable to avoid buffering large bodies
|
||||
if seeker, ok := body.(io.ReadSeeker); ok {
|
||||
// Remember the current position
|
||||
startPos, err := seeker.Seek(0, io.SeekCurrent)
|
||||
if err == nil {
|
||||
// Seeking succeeded, use the seekable body directly
|
||||
return &negotiatorBody{
|
||||
body: seeker,
|
||||
closed: make(chan struct{}, 1),
|
||||
startPos: startPos,
|
||||
}, nil
|
||||
}
|
||||
// Seeking failed (e.g., pipes), fallback to buffering
|
||||
}
|
||||
// For non-seekable bodies, buffer in memory as required
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &negotiatorBody{
|
||||
body: bytes.NewReader(data),
|
||||
closed: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *negotiatorBody) Read(p []byte) (n int, err error) {
|
||||
if b == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return b.body.Read(p)
|
||||
}
|
||||
|
||||
// Close signals that the body is no longer needed for the current request.
|
||||
// It allows the negotiator to rewind the body for potential reuse.
|
||||
// The underlying body is not closed here; use close() for that.
|
||||
func (b *negotiatorBody) Close() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case b.closed <- struct{}{}:
|
||||
default:
|
||||
// Already signaled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// close closes the underlying body if it implements io.Closer.
|
||||
func (b *negotiatorBody) close() {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if closer, ok := b.body.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// rewind rewinds the body to the start position for reuse.
|
||||
func (b *negotiatorBody) rewind() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
// Wait for the body to be closed before rewinding
|
||||
<-b.closed
|
||||
_, err := b.body.Seek(b.startPos, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDomain extracts the user domain from the username if present.
|
||||
//
|
||||
// Deprecated: Pass the username directly to [ProcessChallenge], it will handle domain extraction.
|
||||
// Don't pass the resulting domain to [NewNegotiateMessage], that function expects the client
|
||||
// machine domain, not the user domain.
|
||||
func GetDomain(username string) (user string, domain string, domainNeeded bool) {
|
||||
if strings.Contains(username, "\\") {
|
||||
ucomponents := strings.SplitN(username, "\\", 2)
|
||||
domain = ucomponents[0]
|
||||
user = ucomponents[1]
|
||||
domainNeeded = true
|
||||
} else if strings.Contains(username, "@") {
|
||||
user = username
|
||||
domainNeeded = false
|
||||
} else {
|
||||
user = username
|
||||
domainNeeded = true
|
||||
}
|
||||
return user, domain, domainNeeded
|
||||
}
|
||||
|
||||
// Negotiator is a [net/http.RoundTripper] decorator that automatically
|
||||
// converts basic authentication to NTLM/Negotiate authentication when appropriate.
|
||||
//
|
||||
// The credentials must be set using [net/http.Request.SetBasicAuth] on a per-request basis.
|
||||
//
|
||||
// By default, no credentials will be sent to the server unless it requests
|
||||
// Basic authentication and [Negotiator.AllowBasicAuth] is set to true.
|
||||
type Negotiator struct {
|
||||
// RoundTripper is the underlying round tripper to use.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
http.RoundTripper
|
||||
|
||||
// AllowBasicAuth controls whether to send Basic authentication credentials
|
||||
// if the server requests it.
|
||||
//
|
||||
// If false (default), Basic authentication requests are ignored
|
||||
// and only NTLM/Negotiate authentication is performed.
|
||||
// If true, Basic authentication requests are honored.
|
||||
//
|
||||
// Only set this to true if you trust the server you are connecting to.
|
||||
// Basic authentication sends the credentials in clear text and may be
|
||||
// vulnerable to man-in-the-middle attacks and compromised servers.
|
||||
AllowBasicAuth bool
|
||||
|
||||
// WorkstationDomain is the domain of the client machine.
|
||||
// It is normally not needed to set this field.
|
||||
// It is passed to the negotiate message.
|
||||
WorkstationDomain string
|
||||
|
||||
// WorkstationName is the workstation name of the client machine.
|
||||
// It is passed to the negotiate and authenticate messages.
|
||||
// Useful for auditing purposes on the server side.
|
||||
WorkstationName string
|
||||
}
|
||||
|
||||
// RoundTrip sends the request to the server, handling any authentication
|
||||
// re-sends as needed.
|
||||
func (l Negotiator) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Use default round tripper if not provided
|
||||
rt := l.RoundTripper
|
||||
if rt == nil {
|
||||
rt = http.DefaultTransport
|
||||
}
|
||||
|
||||
// If it is not basic auth, just round trip the request as usual
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return rt.RoundTrip(req)
|
||||
}
|
||||
id := identity{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
req = req.Clone(req.Context()) // Clone the request to avoid modifying the original
|
||||
|
||||
// We need to buffer or seek the request body to handle authentication challenges
|
||||
// that require resending the body multiple times during the NTLM handshake.
|
||||
body, err := newNegotiatorBody(req.Body)
|
||||
if err != nil {
|
||||
if req.Body != nil {
|
||||
_ = req.Body.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer body.close()
|
||||
|
||||
// First try anonymous, in case the server still finds us authenticated from previous traffic
|
||||
req.Body = body
|
||||
req.Header.Del("Authorization")
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// No authentication required, return the response as is
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Note that from here on, the response returned in case of error or unsuccessful
|
||||
// negotiation is the one we just got from the server. This is to allow the caller
|
||||
// to do its own handling in case we can't do it in this roundtrip.
|
||||
originalResp := resp
|
||||
|
||||
resauth := newAuthHeader(resp.Header)
|
||||
if l.AllowBasicAuth && resauth.isBasic() {
|
||||
// Basic auth requested instead of NTLM/Negotiate.
|
||||
//
|
||||
// Rewind the body, we will resend it.
|
||||
if body.rewind() != nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
req.SetBasicAuth(id.username, id.password)
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// Basic auth succeeded, return the new response
|
||||
drainResponse(originalResp)
|
||||
return resp, nil
|
||||
}
|
||||
resauth = newAuthHeader(resp.Header)
|
||||
if !resauth.isNTLM() {
|
||||
// No NTLM/Negotiate requested, return the response as is
|
||||
return resp, nil
|
||||
}
|
||||
// Server upgraded from Basic to NTLM/Negotiate (rare but possible)
|
||||
drainResponse(resp)
|
||||
// After Basic-to-NTLM upgrade, update originalResp to the NTLM-triggering response
|
||||
originalResp = resp
|
||||
} else if !resauth.isNTLM() {
|
||||
// No NTLM/Negotiate requested, return the response as is
|
||||
return originalResp, nil
|
||||
}
|
||||
|
||||
// Server requested Negotiate/NTLM, start handshake
|
||||
|
||||
// First step: send negotiate message
|
||||
resp = clientHandshake(rt, req, resauth.schema, l.WorkstationDomain, l.WorkstationName)
|
||||
if resp == nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// We are expecting a 401 with challenge, but the server responded differently,
|
||||
// maybe it even accepted our negotiate message without further challenge, which is
|
||||
// valid per the spec (RFC 4559 Section 5).
|
||||
// Return the response as is, negotiation is over.
|
||||
drainResponse(originalResp)
|
||||
return resp, nil
|
||||
}
|
||||
resauth = newAuthHeader(resp.Header)
|
||||
drainResponse(resp)
|
||||
|
||||
// Second step: process challenge and resend the original body with the authenticate message
|
||||
resp = completeHandshake(rt, resauth, req, id, l.WorkstationName)
|
||||
if resp == nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
// We could return the original response in case of 401 again, but at this point
|
||||
// it's better to return the latest response from the server, as it might be the case
|
||||
// that we are really not authorized.
|
||||
drainResponse(originalResp) // Done with the original response
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type identity struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func drainResponse(res *http.Response) {
|
||||
// Drain body and close it to allow reusing the connection
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
|
||||
func rewindBody(req *http.Request) error {
|
||||
if req.Body == nil {
|
||||
return nil
|
||||
}
|
||||
if nb, ok := req.Body.(*negotiatorBody); ok {
|
||||
return nb.rewind()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientHandshake(rt http.RoundTripper, req *http.Request, schema string, domain, workstation string) *http.Response {
|
||||
if rewindBody(req) != nil {
|
||||
return nil
|
||||
}
|
||||
auth, err := NewNegotiateMessage(domain, workstation)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("Authorization", schema+" "+base64.StdEncoding.EncodeToString(auth))
|
||||
res, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func completeHandshake(rt http.RoundTripper, resauth authheader, req *http.Request, id identity, workstation string) *http.Response {
|
||||
if rewindBody(req) != nil {
|
||||
return nil
|
||||
}
|
||||
challenge, err := resauth.token()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !resauth.isNTLM() || len(challenge) == 0 {
|
||||
// The only expected schema here is NTLM/Negotiate with a challenge token,
|
||||
// otherwise the negotiation is over.
|
||||
return nil
|
||||
}
|
||||
var opts *AuthenticateMessageOptions
|
||||
if workstation != "" {
|
||||
opts = &AuthenticateMessageOptions{
|
||||
WorkstationName: workstation,
|
||||
}
|
||||
}
|
||||
auth, err := NewAuthenticateMessage(challenge, id.username, id.password, opts)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("Authorization", resauth.schema+" "+base64.StdEncoding.EncodeToString(auth))
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user