Skip to content

Resend POST requests for Kerberos in WinHTTP#2529

Closed
chescock wants to merge 3 commits intolibgit2:masterfrom
chescock:resend-negotiate
Closed

Resend POST requests for Kerberos in WinHTTP#2529
chescock wants to merge 3 commits intolibgit2:masterfrom
chescock:resend-negotiate

Conversation

@chescock
Copy link
Contributor

I'm trying to use LibGit2Sharp to connect to an Apache server that uses mod_auth_kerb for authentication, and I'm getting the error "Failed to receive response: The request must be resent". It seems that the way WinHttp handles Kerberos authentication involves sending the request twice, which works fine for GET requests that have no body but fails for POST requests because it doesn’t store a copy of the request body to resend.

The workaround I’m using here is to send an initial empty POST when using Negotiate, which should trigger the 401 early and allow the actual request to succeed without having to write the body twice. I think this may be unnecessary in some cases, but I don’t see a way to tell whether it’s required and the extra empty POST appears to be harmless if it succeeds.

I’m fairly new to both Kerberos and WinHttp, so I apologize in advance if I’ve done something completely crazy and I welcome suggestions for other solutions to my problem.

…ROR_WINHTTP_RESEND_REQUEST.

We first send a zero-length request to get a 401 without writing the body twice.
@vmg
Copy link
Member

vmg commented Aug 19, 2014

This is terrifying. @ethomson?

@ethomson
Copy link
Member

Hmm, that's very interesting. So yes, you're very much right that we should be re-posting the form data if we received a 401 on the original POST. This was sort of sneaky because we assume that we would never be in a situation where this mattered: the assumption is that a GET of /info/refs (which is the first step in any push/fetch) would authenticate you and then you could prompty POST to the upload pack / receive pack handlers on the same session without requiring re-authentication.

If I had to guess: Apache is tearing down the connection between the GET and the POST for some reason. I don't really know why it would do that but it's not really wrong to do that, per se. Just annoying (and costly for any session-based authentication mechanism like NTLM or Negotiate).

I'd love to see a fiddler capture, if you don't mind. It's possible that we're not sending keep-alive headers correctly for some reason, or that we're tearing down the connection... (Less likely is that it's not treating a kept-alive session as authenticated, which would be a bug in their implementation.)

In any case, this is a very valid bug that I regret. I think that the really proper support would be to send a POST with Expect: continue and then do our authentication dance until we get a 100 and then post the data.

This should be the standards-compliant way to accomplish this same thing you're doing, but I'm not sure how to coerce winhttp into doing this offhand. (I'm actually pretty shocked that winhttp didn't do this for us.)

@chescock
Copy link
Contributor Author

I took a Fiddler trace and stripped out the hostnames and tickets and things. Should I just paste it here as a comment?

I get Connection: Keep-Alive on every request and response, so I don’t think Apache is closing the connection. I did have that misconfigured on the server at one point, which actually caused the initial GET to go into an infinite loop because it wouldn’t send the credentials on replay.

It does seem like mod_auth_kerb treats the request as authenticated instead of the connection. I haven’t found any hard documentation, but I’ve found a few references that make me think that's expected:

Generally IIS only needs 1 Spnego token per connection, while mod_auth_kerb in apache wants one per request

at http://kerberos.996246.n3.nabble.com/resend-spnego-token-td21160.html, and

It's clear that the expected authentication persistence behavior hasn't been documented in RFC 4559. Microsoft has implemented per-connection authentication in their products, whereas Tomcat and mod_auth_kerb have per-request authentication.

at http://code.google.com/p/serf/issues/detail?id=89.

Web browsers seem to send the Authorization header on subsequent requests to avoid the round-trip, but I couldn’t find any way to make WinHttp do that.

I can start looking into the Expect: continue approach you suggest, but I’m new to both WinHttp and Kerberos and very very rusty at C, so I’d appreciate any help you can provide.

Thanks!

@vmg
Copy link
Member

vmg commented Aug 19, 2014

Should I just paste it here as a comment?

That works. You can also create a Gist if you feel the paste is too large. https://gist.github.com/

@chescock
Copy link
Contributor Author

GET https://SERVER/repository.git/info/refs?service=git-upload-pack HTTP/1.1
Connection: Keep-Alive
Pragma: no-cache
Accept: */*
User-Agent: git/1.0 (libgit2 0.20.0)
Host: SERVER


HTTP/1.1 401 Authorization Required
Date: Tue, 19 Aug 2014 19:38:09 GMT
WWW-Authenticate: Negotiate
Content-Length: 492
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Proxy-Support: Session-Based-Authentication

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><not interesting></html>


------------------------------------------------------------------

GET https://SERVER/repository.git/info/refs?service=git-upload-pack HTTP/1.1
Connection: Keep-Alive
Pragma: no-cache
Accept: */*
User-Agent: git/1.0 (libgit2 0.20.0)
Host: SERVER


HTTP/1.1 401 Authorization Required
Date: Tue, 19 Aug 2014 19:38:09 GMT
WWW-Authenticate: Negotiate
Content-Length: 492
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Proxy-Support: Session-Based-Authentication

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><not interesting></html>


------------------------------------------------------------------

