Skip to content

Commit b841181

Browse files
committed
Add support for httpCookieFile
1 parent 61cd8e3 commit b841181

File tree

5 files changed

+141
-0
lines changed

5 files changed

+141
-0
lines changed

src/shared/Core.Tests/HttpClientFactoryTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,36 @@ public void HttpClientFactory_GetClient_ChecksCertBundleOnlyIfEnabled(string cus
311311
fileSystemMock.Verify(fs => fs.FileExists(It.IsAny<string>()), expectBundleChecked ? Times.Once : Times.Never);
312312
}
313313

314+
[Theory]
315+
[InlineData(null, false, null)]
316+
[InlineData("~/.git-cookie", true, "# Netscape HTTP Cookie File\n" +
317+
"# https://curl.haxx.se/rfc/cookie_spec.html\n" +
318+
"# This is a generated file! Do not edit.\n" +
319+
"\n" +
320+
".example.com\tTRUE\t/\tTRUE\t0\tcookie1\tvalue1\n" +
321+
".example.com\tTRUE\t/\tTRUE\t0\tcookie2\tvalue2\n" +
322+
"#HttpOnly_.example.com\tTRUE\t/\tTRUE\t0\tcookie3\tvalue3\n")]
323+
public void HttpClientFactory_GetClient_SetCookieOnlyIfEnabled(string cookieFilePath, bool expectCookieChecked, string cookieFileContent)
324+
{
325+
var fileSystemMock = new Mock<IFileSystem>();
326+
fileSystemMock.Setup(fs => fs.FileExists(It.IsAny<string>())).Returns(true);
327+
if (!string.IsNullOrWhiteSpace(cookieFileContent))
328+
{
329+
fileSystemMock.Setup(fs => fs.ReadAllText(cookieFilePath)).Returns(cookieFileContent);
330+
}
331+
332+
var settings = new TestSettings()
333+
{
334+
CustomCookieFilePath = cookieFilePath
335+
};
336+
337+
var factory = new HttpClientFactory(fileSystemMock.Object, Mock.Of<ITrace>(), Mock.Of<ITrace2>(), settings, new TestStandardStreams());
338+
339+
HttpClient client = factory.CreateClient();
340+
341+
fileSystemMock.Verify(fs => fs.FileExists(It.IsAny<string>()), expectCookieChecked ? Times.AtLeastOnce : Times.Never);
342+
}
343+
314344
private static void AssertDefaultCredentials(ICredentials credentials)
315345
{
316346
var netCred = (NetworkCredential) credentials;

src/shared/Core/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public static class Http
171171
public const string SslVerify = "sslVerify";
172172
public const string SslCaInfo = "sslCAInfo";
173173
public const string SslAutoClientCert = "sslAutoClientCert";
174+
public const string CookieFile = "cookieFile";
174175
}
175176

176177
public static class Remote

src/shared/Core/HttpClientFactory.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,40 @@ public HttpClient CreateClient()
197197
#endif
198198
}
199199

200+
_trace.WriteLine($"Custom cookie file has been enabled with {_settings.CustomCookieFilePath}");
201+
// If IsUsingCookieEnabled is enabled, set Cookie header from cookie file, which is written by libcurl
202+
if (!string.IsNullOrWhiteSpace(_settings.CustomCookieFilePath))
203+
{
204+
// get the filename from gitconfig
205+
string cookieFilePath = _settings.CustomCookieFilePath;
206+
_trace.WriteLine($"Custom cookie file has been enabled with {cookieFilePath}");
207+
208+
// Throw exception if cookie file not found
209+
if (!_fileSystem.FileExists(cookieFilePath) || string.IsNullOrEmpty(_fileSystem.ReadAllText(cookieFilePath)))
210+
{
211+
var format = "Custom cookie file not found at path: {0}";
212+
var message = string.Format(format, cookieFilePath);
213+
throw new Trace2FileNotFoundException(_trace2, message, format, cookieFilePath);
214+
}
215+
216+
// get cookie from cookie file
217+
var cookieParser = new CurlCookieParser(_trace);
218+
var cookies = cookieParser.Parse(_fileSystem.ReadAllText(cookieFilePath));
219+
220+
// Set the cookie
221+
var cookieContainer = new CookieContainer();
222+
foreach (var cookie in cookies)
223+
{
224+
var schema = cookie.Secure ? "https" : "http";
225+
var uri = new UriBuilder(schema, cookie.Domain).Uri;
226+
cookieContainer.Add(uri, new Cookie(cookie.Name, cookie.Value));
227+
}
228+
handler.CookieContainer = cookieContainer;
229+
230+
_trace.WriteLine("Configured to automatically send cookie header.");
231+
232+
}
233+
200234
var client = new HttpClient(handler);
201235

