Files
QSfera/Server/vendor/github.com/Azure/go-ntlmssp/negotiator.go
T
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

332 lines
9.7 KiB
Go

// 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
}