Skip to content
Open
6 changes: 4 additions & 2 deletions FlareSolverrSharp.sln
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30709.132
# Visual Studio Version 17
VisualStudioVersion = 17.11.35312.102
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlareSolverrSharp", "src\FlareSolverrSharp\FlareSolverrSharp.csproj", "{E541E27A-8D55-4E2F-AC7D-DCA0DCDAC220}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlareSolverrSharp.Sample", "sample\FlareSolverrSharp.Sample\FlareSolverrSharp.Sample.csproj", "{F44FEFA6-B85B-4C05-AD34-836DF6BF63B9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlareSolverrSharp.Tests", "test\FlareSolverrSharp.Tests\FlareSolverrSharp.Tests.csproj", "{89A9D8CB-01BA-43CA-83AE-2D760088154C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D0184B45-49CD-4C2F-B956-3D2253321FF2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
112 changes: 58 additions & 54 deletions sample/FlareSolverrSharp.Sample/ClearanceHandlerSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,73 @@
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Flurl.Http;

namespace FlareSolverrSharp.Sample
namespace FlareSolverrSharp.Sample;

public static class ClearanceHandlerSample
{
public static class ClearanceHandlerSample
{

public static string FlareSolverrUrl = "http://localhost:8191/";
public static string ProtectedUrl = "https://badasstorrents.com/torrents/search/720p/date/desc";
public static string FlareSolverrUrl = "http://localhost:8191/";
public static string ProtectedUrl = "http://iqdb.org";

public static async Task SampleGet()
{
var handler = new ClearanceHandler(FlareSolverrUrl)
{
MaxTimeout = 60_000
};

var flurlClient = new FlurlClient(new HttpClient(handler)) {Settings = { }};
var content2 = await flurlClient.Request(ProtectedUrl).GetAsync();
Console.WriteLine(content2.ResponseMessage.IsSuccessStatusCode);
Console.WriteLine(content2.Cookies.Count);

}

public static async Task SamplePostUrlEncoded()
{
var handler = new ClearanceHandler(FlareSolverrUrl)
{
MaxTimeout = 60_000

public static async Task SampleGet()
{
var handler = new ClearanceHandler(FlareSolverrUrl)
{
MaxTimeout = 60000
};
};

var client = new HttpClient(handler);
var content = await client.GetStringAsync(ProtectedUrl);
Console.WriteLine(content);
}
var request = new HttpRequestMessage();
request.Headers.ExpectContinue = false;
request.RequestUri = new Uri(ProtectedUrl);
var postData = new Dictionary<string, string> { { "story", "test" } };
request.Content = FormUrlEncodedContentWithEncoding(postData, Encoding.UTF8);
request.Method = HttpMethod.Post;

public static async Task SamplePostUrlEncoded()
{
var handler = new ClearanceHandler(FlareSolverrUrl)
{
MaxTimeout = 60000
};
var client = new HttpClient(handler);
var content = await client.SendAsync(request);
Console.WriteLine(content);
}

var request = new HttpRequestMessage();
request.Headers.ExpectContinue = false;
request.RequestUri = new Uri(ProtectedUrl);
var postData = new Dictionary<string, string> { { "story", "test" }};
request.Content = FormUrlEncodedContentWithEncoding(postData, Encoding.UTF8);
request.Method = HttpMethod.Post;
static ByteArrayContent FormUrlEncodedContentWithEncoding(
IEnumerable<KeyValuePair<string, string>> nameValueCollection, Encoding encoding)
{
// utf-8 / default
if (Encoding.UTF8.Equals(encoding) || encoding == null)
return new FormUrlEncodedContent(nameValueCollection);

var client = new HttpClient(handler);
var content = await client.SendAsync(request);
Console.WriteLine(content);
}
// other encodings
var builder = new StringBuilder();

static ByteArrayContent FormUrlEncodedContentWithEncoding(
IEnumerable<KeyValuePair<string, string>> nameValueCollection, Encoding encoding)
{
// utf-8 / default
if (Encoding.UTF8.Equals(encoding) || encoding == null)
return new FormUrlEncodedContent(nameValueCollection);
foreach (var pair in nameValueCollection) {
if (builder.Length > 0)
builder.Append('&');
builder.Append(HttpUtility.UrlEncode(pair.Key, encoding));
builder.Append('=');
builder.Append(HttpUtility.UrlEncode(pair.Value, encoding));
}

// other encodings
var builder = new StringBuilder();
foreach (var pair in nameValueCollection)
{
if (builder.Length > 0)
builder.Append('&');
builder.Append(HttpUtility.UrlEncode(pair.Key, encoding));
builder.Append('=');
builder.Append(HttpUtility.UrlEncode(pair.Value, encoding));
}
// HttpRuleParser.DefaultHttpEncoding == "latin1"
var data = Encoding.GetEncoding("latin1").GetBytes(builder.ToString());
var content = new ByteArrayContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
return content;
}
// HttpRuleParser.DefaultHttpEncoding == "latin1"
var data = Encoding.GetEncoding("latin1").GetBytes(builder.ToString());
var content = new ByteArrayContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
return content;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>FlareSolverrSharp.Sample</RootNamespace>
<AssemblyName>FlareSolverrSharp.Sample</AssemblyName>
<Version>3.0.7</Version>
<Version>3.0.8</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\FlareSolverrSharp\FlareSolverrSharp.csproj" />
</ItemGroup>
Expand Down
27 changes: 16 additions & 11 deletions sample/FlareSolverrSharp.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@

namespace FlareSolverrSharp.Sample
using System;
using System.Collections.Generic;
using FlareSolverrSharp.Exceptions;
using System.Net.Http;
using System.Threading.Tasks;

namespace FlareSolverrSharp.Sample;

public static class Program
{
static class Program
{
static void Main()
{
ClearanceHandlerSample.SampleGet().Wait();
ClearanceHandlerSample.SamplePostUrlEncoded().Wait();
}
}
}

public static async Task Main()
{
await ClearanceHandlerSample.SampleGet();
}

}
34 changes: 34 additions & 0 deletions sample/FlareSolverrSharp.Sample/Settings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("FlareSolverrSharp.Tests")]

namespace FlareSolverrSharp.Sample;

internal static class Settings
{

internal const string FlareSolverrApiUrl = "http://localhost:8191/";
internal const string ProxyUrl = "http://127.0.0.1:8888/";
internal static readonly Uri ProtectedUri = new Uri("https://nowsecure.nl");

internal static readonly Uri ProtectedPostUri =
new Uri("https://badasstorrents.com/torrents/search/720p/date/desc");

internal static readonly Uri ProtectedDdgUri = new Uri("https://anidex.info/?q=text");
internal static readonly Uri ProtectedCcfUri = new Uri("https://www.muziekfabriek.org");


// causes a redirect making the test falsely fail
// internal static readonly Uri ProtectedBlockedUri = new Uri("https://cpasbiens3.fr/");

/*
To configure TinyProxy in local:
* sudo vim /etc/tinyproxy/tinyproxy.conf
* edit => LogFile "/tmp/tinyproxy.log"
* edit => Syslog Off
* sudo tinyproxy -d
* sudo tail -f /tmp/tinyproxy.log
*/

}
107 changes: 56 additions & 51 deletions src/FlareSolverrSharp/ChallengeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,61 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FlareSolverrSharp.Constants;

namespace FlareSolverrSharp
namespace FlareSolverrSharp;

public static class ChallengeDetector
{
public static class ChallengeDetector
{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string>{
"cloudflare",
"cloudflare-nginx",
"ddos-guard"
};

/// <summary>
/// Checks if clearance is required.
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site requires clearance</returns>
public static bool IsClearanceRequired(HttpResponseMessage response) => IsCloudflareProtected(response);

/// <summary>
/// Checks if the site is protected by Cloudflare
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site is protected</returns>
private static bool IsCloudflareProtected(HttpResponseMessage response)
{
// check response headers
if (!response.Headers.Server.Any(i =>
i.Product != null && CloudflareServerNames.Contains(i.Product.Name.ToLower())))
return false;

// detect CloudFlare and DDoS-GUARD
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden)) {
var responseHtml = response.Content.ReadAsStringAsync().Result;
if (responseHtml.Contains("<title>Just a moment...</title>") || // Cloudflare
responseHtml.Contains("<title>Access denied</title>") || // Cloudflare Blocked
responseHtml.Contains("<title>Attention Required! | Cloudflare</title>") || // Cloudflare Blocked
responseHtml.Trim().Equals("error code: 1020") || // Cloudflare Blocked
responseHtml.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1) // DDOS-GUARD
return true;
}

// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary.ToString() == "Accept-Encoding,User-Agent" &&
response.Content.Headers.ContentEncoding.ToString() == "" &&
response.Content.ReadAsStringAsync().Result.ToLower().Contains("ddos"))
return true;

return false;
}

}
}

