Skip to content

Commit 5271224

Browse files
committed
New: Fallback to libcurl/libssl on mono for https connections.
1 parent c4430ab commit 5271224

File tree

9 files changed

+348
-20
lines changed

9 files changed

+348
-20
lines changed

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[submodule "src/ExternalModules/CurlSharp"]
2+
path = src/ExternalModules/CurlSharp
3+
url = https://github.com/Sonarr/CurlSharp.git
4+
branch = master

build.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ Function PackageMono()
116116

117117
Write-Host "Adding NzbDrone.Core.dll.config (for dllmap)"
118118
Copy-Item "$sourceFolder\NzbDrone.Core\NzbDrone.Core.dll.config" $outputFolderMono
119+
120+
Write-Host "Adding CurlSharp.dll.config (for dllmap)"
121+
Copy-Item "$sourceFolder\NzbDrone.Common\CurlSharp.dll.config" $outputFolderMono
119122

120123
Write-Host Renaming NzbDrone.Console.exe to NzbDrone.exe
121124
Get-ChildItem $outputFolderMono -File -Filter "NzbDrone.exe*" -Recurse | foreach ($_) {remove-item $_.fullname}

src/ExternalModules/CurlSharp

Submodule CurlSharp added at cfdbbbd

src/NzbDrone.Common.Test/Http/HttpClientFixture.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ public void should_execute_typed_get()
4444
response.Resource.Url.Should().Be(request.Url.ToString());
4545
}
4646

47+
[Test]
48+
public void should_execute_simple_post()
49+
{
50+
var request = new HttpRequest("http://eu.httpbin.org/post");
51+
request.Body = "{ my: 1 }";
52+
53+
var response = Subject.Post<HttpBinResource>(request);
54+
55+
response.Resource.Data.Should().Be(request.Body);
56+
}
57+
4758
[TestCase("gzip")]
4859
public void should_execute_get_using_gzip(string compression)
4960
{
@@ -224,5 +235,6 @@ public class HttpBinResource
224235
public Dictionary<string, object> Headers { get; set; }
225236
public string Origin { get; set; }
226237
public string Url { get; set; }
238+
public string Data { get; set; }
227239
}
228240
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<!-- <dllmap os="osx" dll="libcurl.dll" target="libcurl.3.dylib"/> -->
4+
<dllmap os="linux" dll="libcurl.dll" target="libcurl.so.3" />
5+
<dllmap os="freebsd" dll="libcurl.dll" target="libcurl.so.3" />
6+
<dllmap os="solaris" dll="libcurl.dll" target="libcurl.so.3" />
7+
</configuration>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.IO.Compression;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Runtime.InteropServices;
8+
using System.Text;
9+
using CurlSharp;
10+
using NLog;
11+
using NzbDrone.Common.Extensions;
12+
using NzbDrone.Common.Instrumentation;
13+
14+
namespace NzbDrone.Common.Http
15+
{
16+
public class CurlHttpClient
17+
{
18+
private static Logger Logger = NzbDroneLogger.GetLogger(typeof(CurlHttpClient));
19+
20+
public CurlHttpClient()
21+
{
22+
if (!CheckAvailability())
23+
{
24+
throw new ApplicationException("Curl failed to initialize.");
25+
}
26+
}
27+
28+
public static bool CheckAvailability()
29+
{
30+
try
31+
{
32+
return CurlGlobalHandle.Instance.Initialize();
33+
}
34+
catch (Exception ex)
35+
{
36+
Logger.TraceException("Initializing curl failed", ex);
37+
return false;
38+
}
39+
}
40+
41+
public HttpResponse GetResponse(HttpRequest httpRequest, HttpWebRequest webRequest)
42+
{
43+
Stream responseStream = new MemoryStream();
44+
Stream headerStream = new MemoryStream();
45+
46+
var curlEasy = new CurlEasy();
47+
curlEasy.AutoReferer = false;
48+
curlEasy.WriteFunction = (b, s, n, o) =>
49+
{
50+
responseStream.Write(b, 0, s * n);
51+
return s * n;
52+
};
53+
curlEasy.HeaderFunction = (b, s, n, o) =>
54+
{
55+
headerStream.Write(b, 0, s * n);
56+
return s * n;
57+
};
58+
59+
curlEasy.UserAgent = webRequest.UserAgent;
60+
curlEasy.FollowLocation = webRequest.AllowAutoRedirect;
61+
curlEasy.HttpGet = webRequest.Method == "GET";
62+
curlEasy.Post = webRequest.Method == "POST";
63+
curlEasy.Put = webRequest.Method == "PUT";
64+
curlEasy.Url = webRequest.RequestUri.ToString();
65+
66+
if (webRequest.CookieContainer != null)
67+
{
68+
curlEasy.Cookie = webRequest.CookieContainer.GetCookieHeader(webRequest.RequestUri);
69+
}
70+
71+
if (!httpRequest.Body.IsNullOrWhiteSpace())
72+
{
73+
// TODO: This might not go well with encoding.
74+
curlEasy.PostFields = httpRequest.Body;
75+
curlEasy.PostFieldSize = httpRequest.Body.Length;
76+
}
77+
78+
curlEasy.HttpHeader = SerializeHeaders(webRequest);
79+
80+
var result = curlEasy.Perform();
81+
82+
if (result != CurlCode.Ok)
83+
{
84+
throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url));
85+
}
86+
87+
var webHeaderCollection = ProcessHeaderStream(webRequest, headerStream);
88+
var responseData = ProcessResponseStream(webRequest, responseStream, webHeaderCollection);
89+
90+
var httpHeader = new HttpHeader(webHeaderCollection);
91+
92+
return new HttpResponse(httpRequest, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
93+
}
94+
95+
private CurlSlist SerializeHeaders(HttpWebRequest webRequest)
96+
{
97+
if (webRequest.SendChunked)
98+
{
99+
throw new NotSupportedException("Chunked transfer is not supported");
100+
}
101+
102+
if (webRequest.ContentLength > 0)
103+
{
104+
webRequest.Headers.Add("Content-Length", webRequest.ContentLength.ToString());
105+
}
106+
107+
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.GZip))
108+
{
109+
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate))
110+
{
111+
webRequest.Headers.Add("Accept-Encoding", "gzip, deflate");
112+
}
113+
else
114+
{
115+
webRequest.Headers.Add("Accept-Encoding", "gzip");
116+
}
117+
}
118+
else
119+
{
120+
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate))
121+
{
122+
webRequest.Headers.Add("Accept-Encoding", "deflate");
123+
}
124+
}
125+
126+
127+
var curlHeaders = new CurlSlist();
128+
for (int i = 0; i < webRequest.Headers.Count; i++)
129+
{
130+
curlHeaders.Append(webRequest.Headers.GetKey(i) + ": " + webRequest.Headers.Get(i));
131+
}
132+
133+
curlHeaders.Append("Content-Type: " + webRequest.ContentType ?? string.Empty);
134+
135+
return curlHeaders;
136+
}
137+
138+
private WebHeaderCollection ProcessHeaderStream(HttpWebRequest webRequest, Stream headerStream)
139+
{
140+
headerStream.Position = 0;
141+
var headerData = headerStream.ToBytes();
142+
var headerString = Encoding.ASCII.GetString(headerData);
143+
144+
var webHeaderCollection = new WebHeaderCollection();
145+
146+
foreach (var header in headerString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1))
147+
{
148+
webHeaderCollection.Add(header);
149+
}
150+
151+
var setCookie = webHeaderCollection.Get("Set-Cookie");
152+
if (setCookie != null && setCookie.Length > 0 && webRequest.CookieContainer != null)
153+
{
154+
webRequest.CookieContainer.SetCookies(webRequest.RequestUri, setCookie);
155+
}
156+
157+
return webHeaderCollection;
158+
}
159+
160+
private byte[] ProcessResponseStream(HttpWebRequest webRequest, Stream responseStream, WebHeaderCollection webHeaderCollection)
161+
{
162+
responseStream.Position = 0;
163+
164+
if (responseStream.Length != 0 && webRequest.AutomaticDecompression != DecompressionMethods.None)
165+
{
166+
var encoding = webHeaderCollection["Content-Encoding"];
167+
if (encoding != null)
168+
{
169+
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.GZip) && encoding.IndexOf("gzip") != -1)
170+
{
171+
responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
172+
173+
webHeaderCollection.Remove("Content-Encoding");
174+
}
175+
else if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate) && encoding.IndexOf("deflate") != -1)
176+
{
177+
responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);
178+
179+
webHeaderCollection.Remove("Content-Encoding");
180+
}
181+
}
182+
}
183+
184+
return responseStream.ToBytes();
185+
186+
}
187+
}
188+
189+
internal sealed class CurlGlobalHandle : SafeHandle
190+
{
191+
public static readonly CurlGlobalHandle Instance = new CurlGlobalHandle();
192+
193+
private bool _initialized;
194+
private bool _available;
195+
196+
protected override void Dispose(bool disposing)
197+
{
198+
base.Dispose(disposing);
199+
}
200+
201+
private CurlGlobalHandle()
202+
: base(IntPtr.Zero, true)
203+
{
204+
205+
}
206+
207+
public bool Initialize()
208+
{
209+
if (_initialized)
210+
return _available;
211+
212+
_initialized = true;
213+
_available = Curl.GlobalInit(CurlInitFlag.All) == CurlCode.Ok;
214+
215+
return _available;
216+
}
217+
218+
protected override bool ReleaseHandle()
219+
{
220+
if (_initialized && _available)
221+
{
222+
Curl.GlobalCleanup();
223+
_available = false;
224+
}
225+
return true;
226+
}
227+
228+
public override bool IsInvalid
229+
{
230+
get { return !_initialized || !_available; }
231+
}
232+
}
233+
}

0 commit comments

Comments
 (0)