202236
// Add default headers
@@ -301,5 +335,68 @@ public bool TryCreateProxy(out IWebProxy proxy)
301335
proxy = null;
302336
return false;
303337
}
338+
339+
public class CurlCookieParser
340+
{
341+
private readonly ITrace _trace;
342+
343+
public CurlCookieParser(ITrace trace)
344+
{
345+
_trace = trace;
346+
}
347+
348+
public IList<Cookie> Parse(string content)
349+
{
350+
var cookies = new List<Cookie>();
351+
352+
// Parse the cookie file content
353+
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
354+
for (int i = 3; i < lines.Length; i++)
355+
{
356+
var parts = lines[i].Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
357+
if (parts.Length >= 7)
358+
{
359+
var domain = parts[0];
360+
domain = domain.StartsWith("#HttpOnly_") ? parts[0].Substring("#HttpOnly_".Length) : parts[0];
361+
domain = domain.TrimStart('.');
362+
var includeSubdomains = parts[1] == "TRUE";
363+
var path = parts[2];
364+
var secureOnly = parts[3] == "TRUE";
365+
var expires = parts[4];
366+
var name = parts[5];
367+
var value = parts[6];
368+
369+
cookies.Add(new Cookie(name, value, path, domain)
370+
{
371+
Expires = ParseExpires(expires),
372+
Secure = secureOnly,
373+
HttpOnly = true
374+
});
375+
}
376+
else
377+
{
378+
_trace.WriteLine($"Invalid cookie line: {lines[i]}");
379+
}
380+
}
381+
382+
return cookies;
383+
}
384+
385+
private static DateTime ParseExpires(string expires)
386+
{
387+
if (expires.Equals("0", StringComparison.OrdinalIgnoreCase))
388+
{
389+
return DateTime.MinValue;
390+
}
391+
else if (DateTime.TryParse(expires, out var result))
392+
{
393+
return result;
394+
}
395+
else
396+
{
397+
return DateTime.MaxValue;
398+
}
399+
}
400+
}
304401
}
305402
}

src/shared/Core/Settings.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ public interface ISettings : IDisposable
154154
/// <remarks>The default value is null if unset.</remarks>
155155
string CustomCertificateBundlePath { get; }
156156

157+
/// <summary>
158+
// Optional path to a file containing one or more cookies.
159+
/// </summary>
160+
/// <remarks>The default value is null if unset.</remarks>
161+
string CustomCookieFilePath { get; }
162+
157163
/// <summary>
158164
/// The SSL/TLS backend.
159165
/// </summary>
@@ -625,6 +631,9 @@ public bool IsCertificateVerificationEnabled
625631
public string CustomCertificateBundlePath =>
626632
TryGetPathSetting(KnownEnvars.GitSslCaInfo, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslCaInfo, out string value) ? value : null;
627633

634+
public string CustomCookieFilePath =>
635+
TryGetPathSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.CookieFile, out string value) ? value : null;
636+
628637
public TlsBackend TlsBackend =>
629638
TryGetSetting(null, KnownGitCfg.Http.SectionName, KnownGitCfg.Http.SslBackend, out string config)
630639
? (Enum.TryParse(config, true, out TlsBackend backend) ? backend : GitCredentialManager.TlsBackend.Other)

src/shared/TestInfrastructure/Objects/TestSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public class TestSettings : ISettings
4343

4444
public string CustomCertificateBundlePath { get; set; }
4545

46+
public string CustomCookieFilePath { get; set; }
47+
4648
public TlsBackend TlsBackend { get; set; }
4749

4850
public bool UseCustomCertificateBundleWithSchannel { get; set; }
@@ -171,6 +173,8 @@ ProxyConfiguration ISettings.GetProxyConfiguration()
171173

172174
string ISettings.CustomCertificateBundlePath => CustomCertificateBundlePath;
173175

176+
string ISettings.CustomCookieFilePath => CustomCookieFilePath;
177+
174178
TlsBackend ISettings.TlsBackend => TlsBackend;
175179

176180
bool ISettings.UseCustomCertificateBundleWithSchannel => UseCustomCertificateBundleWithSchannel;

0 commit comments

Comments
 (0)