/// <summary>
/// Checks if clearance is required.
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site requires clearance</returns>
public static bool IsClearanceRequiredAsync(HttpResponseMessage response)
=> IsCloudflareProtectedAsync(response);

/// <summary>
/// Checks if the site is protected by Cloudflare
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site is protected</returns>
private static bool IsCloudflareProtectedAsync(HttpResponseMessage response)
{
// check response headers
if (response.Headers.Server.Any(i =>
i.Product != null
&& CloudflareValues.CloudflareServerNames.Contains(
i.Product.Name.ToLower()))) {
// return false;
return true;
}

// detect CloudFlare and DDoS-GUARD
if (response.StatusCode is HttpStatusCode.ServiceUnavailable or HttpStatusCode.Forbidden
or (HttpStatusCode) CloudflareStatusCodes.OriginUnreachable) {
var responseHtml = response.Content.ReadAsStringAsync().Result;


if (CloudflareValues.CloudflareBlocked.Any(responseHtml.Contains) || // Cloudflare Blocked
responseHtml.Trim().StartsWith(CloudflareValues.CLOUDFLARE_ERROR_CODE_PREFIX) || // Cloudflare Blocked
responseHtml.IndexOf(CloudflareValues.DDOS_GUARD_TITLE, StringComparison.OrdinalIgnoreCase)
> -1) // DDOS-GUARD
return true;
}

// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary.ToString() == "Accept-Encoding,User-Agent" &&
response.Content.Headers.ContentEncoding.ToString() == String.Empty &&
(response.Content.ReadAsStringAsync().Result).ToLower().Contains("ddos"))
return true;

return false;
}

public static bool IsCloudflareCookie(string cookieName) =>
cookieName.StartsWith("cf_") || cookieName.StartsWith("__cf") || cookieName.StartsWith("__ddg");

}
Loading