GET https://SERVER/repository.git/info/refs?service=git-upload-pack HTTP/1.1
Connection: Keep-Alive
Pragma: no-cache
Accept: */*
User-Agent: git/1.0 (libgit2 0.20.0)
Host: SERVER
Authorization: Negotiate <redacted>


HTTP/1.1 200 OK
Date: Tue, 19 Aug 2014 19:38:09 GMT
WWW-Authenticate: Negotiate <redacted>
X-AREQUESTID: 938x32847x0
Set-Cookie: JSESSIONID=3BD9B04FA16048A35FF67D23D03C2B9B; Path=/; Secure; HttpOnly
X-AUSERNAME: chescock
X-AUSERID: 7143
X-ASESSIONID: sixdua
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Expires: Fri, 01 Jan 1980 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache, max-age=0, must-revalidate
Content-Type: application/x-git-upload-pack-advertisement
Content-Length: 759
Keep-Alive: timeout=15, max=99
Connection: Keep-Alive

<redacted>
0000


------------------------------------------------------------------

POST https://SERVER/repository.git/git-upload-pack HTTP/1.1
Connection: Keep-Alive
Pragma: no-cache
Content-Type: application/x-git-upload-pack-request
Accept: application/x-git-upload-pack-result
User-Agent: git/1.0 (libgit2 0.20.0)
Content-Length: 429
Host: SERVER
Cookie: JSESSIONID=3BD9B04FA16048A35FF67D23D03C2B9B

<redacted>00000009done

HTTP/1.1 401 Authorization Required
Date: Tue, 19 Aug 2014 19:38:10 GMT
WWW-Authenticate: Negotiate
Content-Length: 492
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
Proxy-Support: Session-Based-Authentication

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><not interesting></html>



@peff
Copy link
Member

peff commented Aug 20, 2014

We deal with this in git-core, too. Ideally you would send an Expect line, but so many servers are flaky with them that we do not by default. Instead, we handle re-POSTing ourselves. When the response is too large to fit in our buffer (1MB by default), we POST a "probe" packet consisting of a single pkt-line flush, to cheaply deal with any authentication or rejection issues. I don't see any similar code in libgit2 (but I might just be missing it; your http implementation is quite different from ours).

The relevant code in git-core is in post_rpc.

@peff
Copy link
Member

peff commented Aug 20, 2014

Also see git/git@c80d96c for Expect handling.

@ethomson
Copy link
Member

Boom, thanks @peff.

@chescock Let's do what git.git is doing then, which is just a refinement of your existing implementation, I think. I hope I didn't lead you too far astray with the expect/continue suggestion.

Happy to provide any help you need.

@chescock
Copy link
Contributor Author

If I'm reading the code correctly, the only change I need to make to match the behavior of probe_rpc is to replace the empty HTTP request body with “0000”, which I assume makes it a valid empty git request. Is that right?

post_rpc calls probe_rpc in a loop until it succeeds, but if I try something equivalent with WinHttp it forgets the authentication after the successful empty request and the actual request fails again.

The other difference is that I'm currently doing this for all requests instead of just large ones. I think WinHttp will resend the request body automatically if we pass the data in WinHttpSendRequest instead of calling WinHttpWriteData afterwards. That could work for small requests that use winhttp_stream_write_single, and would remove the need for the if statement I added to winhttp_action. The buffer needs to be available until WinHttpReceiveResponse is called and the one there now isn’t, so I would need to make a copy. Should I make that change as well?

…ead of calling WinHttpWriteData so that WinHttp can resend the request if needed.
@chescock
Copy link
Contributor Author

@ethomson Does this change look reasonable with the updates? Is there anything else you think I should fix?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line looks off. Are you trying to do HTTP chunking by hand? Isn't WinHTTP supposed to do that? Do we even want chunking for this? We should be able to simply tell WinHTTP to send the flush packet since we know what length we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that code is doing HTTP chunking by hand. There is similar code already in write_chunk and winhttp_stream_read. It seems that the way you do chunking in WinHTTP is by adding the header manually and then writing the chunk boundaries manually.

The documentation for WinHttpSendRequest says that “dwTotalLength must not change between calls to WinHttpSendRequest for the same request”, and we need to use the same request handle in order to preserve the authentication information. When using chunked encoding, dwTotalLength is set to WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH and so we’re able to send requests of different lengths, but I don’t see a way to use normal encoding if we’re trying to send an empty request followed by a real request on the same request handle.

@ethomson
Copy link
Member

Sorry! I have been slammed with other things this week.

So the WinHTTP transport layer is not the only place where this code makes sense. Now that we have stateful authentication in the regular HTTP transport, we'll want to do the same sort of dance there.

I wonder if it makes sense (or is possible) to move this up a level so that both transport layers can benefit from this logic for dealing with deficient servers.

@carlosmn
Copy link
Member

It should definitely be possible to do this in the smart protocol layer. We already need to perform special handling if the transport is stateless/rpc in some places, so we should be able to send a flush packet before we start the push proper to make sure the authentication happens early.

@chescock
Copy link
Contributor Author

Sorry for the delay in responding! I was out on vacation last week and I’m still catching up.

I’ll start looking into how I could implement this in the higher layer. I’m a little more nervous about making a larger change, though, since I’m new to the codebase and extremely rusty at C.

I’m not entirely convinced that this is actually a general concern as opposed to just handling quirks in the WinHTTP API. In the upload-pack case we can just give WinHTTP the entire request body and it will resend it as needed, but other transports might need a flush packet there as well. Most web browsers appear to resend the Authorization header on subsequent requests so that they don’t get 401 responses every time, and a transport that did that wouldn’t need to send the flush packet first.

@rnowosielski
Copy link
Contributor

Seems this effort got stuck a little bit? It would be great to have this solved in the next version that will be published, as the Kerberos authentication simply doesn't work with Apache and mod_auth_kerb.

I assume there mp workaround that can be applied on Apache configuration to get this working?

@ethomson
Copy link
Member

Closed with #5286

@ethomson ethomson closed this Apr 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants