0% found this document useful (0 votes)
179 views231 pages

OpenBullet2 Enhanced Part1 With Color

The document contains unit tests for the RuriLib HTTP client, focusing on various HTTP request scenarios such as sending headers, handling cookies, and managing response content. It uses the Xunit testing framework and includes methods for sending requests and validating responses. The tests cover functionalities like GET requests, handling different content types, and managing proxy settings.

Uploaded by

joeblow232123
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
179 views231 pages

OpenBullet2 Enhanced Part1 With Color

The document contains unit tests for the RuriLib HTTP client, focusing on various HTTP request scenarios such as sending headers, handling cookies, and managing response content. It uses the Xunit testing framework and includes methods for sending requests and validating responses. The tests cover functionalities like GET requests, handling different content types, and managing proxy settings.

Uploaded by

joeblow232123
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 231

Table of Contents

-----------------------------------
File: /mnt/data/RuriLib_Http_Tests_Documentation.pdf
ProxyClientHandlerTests.cs

using Newtonsoft.Json.Linq;

using RuriLib.Proxies;

using RuriLib.Proxies.Clients;

using System;

using System.Collections.Generic;

using System.Net;

using System.Net.Http;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Http.Tests

public class ProxyClientHandlerTests

[Fact]

public async Task SendAsync_Get_Headers()

var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/user-agent")

};

message.Headers.Add("User-Agent", userAgent);

var response = await RequestAsync(message);


var userAgentActual = await GetJsonStringValueAsync(response, "user-agent");

Assert.NotEmpty(userAgentActual);

Assert.Equal(userAgent, userAgentActual);

[Fact]

public async Task SendAsync_Get_Query()

var key = "key";

var value = "value";

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri($"http://httpbin.org/get?{key}={value}")

};

var response = await RequestAsync(message);

var actual = await GetJsonDictionaryValueAsync(response, "args");

Assert.True(actual.ContainsKey(key));

Assert.True(actual.ContainsValue(value));

[Fact]

public async Task SendAsync_Get_UTF8()

var expected = "


n
";
var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/encoding/utf8")

};

var response = await RequestAsync(message);

var actual = await response.Content.ReadAsStringAsync();

Assert.Contains(expected, actual);

[Fact]

public async Task SendAsync_Get_HTML()

long expectedLength = 3741;

var contentType = "text/html";

var charSet = "utf-8";

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/html")

};

var response = await RequestAsync(message);

var content = response.Content;

Assert.NotNull(content);
var headers = response.Content.Headers;

Assert.NotNull(headers);

Assert.NotNull(headers.ContentLength);

Assert.Equal(expectedLength, headers.ContentLength.Value);

Assert.NotNull(headers.ContentType);

Assert.Equal(contentType, headers.ContentType.MediaType);

Assert.Equal(charSet, headers.ContentType.CharSet);

[Fact]

public async Task SendAsync_Get_Delay()

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/delay/4")

};

var response = await RequestAsync(message);

var source = response.Content.ReadAsStringAsync();

Assert.NotNull(response);

Assert.NotNull(source);

[Fact]

public async Task SendAsync_Get_Stream()

var message = new HttpRequestMessage


{

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/stream/20")

};

var response = await RequestAsync(message);

var source = response.Content.ReadAsStringAsync();

Assert.NotNull(response);

Assert.NotNull(source);

[Fact]

public async Task SendAsync_Get_Gzip()

var expected = "gzip, deflate";

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri("http://httpbin.org/gzip")

};

message.Headers.TryAddWithoutValidation("Accept-Encoding", expected);

var response = await RequestAsync(message);

var actual = await GetJsonDictionaryValueAsync(response, "headers");

Assert.Equal(expected, actual["Accept-Encoding"]);

[Fact]
public async Task SendAsync_Get_Cookies()

var name = "name";

var value = "value";

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri($"http://httpbin.org/cookies/set?{name}={value}")

};

var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);

var cookieContainer = new CookieContainer();

using var proxyClientHandler = new ProxyClientHandler(proxyClient)

CookieContainer = cookieContainer

};

using var client = new HttpClient(proxyClientHandler);

var response = await client.SendAsync(message);

var cookies = cookieContainer.GetCookies(new Uri("http://httpbin.org/"));

Assert.Single(cookies);

var cookie = cookies[name];

Assert.Equal(name, cookie.Name);

Assert.Equal(value, cookie.Value);

client.Dispose();
}

[Fact]

public async Task SendAsync_Get_StatusCode()

var code = "404";

var expected = "NotFound";

var message = new HttpRequestMessage

Method = HttpMethod.Get,

RequestUri = new Uri($"http://httpbin.org/status/{code}")

};

var response = await RequestAsync(message);

Assert.NotNull(response);

Assert.Equal(expected, response.StatusCode.ToString());

[Fact]

public async Task SendAsync_Get_ExplicitHostHeader()

var message = new HttpRequestMessage(HttpMethod.Get, "https://httpbin.org/headers");

message.Headers.Host = "httpbin.org";

var response = await RequestAsync(message);

Assert.NotNull(response);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

}
private static async Task<HttpResponseMessage> RequestAsync(HttpRequestMessage request

var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);

using var proxyClientHandler = new ProxyClientHandler(proxyClient)

CookieContainer = new CookieContainer()

};

using var client = new HttpClient(proxyClientHandler);

return await client.SendAsync(request);

private static async Task<string> GetJsonStringValueAsync(HttpResponseMessage response, st

var source = await response.Content.ReadAsStringAsync();

var obj = JObject.Parse(source);

var result = obj.TryGetValue(valueName, out var token);

return result

? token.Value<string>()

: string.Empty;

private static async Task<Dictionary<string, string>> GetJsonDictionaryValueAsync(HttpRespons

var source = await response.Content.ReadAsStringAsync();

var obj = JObject.Parse(source);


var result = obj.TryGetValue(valueName, out var token);

return result

? token.ToObject<Dictionary<string, string>>()

: null;

RLHttpClientTests.cs

using Newtonsoft.Json.Linq;

using RuriLib.Http.Models;

using RuriLib.Proxies;

using RuriLib.Proxies.Clients;

using System;

using System.Collections.Generic;

using System.Net;

using System.Net.Http;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Http.Tests

public class RLHttpClientTests

[Fact]

public async Task SendAsync_Get_Headers()

var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, lik
var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/user-agent")

};

message.Headers.Add("User-Agent", userAgent);

var response = await RequestAsync(message);

var userAgentActual = await GetJsonValueAsync<string>(response, "user-agent");

Assert.NotEmpty(userAgentActual);

Assert.Equal(userAgent, userAgentActual);

[Fact]

public async Task SendAsync_Get_Query()

var key = "key";

var value = "value";

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri($"http://httpbin.org/get?{key}={value}")

};

var response = await RequestAsync(message);

var actual = await GetJsonDictionaryValueAsync(response, "args");

Assert.True(actual.ContainsKey(key));
Assert.True(actual.ContainsValue(value));

[Fact]

public async Task SendAsync_Get_UTF8()

var expected = "


n
";

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/encoding/utf8")

};

var response = await RequestAsync(message);

var actual = await response.Content.ReadAsStringAsync();

Assert.Contains(expected, actual);

[Fact]

public async Task SendAsync_Get_HTML()

long expectedLength = 3741;

var contentType = "text/html";

var charSet = "utf-8";

var message = new HttpRequest

{
Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/html")

};

var response = await RequestAsync(message);

var content = response.Content;

Assert.NotNull(content);

var headers = response.Content.Headers;

Assert.NotNull(headers);

Assert.NotNull(headers.ContentLength);

Assert.Equal(expectedLength, headers.ContentLength.Value);

Assert.NotNull(headers.ContentType);

Assert.Equal(contentType, headers.ContentType.MediaType);

Assert.Equal(charSet, headers.ContentType.CharSet);

[Fact]

public async Task SendAsync_Get_Delay()

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/delay/4")

};

var response = await RequestAsync(message);

var source = response.Content.ReadAsStringAsync();


Assert.NotNull(response);

Assert.NotNull(source);

[Fact]

public async Task SendAsync_Get_Stream()

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/stream/20")

};

var response = await RequestAsync(message);

var source = response.Content.ReadAsStringAsync();

Assert.NotNull(response);

Assert.NotNull(source);

[Fact]

public async Task SendAsync_Get_Gzip()

var expected = "gzip, deflate";

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("http://httpbin.org/gzip")

};
message.Headers["Accept-Encoding"] = expected;

var response = await RequestAsync(message);

var actual = await GetJsonDictionaryValueAsync(response, "headers");

Assert.Equal(expected, actual["Accept-Encoding"]);

[Fact]

public async Task SendAsync_Get_Cookies()

var name = "name";

var value = "value";

var cookies = new Dictionary<string, string>();

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri($"http://httpbin.org/cookies/set?{name}={value}"),

Cookies = cookies

};

var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);

using var client = new RLHttpClient(proxyClient);

var response = await client.SendAsync(message);

Assert.Single(cookies);

Assert.Equal(value, cookies[name]);
}

[Fact]

public async Task SendAsync_Get_StatusCode()

var code = "404";

var expected = "NotFound";

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri($"http://httpbin.org/status/{code}")

};

var response = await RequestAsync(message);

Assert.NotNull(response);

Assert.Equal(expected, response.StatusCode.ToString());

[Fact]

public async Task SendAsync_Get_ExplicitHostHeader()

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("https://httpbin.org/headers")

};

message.Headers["Host"] = "httpbin.org";
var response = await RequestAsync(message);

Assert.NotNull(response);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);

[Fact]

public async Task SendAsync_GZip_Decompress()

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("https://nghttp2.org/httpbin/gzip")

};

var response = await RequestAsync(message);

var actual = await GetJsonValueAsync<bool>(response, "gzipped");

Assert.True(actual);

[Fact]

public async Task SendAsync_Brotli_Decompress()

var message = new HttpRequest

Method = HttpMethod.Get,

Uri = new Uri("https://nghttp2.org/httpbin/brotli")

};

var response = await RequestAsync(message);


var actual = await GetJsonValueAsync<bool>(response, "brotli");

Assert.True(actual);

private static async Task<HttpResponse> RequestAsync(HttpRequest request)

var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);

using var client = new RLHttpClient(proxyClient);

return await client.SendAsync(request);

private static async Task<T> GetJsonValueAsync<T>(HttpResponse response, string valueName

var source = await response.Content.ReadAsStringAsync();

var obj = JObject.Parse(source);

var result = obj.TryGetValue(valueName, out var token);

return result

? token.Value<T>()

: default;

private static async Task<Dictionary<string, string>> GetJsonDictionaryValueAsync(HttpRespons

var source = await response.Content.ReadAsStringAsync();

var obj = JObject.Parse(source);


var result = obj.TryGetValue(valueName, out var token);

return result

? token.ToObject<Dictionary<string, string>>()

: null;

RuriLib.Http.Tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>

</PropertyGroup>

<ItemGroup>

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />

<PackageReference Include="xunit" Version="2.8.1" />

<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">

<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

<PrivateAssets>all</PrivateAssets>

</PackageReference>

<PackageReference Include="coverlet.collector" Version="6.0.2">

<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

<PrivateAssets>all</PrivateAssets>

</PackageReference>

</ItemGroup>
<ItemGroup>

<ProjectReference Include="..\RuriLib.Http\RuriLib.Http.csproj" />

<ProjectReference Include="..\RuriLib.Proxies\RuriLib.Proxies.csproj" />

</ItemGroup>

</Project>
File: /mnt/data/RuriLib_Http_Documentation.pdf
HttpRequestMessageBuilder.cs

using System.Text;

using System.Net.Http;

using System.Collections.Generic;

using System.Net;

using System.Linq;

using RuriLib.Http.Extensions;

using System;

namespace RuriLib.Http

static internal class HttpRequestMessageBuilder

private static readonly string newLine = "\r\n";

private static readonly string[] commaHeaders = new[] { "Accept", "Accept-Encoding" };

// Builds the first line, for example

// GET /resource HTTP/1.1

public static string BuildFirstLine(HttpRequestMessage request)

if (request.Version >= new Version(2, 0))

throw new Exception($"HTTP/{request.Version.Major}.{request.Version.Minor} not supported yet");

return $"{request.Method.Method} {request.RequestUri.PathAndQuery} HTTP/{request.Version}{newLine}";

// Builds the headers, for example

// Host: example.com

// Connection: Close
public static string BuildHeaders(HttpRequestMessage request, CookieContainer cookies = null)

// NOTE: Do not use AppendLine because it appends \n instead of \r\n

// on Unix-like systems.

var sb = new StringBuilder();

var headers = new List<KeyValuePair<string, string>>();

// Add the Host header if not already provided

if (string.IsNullOrEmpty(request.Headers.Host))

headers.Add("Host", request.RequestUri.Host);

// Add the Connection: Close header if none is present

if (request.Headers.Connection.Count == 0)

headers.Add("Connection", "Close");

// Add the non-content headers

foreach (var header in request.Headers)

headers.Add(header.Key, GetHeaderValue(header));

// Add the Cookie header

if (cookies != null)

var cookiesCollection = cookies.GetCookies(request.RequestUri);


if (cookiesCollection.Count > 0)

var cookieBuilder = new StringBuilder();

foreach (var cookie in cookiesCollection)

cookieBuilder

.Append(cookie)

.Append("; ");

// Remove the last ; and space if not empty

if (cookieBuilder.Length > 2)

cookieBuilder.Remove(cookieBuilder.Length - 2, 2);

headers.Add("Cookie", cookieBuilder);

// Add the content headers

if (request.Content != null)

foreach (var header in request.Content.Headers)

headers.Add(header.Key, GetHeaderValue(header));

// Add the Content-Length header if not already present


if (!headers.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase

var contentLength = request.Content.Headers.ContentLength;

if (contentLength.HasValue && contentLength.Value > 0)

headers.Add("Content-Length", contentLength);

// Write all non-empty headers to the StringBuilder

foreach (var header in headers.Where(h => !string.IsNullOrEmpty(h.Value)))

sb

.Append(header.Key)

.Append(": ")

.Append(header.Value)

.Append(newLine);

// Write the final blank line after all headers

sb.Append(newLine);

return sb.ToString();

private static string GetHeaderValue(KeyValuePair<string, IEnumerable<string>> header)

var values = header.Value.ToArray();


return values.Length switch

0 => string.Empty,

1 => values[0],

_ => string.Join(commaHeaders.Contains(header.Key) ? ", " : " ", values)

};

HttpResponseBuilder.cs

using RuriLib.Http.Helpers;

using RuriLib.Http.Models;

using System;

using System.Collections.Generic;

using System.IO;

using System.IO.Compression;

using System.Net;

using System.Net.Http;

using System.Net.Sockets;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

using System.IO.Pipelines;

using System.Buffers;

using System.Runtime.CompilerServices;

namespace RuriLib.Http
{

internal class HttpResponseBuilder

private PipeReader reader;

private const string newLine = "\r\n";

private readonly byte[] CRLF = Encoding.UTF8.GetBytes(newLine);

private static byte[] CRLFCRLF_Bytes = { 13, 10, 13, 10 };

private HttpResponse response;

private Dictionary<string, List<string>> contentHeaders;

private int contentLength = -1;

internal TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10);

internal HttpResponseBuilder()

// pipe = new Pipe();

/// <summary>

/// Builds an HttpResponse by reading a network stream.

/// </summary>

[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveOptimization)]

async internal Task<HttpResponse> GetResponseAsync(HttpRequest request, Stream stream,

bool readResponseContent = true, CancellationToken cancellationToken = default)

reader = PipeReader.Create(stream);

response = new HttpResponse

{
Request = request

};

contentHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

try

await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false);

await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false);

if (request.Method != HttpMethod.Head)

await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(fa

catch

response.Dispose();

throw;

return response;

// Parses the first line, for example

// HTTP/1.1 200 OK

private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default)

var startingLine = string.Empty;


// Read the first line from the Network Stream

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

int crlfIndex = buff.FirstSpan.IndexOf(CRLF);

if (crlfIndex > -1)

try

startingLine = Encoding.UTF8.GetString(res.Buffer.FirstSpan.Slice(0, crlfIndex));

var fields = startingLine.Split(' ');

response.Version = Version.Parse(fields[0].Trim()[5..]);

response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1

buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF

reader.AdvanceTo(buff.End); // advance to the consumed position

break;

catch

throw new FormatException($"Invalid first line of the HTTP response: {startingLine}");

else

// the responce is incomplete ex. (HTTP/1.1 200 O)

reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined lo


}

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

break;

// Parses the headers

private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default)

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

if (buff.IsSingleSegment)

if (ReadHeadersFastPath(ref buff))

reader.AdvanceTo(buff.Start);

break;

else

{
if (ReadHeadersSlowerPath(ref buff))

reader.AdvanceTo(buff.Start);

break;

reader.AdvanceTo(buff.Start, buff.End); // not adding this line might result in infinit loop.

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

break;

/// <summary>

/// Reads all Header Lines using <see cref="Span{T}"/> For High Perfromace Parsing.

/// </summary>

/// <param name="buff">Buffered Data From Pipe</param>

private bool ReadHeadersFastPath(ref ReadOnlySequence<byte> buff)

int endofheadersindex;

if ((endofheadersindex = buff.FirstSpan.IndexOf(CRLFCRLF_Bytes)) > -1)

var spanLines = buff.FirstSpan.Slice(0, endofheadersindex + 4);

var Lines = spanLines.SplitLines();// we use spanHelper class here to make a for each loop.
foreach (var Line in Lines)

ProcessHeaderLine(Line);

buff = buff.Slice(endofheadersindex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe b

return true;

return false;

/// <summary>

/// Reads all Header Lines using SequenceReader.

/// </summary>

/// <param name="buff">Buffered Data From Pipe</param>

private bool ReadHeadersSlowerPath(ref ReadOnlySequence<byte> buff)

var reader = new SequenceReader<byte>(buff);

while (reader.TryReadTo(out ReadOnlySpan<byte> Line, CRLF, true))

if (Line.Length == 0)// reached last crlf (empty line)

buff = buff.Slice(reader.Position);

return true;// all headers received

ProcessHeaderLine(Line);

}
buff = buff.Slice(reader.Position);

return false;// empty line not found need more data

private void ProcessHeaderLine(ReadOnlySpan<byte> header)

if (header.Length == 0)

return;

// changed to use span directly to decrease the number of strings allocated (less GC activity)

var separatorPos = header.IndexOf((byte)':');

// If not found, don't do anything because the header is not valid

// Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe

// is not advanced properly) so it can cause an exception.

if (separatorPos == -1)

return;

var headerName = Encoding.UTF8.GetString(header.Slice(0, separatorPos));

var headerValuespan = header.Slice(separatorPos + 1); // skip ':'

var headerValue = headerValuespan[0] == (byte)' ' ? Encoding.UTF8.GetString(headerValuesp

// If the header is Set-Cookie, add the cookie

if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) ||

headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase))

{
SetCookie(response, headerValue);

// If it's a content header

else if (ContentHelper.IsContentHeader(headerName))

if (contentHeaders.TryGetValue(headerName, out var values))

values.Add(headerValue);

else

values = new List<string>

headerValue

};

contentHeaders.Add(headerName, values);

else

response.Headers[headerName] = headerValue;

// Sets the value of a cookie

private static void SetCookie(HttpResponse response, string value)

if (value.Length == 0)
{

return;

var endCookiePos = value.IndexOf(';');

var separatorPos = value.IndexOf('=');

if (separatorPos == -1)

// Invalid cookie, simply don't add it

return;

string cookieValue;

var cookieName = value.Substring(0, separatorPos);

if (endCookiePos == -1)

cookieValue = value[(separatorPos + 1)..];

else

cookieValue = value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1);

response.Request.Cookies[cookieName] = cookieValue;

private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken

{
// If there are content headers

if (contentHeaders.Count != 0)

contentLength = GetContentLength();

if (readResponseContent)

// Try to get the body and write it to a MemoryStream

var finaleResponceStream = await GetMessageBodySource(cancellationToken).Configure

// Rewind the stream and set the content of the response and its headers

finaleResponceStream.Seek(0, SeekOrigin.Begin);

response.Content = new StreamContent(finaleResponceStream);

else

response.Content = new ByteArrayContent(Array.Empty<byte>());

foreach (var pair in contentHeaders)

response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value);

private Task<Stream> GetMessageBodySource(CancellationToken cancellationToken)

if (response.Headers.ContainsKey("Transfer-Encoding"))
{

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetChunkedDecompressedStream(cancellationToken);

else

return ReceiveMessageBodyChunked(cancellationToken);

else if (contentLength > -1)

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetContentLengthDecompressedStream(cancellationToken);

else

return ReciveContentLength(cancellationToken);

else // handle the case where sever never sent chunked encoding nor content-length headrs (th

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetResponcestreamUntilCloseDecompressed(cancellationToken);

}
else

return GetResponcestreamUntilClose(cancellationToken);

private async Task<Stream> GetResponcestreamUntilClose(CancellationToken cancellationToke

var responcestream = new MemoryStream();

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

if (res.IsCanceled)

cancellationToken.ThrowIfCancellationRequested();

var buff = res.Buffer;

if (buff.IsSingleSegment)

responcestream.Write(buff.FirstSpan);

else

foreach (var seg in buff)

{
responcestream.Write(seg.Span);

reader.AdvanceTo(buff.End);

if (res.IsCompleted || res.Buffer.Length == 0)// here the pipe will be complete if the server clo

break;

return responcestream;

private async Task<Stream> GetContentLengthDecompressedStream(CancellationToken cancel

using var compressedStream = GetZipStream(await ReciveContentLength(cancellationToken).

var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureA

return decompressedStream;

private async Task<Stream> GetChunkedDecompressedStream(CancellationToken cancellationT

using var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellati

var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureA

return decompressedStream;

}
private async Task<Stream> GetResponcestreamUntilCloseDecompressed(CancellationToken c

using var compressedStream = GetZipStream(await GetResponcestreamUntilClose(cancellatio

var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureA

return decompressedStream;

private async Task<Stream> ReciveContentLength(CancellationToken cancellationToken)

var contentlenghtStream = new MemoryStream(contentLength == -1 ? 0 : contentLength);

if (contentLength == 0)

return contentlenghtStream;

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

if (buff.IsSingleSegment)

contentlenghtStream.Write(buff.FirstSpan);

else

foreach (var seg in buff)

{
contentlenghtStream.Write(seg.Span);

reader.AdvanceTo(buff.End);

if (contentlenghtStream.Length >= contentLength)

return contentlenghtStream;

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

break;

return contentlenghtStream;

private int GetContentLength()

if (contentHeaders.TryGetValue("Content-Length", out var values))

if (int.TryParse(values[0], out var length))

return length;

return -1;
}

private string GetContentEncoding()

var encoding = "";

if (contentHeaders.TryGetValue("Content-Encoding", out var values))

encoding = values[0];

return encoding;

[MethodImpl(MethodImplOptions.AggressiveOptimization)]

private async Task<Stream> ReceiveMessageBodyChunked(CancellationToken cancellationTok

var chunkedDecoder = new ChunkedDecoderOptimized();

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

chunkedDecoder.Decode(ref buff);

reader.AdvanceTo(buff.Start, buff.End);

if (chunkedDecoder.Finished)

return chunkedDecoder.DecodedStream;

if (res.IsCanceled || res.IsCompleted)
{

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

break;

return chunkedDecoder.DecodedStream;

private Stream GetZipStream(Stream stream)

var contentEncoding = GetContentEncoding().ToLower();

stream.Seek(0, SeekOrigin.Begin);

return contentEncoding switch

"gzip" => new GZipStream(stream, CompressionMode.Decompress, false),

"deflate" => new DeflateStream(stream, CompressionMode.Decompress, false),

"br" => new BrotliStream(stream, CompressionMode.Decompress, false),

"utf-8" => stream,

_ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding for

};

HttpResponseMessageBuilder.cs

using System;

using System.IO;
using System.Net;

using System.Text;

using System.Net.Http;

using System.Threading;

using System.Net.Sockets;

using System.IO.Compression;

using System.Threading.Tasks;

using System.Collections.Generic;

using RuriLib.Http.Helpers;

using RuriLib.Http;

using System.IO.Pipelines;

using System.Buffers;

namespace RuriLib.Http

internal class HttpResponseMessageBuilder

private PipeReader reader;

private const string newLine = "\r\n";

private readonly byte[] CRLF = Encoding.UTF8.GetBytes(newLine);

private static byte[] CRLFCRLF_Bytes = { 13, 10, 13, 10 };

private int contentLength = -1;

//private NetworkStream networkStream;

//private Stream commonStream;

private HttpResponseMessage response;

private Dictionary<string, List<string>> contentHeaders;


private readonly CookieContainer cookies;

private readonly Uri uri;

// private readonly ReceiveHelper receiveHelper;

public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(10);

public HttpResponseMessageBuilder(int bufferSize, CookieContainer cookies = null, Uri uri = null

// this.bufferSize = bufferSize;

this.cookies = cookies;

this.uri = uri;

// receiveHelper = new ReceiveHelper(bufferSize);

public async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage request, S

bool readResponseContent = true, CancellationToken cancellationToken = default) {

reader = PipeReader.Create(stream);

response = new HttpResponseMessage();

contentHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

response.RequestMessage = request;

try

await ReceiveFirstLineAsync(cancellationToken).ConfigureAwait(false);

await ReceiveHeadersAsync(cancellationToken).ConfigureAwait(false);

await ReceiveContentAsync(readResponseContent, cancellationToken).ConfigureAwait(fals


}

catch

response.Dispose();

throw;

return response;

// Parses the first line, for example

// HTTP/1.1 200 OK

private async Task ReceiveFirstLineAsync(CancellationToken cancellationToken = default)

var startingLine = string.Empty;

// Read the first line from the Network Stream

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

int crlfIndex = buff.FirstSpan.IndexOf(CRLF);

if (crlfIndex > -1)

try

startingLine = Encoding.UTF8.GetString(buff.FirstSpan.Slice(0, crlfIndex));

var fields = startingLine.Split(' ');


response.Version = Version.Parse(fields[0].Trim()[5..]);

response.StatusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), fields[1

buff = buff.Slice(0, crlfIndex + 2); // add 2 bytes for the CRLF

reader.AdvanceTo(buff.End); // advance to the consumed position

break;

catch

throw new FormatException($"Invalid first line of the HTTP response: {startingLine}");

else

// the responce is incomplete ex. (HTTP/1.1 200 O)

reader.AdvanceTo(buff.Start, buff.End); // nothing consumed but all the buffer examined lo

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

// Parses the headers

private async Task ReceiveHeadersAsync(CancellationToken cancellationToken = default)

{
while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

if (buff.IsSingleSegment)

if (ReadHeadersFastPath(ref buff))

reader.AdvanceTo(buff.Start);

break;

else

if (ReadHeadersSlowerPath(ref buff))

reader.AdvanceTo(buff.Start);

break;

reader.AdvanceTo(buff.Start, buff.End);// not adding ghis linw might result in infinit loop

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

}
}

/// <summary>

/// Reads all Header Lines using <see cref="Span{T}"/> For High Perfromace Parsing.

/// </summary>

/// <param name="buff">Buffered Data From Pipe</param>

private bool ReadHeadersFastPath(ref ReadOnlySequence<byte> buff)

int endofheadersindex;

if ((endofheadersindex = buff.FirstSpan.IndexOf(CRLFCRLF_Bytes)) > -1)

var spanLines = buff.FirstSpan.Slice(0, endofheadersindex + 4);

var Lines = spanLines.SplitLines();// we use spanHelper class here to make a for each loop.

foreach (var Line in Lines)

ProcessHeaderLine(Line);

buff = buff.Slice(endofheadersindex + 4); // add 4 bytes for \r\n\r\n and to advance the pipe b

return true;

return false;

/// <summary>

/// Reads all Header Lines using SequenceReader.

/// </summary>

/// <param name="buff">Buffered Data From Pipe</param>


private bool ReadHeadersSlowerPath(ref ReadOnlySequence<byte> buff)

var reader = new SequenceReader<byte>(buff);

while (reader.TryReadTo(out ReadOnlySpan<byte> Line, CRLF, true))

if (Line.Length == 0)// reached last crlf (empty line)

buff = buff.Slice(reader.Position);

return true;// all headers received

ProcessHeaderLine(Line);

buff = buff.Slice(reader.Position);

return false;// empty line not found need more data

private void ProcessHeaderLine(ReadOnlySpan<Byte> header)

if (header.Length == 0)

return;

// changed to use span directly to decrease the number of strings allocated (less GC activity)

var separatorPos = header.IndexOf((byte)':');

// If not found, don't do anything because the header is not valid

// Sometimes it can happen that the first line e.g. HTTP/1.1 200 OK is read as a header (maybe
// is not advanced properly) so it can cause an exception.

if (separatorPos == -1)

return;

var headerName = Encoding.UTF8.GetString(header.Slice(0, separatorPos));

var headerValuespan = header.Slice(separatorPos + 1); // skip ':'

var headerValue = headerValuespan[0] == (byte)' ' ? Encoding.UTF8.GetString(headerValuesp

// If the header is Set-Cookie, add the cookie

if (headerName.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase) ||

headerName.Equals("Set-Cookie2", StringComparison.OrdinalIgnoreCase))

SetCookie(headerValue);

// If it's a content header

else if (ContentHelper.IsContentHeader(headerName))

if (contentHeaders.TryGetValue(headerName, out var values))

values.Add(headerValue);

else

values = new List<string>

headerValue

};
contentHeaders.Add(headerName, values);

else

response.Headers.TryAddWithoutValidation(headerName, headerValue);

// Sets the value of a cookie

private void SetCookie(string value)

if (value.Length == 0)

return;

var endCookiePos = value.IndexOf(';');

var separatorPos = value.IndexOf('=');

if (separatorPos == -1)

// Invalid cookie, simply don't add it

return;

string cookieValue;

var cookieName = value.Substring(0, separatorPos);

if (endCookiePos == -1)
{

cookieValue = value[(separatorPos + 1)..];

else

cookieValue = value.Substring(separatorPos + 1, (endCookiePos - separatorPos) - 1);

#region Get Expiration Time

var expiresPos = value.IndexOf("expires=");

if (expiresPos != -1)

string expiresStr;

var endExpiresPos = value.IndexOf(';', expiresPos);

expiresPos += 8;

if (endExpiresPos == -1)

expiresStr = value[expiresPos..];

else

expiresStr = value[expiresPos..endExpiresPos];

if (DateTime.TryParse(expiresStr, out var expires) &&

expires < DateTime.Now)

{
var collection = cookies.GetCookies(uri);

if (collection[cookieName] != null)

collection[cookieName].Expired = true;

#endregion

if (cookieValue.Length == 0 ||

cookieValue.Equals("deleted", StringComparison.OrdinalIgnoreCase))

var collection = cookies.GetCookies(uri);

if (collection[cookieName] != null)

collection[cookieName].Expired = true;

else

cookies.Add(new Cookie(cookieName, cookieValue, "/", uri.Host));

// TODO: Make this async (need to refactor the mess below)

private async Task ReceiveContentAsync(bool readResponseContent = true, CancellationToken

// If there are content headers

if (contentHeaders.Count != 0)

contentLength = GetContentLength();
if (readResponseContent)

// Try to get the body and write it to a MemoryStream

var finaleResponceStream = await GetMessageBodySource(cancellationToken).Configure

// Rewind the stream and set the content of the response and its headers

finaleResponceStream.Seek(0, SeekOrigin.Begin);

response.Content = new StreamContent(finaleResponceStream);

else

response.Content = new ByteArrayContent(Array.Empty<byte>());

foreach (var pair in contentHeaders)

response.Content.Headers.TryAddWithoutValidation(pair.Key, pair.Value);

private Task<Stream> GetMessageBodySource(CancellationToken cancellationToken)

if (response.Headers.Contains("Transfer-Encoding"))

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetChunkedDecompressedStream(cancellationToken);

}
else

return ReceiveMessageBodyChunked(cancellationToken);

else if (contentLength > -1)

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetContentLengthDecompressedStream(cancellationToken);

else

return ReciveContentLength(cancellationToken);

else // handle the case where sever never sent chunked encoding nor content-length headrs (th

if (contentHeaders.ContainsKey("Content-Encoding"))

return GetResponcestreamUntilCloseDecompressed(cancellationToken);

else

return GetResponcestreamUntilClose(cancellationToken);

}
}

private async Task<Stream> GetResponcestreamUntilClose(CancellationToken cancellationToke

var responcestream = new MemoryStream();

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

if (res.IsCanceled)

cancellationToken.ThrowIfCancellationRequested();

var buff = res.Buffer;

if (buff.IsSingleSegment)

responcestream.Write(buff.FirstSpan);

else

foreach (var seg in buff)

responcestream.Write(seg.Span);

reader.AdvanceTo(buff.End);

if (res.IsCompleted || res.Buffer.Length == 0)// here the pipe will be complete if the server clo
{

break;

return responcestream;

private async Task<Stream> GetContentLengthDecompressedStream(CancellationToken cancel

using (var compressedStream = GetZipStream(await ReciveContentLength(cancellationToken)

var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken);

return decompressedStream;

private async Task<Stream> GetChunkedDecompressedStream(CancellationToken cancellationT

using (var compressedStream = GetZipStream(await ReceiveMessageBodyChunked(cancellat

var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken).Configu

return decompressedStream;

private async Task<Stream> GetResponcestreamUntilCloseDecompressed(CancellationToken c

using var compressedStream = GetZipStream(await GetResponcestreamUntilClose(cancellatio


var decompressedStream = new MemoryStream();

await compressedStream.CopyToAsync(decompressedStream, cancellationToken).ConfigureA

return decompressedStream;

private async Task<Stream> ReciveContentLength(CancellationToken cancellationToken)

MemoryStream contentlenghtStream = new MemoryStream(contentLength);

if (contentLength == 0)

return contentlenghtStream;

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

if (buff.IsSingleSegment)

contentlenghtStream.Write(buff.FirstSpan);

else

foreach (var seg in buff)

contentlenghtStream.Write(seg.Span);

reader.AdvanceTo(buff.End);
if (contentlenghtStream.Length >= contentLength)

return contentlenghtStream;

if (res.IsCanceled || res.IsCompleted)

reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

private int GetContentLength()

if (contentHeaders.TryGetValue("Content-Length", out var values))

if (int.TryParse(values[0], out var length))

return length;

return -1;

private string GetContentEncoding()


{

var encoding = "";

if (contentHeaders.TryGetValue("Content-Encoding", out var values))

encoding = values[0];

return encoding;

private async Task<Stream> ReceiveMessageBodyChunked(CancellationToken cancellationTok

var chunkedDecoder = new ChunkedDecoderOptimized();

while (true)

var res = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);

var buff = res.Buffer;

chunkedDecoder.Decode(ref buff);

reader.AdvanceTo(buff.Start, buff.End);

if (chunkedDecoder.Finished)

return chunkedDecoder.DecodedStream;

if (res.IsCanceled || res.IsCompleted)

{
reader.Complete();

cancellationToken.ThrowIfCancellationRequested();

private Stream GetZipStream(Stream stream)

var contentEncoding = GetContentEncoding().ToLower();

stream.Seek(0, SeekOrigin.Begin);

return contentEncoding switch

"gzip" => new GZipStream(stream, CompressionMode.Decompress, false),

"deflate" => new DeflateStream(stream, CompressionMode.Decompress, false),

"br" => new BrotliStream(stream, CompressionMode.Decompress, false),

_ => throw new InvalidOperationException($"'{contentEncoding}' not supported encoding for

};

LICENSE

Some portions of the code were the work of Ruslan Khuduev and Artem Dontsov, to which

I am grateful, all rights are reserved to them. Their work is under the MIT license.
-----------------------------------------------------------------------------

Copyright © 2012-2021 Ruslan Khuduev <[email protected]>

Copyright © 2017-2021 Artem Dontsov

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in

all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

THE SOFTWARE.

ProxyClientHandler.cs

using System;

using System.IO;

using System.Net;

using System.Net.Http;

using System.Net.Sockets;

using System.Net.Security;

using System.Security.Authentication;
using System.Threading;

using System.Threading.Tasks;

using System.Security.Cryptography.X509Certificates;

using RuriLib.Proxies;

using System.Text;

using RuriLib.Proxies.Exceptions;

using System.Collections.Generic;

namespace RuriLib.Http

/// <summary>

/// Represents <see cref="HttpMessageHandler"/> with a <see cref="ProxyClient"/>

/// to provide proxy support to the <see cref="HttpClient"/>.

/// </summary>

public class ProxyClientHandler : HttpMessageHandler, IDisposable

private readonly ProxyClient proxyClient;

private TcpClient tcpClient;

private Stream connectionCommonStream;

private NetworkStream connectionNetworkStream;

#region Properties

/// <summary>

/// The underlying proxy client.

/// </summary>

public ProxyClient ProxyClient => proxyClient;

/// <summary>
/// Gets the raw bytes of the last request that was sent.

/// </summary>

public List<byte[]> RawRequests { get; } = new();

/// <summary>

/// Allow automatic redirection on 3xx reply.

/// </summary>

public bool AllowAutoRedirect { get; set; } = true;

/// <summary>

/// The maximum number of times a request will be redirected.

/// </summary>

public int MaxNumberOfRedirects { get; set; } = 8;

/// <summary>

/// Whether to read the content of the response. Set to false if you're only interested

/// in headers.

/// </summary>

public bool ReadResponseContent { get; set; } = true;

/// <summary>

/// The allowed SSL or TLS protocols.

/// </summary>

public SslProtocols SslProtocols { get; set; } = SslProtocols.None;

/// <summary>

/// If true, <see cref="AllowedCipherSuites"/> will be used instead of the default ones.

/// </summary>

public bool UseCustomCipherSuites { get; set; } = false;

/// <summary>
/// The cipher suites to send to the server during the TLS handshake, in order.

/// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020.

/// </summary>

public TlsCipherSuite[] AllowedCipherSuites { get; set; } = new TlsCipherSuite[]

TlsCipherSuite.TLS_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA

};

/// <summary>

/// Gets the type of decompression method used by the handler for automatic

/// decompression of the HTTP content response.


/// </summary>

/// <remarks>

/// Support GZip and Deflate encoding automatically

/// </remarks>

public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | Dec

/// <summary>

/// Gets or sets a value that indicates whether the handler uses the CookieContainer

/// property to store server cookies and uses these cookies when sending requests.

/// </summary>

public bool UseCookies { get; set; } = true;

/// <summary>

/// Gets or sets the cookie container used to store server cookies by the handler.

/// </summary>

public CookieContainer CookieContainer { get; set; }

/// <summary>

/// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL)

/// certificate used for authentication.

/// </summary>

public RemoteCertificateValidationCallback ServerCertificateCustomValidationCallback { get; set;

/// <summary>

/// Gets or sets the X509 certificate revocation mode.

/// </summary>

public X509RevocationMode CertRevocationMode { get; set; }

#endregion

/// <summary>
/// Creates a new instance of <see cref="ProxyClientHandler"/> given a <paramref name="proxyC

/// </summary>

public ProxyClientHandler(ProxyClient proxyClient)

this.proxyClient = proxyClient ?? throw new ArgumentNullException(nameof(proxyClient));

/// <summary>

/// Asynchronously sends a <paramref name="request"/> and returns an <see cref="HttpRespons

/// </summary>

/// <param name="request">The request to send</param>

/// <param name="cancellationToken">A cancellation token to cancel the operation</param>

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,

CancellationToken cancellationToken = default)

=> SendAsync(request, 0, cancellationToken);

private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,

int redirects, CancellationToken cancellationToken = default)

if (redirects > MaxNumberOfRedirects)

throw new Exception("Maximum number of redirects exceeded");

if (request == null)

throw new ArgumentNullException(nameof(request));

}
if (UseCookies && CookieContainer == null)

throw new ArgumentNullException(nameof(CookieContainer));

await CreateConnection(request, cancellationToken).ConfigureAwait(false);

await SendDataAsync(request, cancellationToken).ConfigureAwait(false);

var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait

try

// Optionally perform auto redirection on 3xx response

if (((int)responseMessage.StatusCode) / 100 == 3 && AllowAutoRedirect)

if (!responseMessage.Headers.Contains("Location"))

throw new Exception($"Status code was {(int)responseMessage.StatusCode} but no Lo

$"Disable auto redirect and try again.");

// Compute the redirection URI

var redirectUri = responseMessage.Headers.Location.IsAbsoluteUri

? responseMessage.Headers.Location

: new Uri(request.RequestUri, responseMessage.Headers.Location);

// If not 307, change the method to GET

if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb)

request.Method = HttpMethod.Get;
request.Content = null;

// Port over the cookies if the domains are different

if (request.RequestUri.Host != redirectUri.Host)

var cookies = CookieContainer.GetCookies(request.RequestUri);

foreach (Cookie cookie in cookies)

CookieContainer.Add(redirectUri, new Cookie(cookie.Name, cookie.Value));

// This is needed otherwise if the Host header was set manually

// it will keep the previous one after a domain switch

request.Headers.Host = string.Empty;

// Remove additional headers that could cause trouble

request.Headers.Remove("Origin");

// Set the new URI

request.RequestUri = redirectUri;

// Dispose the previous response

responseMessage.Dispose();

// Perform a new request

return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false);

}
catch

responseMessage.Dispose();

throw;

return responseMessage;

private async Task SendDataAsync(HttpRequestMessage request, CancellationToken cancellatio

byte[] buffer;

using var ms = new MemoryStream();

// Send the first line

buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildFirstLine(request));

ms.Write(buffer);

await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationT

// Send the headers

buffer = Encoding.ASCII.GetBytes(HttpRequestMessageBuilder.BuildHeaders(request, Cookie

ms.Write(buffer);

await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationT

// Optionally send the content

if (request.Content != null)

buffer = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(f

ms.Write(buffer);

await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellatio


}

ms.Seek(0, SeekOrigin.Begin);

RawRequests.Add(ms.ToArray());

private Task<HttpResponseMessage> ReceiveDataAsync(HttpRequestMessage request,

CancellationToken cancellationToken)

var responseBuilder = new HttpResponseMessageBuilder(1024, CookieContainer, request.Req

return responseBuilder.GetResponseAsync(request, connectionCommonStream, ReadRespon

private async Task CreateConnection(HttpRequestMessage request, CancellationToken cancella

// Dispose of any previous connection (if we're coming from a redirect)

tcpClient?.Close();

connectionCommonStream?.Dispose();

connectionNetworkStream?.Dispose();

// Get the stream from the proxies TcpClient

var uri = request.RequestUri;

tcpClient = await proxyClient.ConnectAsync(uri.Host, uri.Port, null, cancellationToken);

connectionNetworkStream = tcpClient.GetStream();

// If https, set up a TLS stream

if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))

try

{
var sslStream = new SslStream(connectionNetworkStream, false, ServerCertificateCustom

var sslOptions = new SslClientAuthenticationOptions

TargetHost = uri.Host,

EnabledSslProtocols = SslProtocols,

CertificateRevocationCheckMode = CertRevocationMode,

};

if (CertRevocationMode != X509RevocationMode.Online)

sslOptions.RemoteCertificateValidationCallback =

new RemoteCertificateValidationCallback((s, c, ch, e) => { return true; });

if (UseCustomCipherSuites)

sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites);

connectionCommonStream = sslStream;

await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken);

catch (Exception ex)

if (ex is IOException || ex is AuthenticationException)

throw new ProxyException("Failed SSL connect");

}
throw;

else

connectionCommonStream = connectionNetworkStream;

/// <inheritdoc/>

protected override void Dispose(bool disposing)

if (disposing)

tcpClient?.Dispose();

connectionCommonStream?.Dispose();

connectionNetworkStream?.Dispose();

base.Dispose(disposing);

README.md

# RuriLib.Http

This is a library that provides a custom HTTP client, in addition to an `HttpMessageHandler` to be used

# Installation

[NuGet](https://nuget.org/packages/RuriLib.Http): `dotnet add package RuriLib.Http`


# Example (Custom Client)

```csharp

using RuriLib.Http;

using RuriLib.Http.Models;

using RuriLib.Proxies;

using RuriLib.Proxies.Clients;

using System;

using System.Collections.Generic;

using System.Net.Http;

using System.Text;

using System.Threading.Tasks;

namespace HttpDemo

class Program

static void Main(string[] args)

_ = MainAsync(args);

Console.ReadLine();

static async Task MainAsync(string[] args)

// Set up the proxy client (see RuriLib.Proxies documentation, here we use

// a NoProxyClient for simplicity)

var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);


// Create the custom proxied client

using var client = new RLHttpClient(proxyClient);

// Create the request

using var request = new HttpRequest

Uri = new Uri("https://httpbin.org/anything"),

Method = HttpMethod.Post,

Headers = new Dictionary<string, string>

{ "Authorization", "Bearer ey..." }

},

Cookies = new Dictionary<string, string>

{ "PHPSESSID", "12345" }

},

// Content a.k.a. the "post data"

Content = new StringContent("My content", Encoding.UTF8, "text/plain")

};

// Send the request and get the response (this can fail so make sure to wrap it in a try/catch blo

using var response = await client.SendAsync(request);

// Read and print the content of the response

var content = await response.Content.ReadAsStringAsync();

Console.WriteLine(content);

}
}

```

# Example (HttpClient)

This example is equivalent to the one above, but it uses the default `HttpClient` so you can take advan

```csharp

using RuriLib.Http;

using RuriLib.Proxies;

using RuriLib.Proxies.Clients;

using System;

using System.Net;

using System.Net.Http;

using System.Text;

using System.Threading.Tasks;

namespace HttpDemo

class Program

static void Main(string[] args)

_ = MainAsync(args);

Console.ReadLine();

static async Task MainAsync(string[] args)

// Set up the proxy client (see RuriLib.Proxies documentation, here we use

// a NoProxyClient for simplicity)


var settings = new ProxySettings();

var proxyClient = new NoProxyClient(settings);

// Create the handler that will be passed to HttpClient

var handler = new ProxyClientHandler(proxyClient)

// This adds cookie support

CookieContainer = new CookieContainer()

};

// Create the proxied HttpClient and the cookie container

using var client = new HttpClient(handler);

// Create the request

using var request = new HttpRequestMessage

RequestUri = new Uri("https://httpbin.org/anything"),

Method = HttpMethod.Post,

// Content a.k.a. the "post data"

Content = new StringContent("My content", Encoding.UTF8, "text/plain")

};

request.Headers.TryAddWithoutValidation("Authorization", "Bearer ey...");

handler.CookieContainer.Add(request.RequestUri, new Cookie("PHPSESSID", "12345"));

// Send the request and get the response (this can fail so make sure to wrap it in a try/catch blo

using var response = await client.SendAsync(request);

// Read and print the content of the response


var content = await response.Content.ReadAsStringAsync();

Console.WriteLine(content);

```

# Credits

Some portions of the code were the work of Ruslan Khuduev and Artem Dontsov, to which I am gratef

RLHttpClient.cs

using System;

using System.IO;

using System.Net;

using System.Net.Http;

using System.Net.Sockets;

using System.Net.Security;

using System.Security.Authentication;

using System.Threading;

using System.Threading.Tasks;

using System.Security.Cryptography.X509Certificates;

using RuriLib.Proxies;

using RuriLib.Proxies.Clients;

using RuriLib.Proxies.Exceptions;

using System.Collections.Generic;

using RuriLib.Http.Models;

using System.Linq;

using System.Runtime.InteropServices;
namespace RuriLib.Http

/// <summary>

/// Custom implementation of an HttpClient.

/// </summary>

public class RLHttpClient : IDisposable

private readonly ProxyClient proxyClient;

private TcpClient tcpClient;

private Stream connectionCommonStream;

private NetworkStream connectionNetworkStream;

#region Properties

/// <summary>

/// The underlying proxy client.

/// </summary>

public ProxyClient ProxyClient => proxyClient;

/// <summary>

/// Gets the raw bytes of all the requests that were sent.

/// </summary>

public List<byte[]> RawRequests { get; } = new();

/// <summary>

/// Allow automatic redirection on 3xx reply.

/// </summary>

public bool AllowAutoRedirect { get; set; } = true;

/// <summary>
/// The maximum number of times a request will be redirected.

/// </summary>

public int MaxNumberOfRedirects { get; set; } = 8;

/// <summary>

/// Whether to read the content of the response. Set to false if you're only interested

/// in headers.

/// </summary>

public bool ReadResponseContent { get; set; } = true;

/// <summary>

/// The allowed SSL or TLS protocols.

/// </summary>

public SslProtocols SslProtocols { get; set; } = SslProtocols.None;

/// <summary>

/// If true, <see cref="AllowedCipherSuites"/> will be used instead of the default ones.

/// </summary>

public bool UseCustomCipherSuites { get; set; } = false;

/// <summary>

/// The cipher suites to send to the server during the TLS handshake, in order.

/// The default value of this property contains the cipher suites sent by Firefox as of 21 Dec 2020.

/// </summary>

public TlsCipherSuite[] AllowedCipherSuites { get; set; } = new TlsCipherSuite[]

TlsCipherSuite.TLS_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_AES_256_GCM_SHA384,
TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,

TlsCipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,

TlsCipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,

TlsCipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA

};

/// <summary>

/// Gets the type of decompression method used by the handler for automatic

/// decompression of the HTTP content response.

/// </summary>

/// <remarks>

/// Support GZip and Deflate encoding automatically

/// </remarks>

public DecompressionMethods AutomaticDecompression => DecompressionMethods.GZip | Dec

/// <summary>

/// Gets or sets delegate to verifies the remote Secure Sockets Layer (SSL)

/// certificate used for authentication.


/// </summary>

public RemoteCertificateValidationCallback ServerCertificateCustomValidationCallback { get; set;

/// <summary>

/// Gets or sets the X509 certificate revocation mode.

/// </summary>

public X509RevocationMode CertRevocationMode { get; set; }

#endregion

/// <summary>

/// Creates a new instance of <see cref="RLHttpClient"/> given a <paramref name="proxyClient"/>

/// If <paramref name="proxyClient"/> is null, <see cref="NoProxyClient"/> will be used.

/// </summary>

public RLHttpClient(ProxyClient proxyClient = null)

this.proxyClient = proxyClient ?? new NoProxyClient();

/// <summary>

/// Asynchronously sends a <paramref name="request"/> and returns an <see cref="HttpRespons

/// </summary>

/// <param name="request">The request to send</param>

/// <param name="cancellationToken">A cancellation token to cancel the operation</param>

public Task<HttpResponse> SendAsync(HttpRequest request, CancellationToken cancellationTo

=> SendAsync(request, 0, cancellationToken);

private async Task<HttpResponse> SendAsync(HttpRequest request, int redirects,

CancellationToken cancellationToken = default)

{
if (redirects > MaxNumberOfRedirects)

throw new Exception("Maximum number of redirects exceeded");

if (request == null)

throw new ArgumentNullException(nameof(request));

await CreateConnection(request, cancellationToken).ConfigureAwait(false);

await SendDataAsync(request, cancellationToken).ConfigureAwait(false);

var responseMessage = await ReceiveDataAsync(request, cancellationToken).ConfigureAwait

try

// Optionally perform auto redirection on 3xx response

if (((int)responseMessage.StatusCode) / 100 == 3 && AllowAutoRedirect)

// Compute the redirection URI

var locationHeaderName = responseMessage.Headers.Keys

.FirstOrDefault(k => k.Equals("Location", StringComparison.OrdinalIgnoreCase));

if (locationHeaderName is null)

throw new Exception($"Status code was {(int)responseMessage.StatusCode} but no Lo

$"Disable auto redirect and try again.");

Uri.TryCreate(responseMessage.Headers[locationHeaderName], UriKind.RelativeOrAbso
var redirectUri = newLocation.IsAbsoluteUri

? newLocation

: new Uri(request.Uri, newLocation);

// If not 307, change the method to GET

if (responseMessage.StatusCode != HttpStatusCode.RedirectKeepVerb)

request.Method = HttpMethod.Get;

request.Content = null;

// Adjust the request if the host is different

if (request.Uri.Host != redirectUri.Host)

// This is needed otherwise if the Host header was set manually

// it will keep the previous one after a domain switch

if (request.HeaderExists("Host", out var hostHeaderName))

request.Headers.Remove(hostHeaderName);

// Remove additional headers that could cause trouble

request.Headers.Remove("Origin");

// Set the new URI

request.Uri = redirectUri;

// Dispose the previous response


responseMessage.Dispose();

// Perform a new request

return await SendAsync(request, redirects + 1, cancellationToken).ConfigureAwait(false);

catch

responseMessage.Dispose();

throw;

return responseMessage;

private async Task SendDataAsync(HttpRequest request, CancellationToken cancellationToken =

var buffer = await request.GetBytesAsync(cancellationToken);

await connectionCommonStream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationT

RawRequests.Add(buffer);

private Task<HttpResponse> ReceiveDataAsync(HttpRequest request,

CancellationToken cancellationToken) =>

new HttpResponseBuilder().GetResponseAsync(request, connectionCommonStream, ReadRe

private async Task CreateConnection(HttpRequest request, CancellationToken cancellationToke

// Dispose of any previous connection (if we're coming from a redirect)

tcpClient?.Close();
if (connectionCommonStream is not null)

await connectionCommonStream.DisposeAsync().ConfigureAwait(false);

if (connectionNetworkStream is not null)

await connectionNetworkStream.DisposeAsync().ConfigureAwait(false);

// Get the stream from the proxies TcpClient

var uri = request.Uri;

tcpClient = await proxyClient.ConnectAsync(uri.Host, uri.Port, null, cancellationToken);

connectionNetworkStream = tcpClient.GetStream();

// If https, set up a TLS stream

if (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))

try

var sslStream = new SslStream(connectionNetworkStream, false, ServerCertificateCustom

var sslOptions = new SslClientAuthenticationOptions

TargetHost = uri.Host,

EnabledSslProtocols = SslProtocols,

CertificateRevocationCheckMode = CertRevocationMode

};
if (CertRevocationMode != X509RevocationMode.Online)

sslOptions.RemoteCertificateValidationCallback =

(_, _, _, _) => true;

if (UseCustomCipherSuites && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(AllowedCipherSuites);

connectionCommonStream = sslStream;

await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken);

catch (Exception ex)

if (ex is IOException || ex is AuthenticationException)

throw new ProxyException("Failed SSL connect");

throw;

else

connectionCommonStream = connectionNetworkStream;

}
/// <inheritdoc/>

public void Dispose()

tcpClient?.Dispose();

connectionCommonStream?.Dispose();

connectionNetworkStream?.Dispose();

RuriLib.Http.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFramework>net8.0</TargetFramework>

<GenerateDocumentationFile>True</GenerateDocumentationFile>

<Authors>Ruri</Authors>

<Version>1.0.1</Version>

<Description>This is a library that provides a custom HTTP client, in addition to an HttpMessageHan

<Copyright>Ruri 2022</Copyright>

<RepositoryUrl>https://github.com/openbullet/OpenBullet2/tree/master/RuriLib.Http</RepositoryUrl>

<PackageTags>http; proxy; socks; client; custom;</PackageTags>

</PropertyGroup>

<ItemGroup>

<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />

</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RuriLib.Proxies\RuriLib.Proxies.csproj" />

</ItemGroup>

</Project>

Extensions/IListExtensions.cs

using System.Collections.Generic;

namespace RuriLib.Http.Extensions

static internal class IListExtensions

public static void Add(this IList<KeyValuePair<string, string>> list, string key, object value)

=> list.Add(new KeyValuePair<string, string>(key, value.ToString()));

Helpers/ChunkedDecoderOptimized.cs

using System;

using System.Buffers;

using System.IO;

using System.Text;

namespace RuriLib.Http.Helpers

internal class ChunkedDecoderOptimized : IDisposable

private long templength;

private static byte[] CRLF_Bytes = { 13, 10 };

// private long remaningchunklength;


private bool Isnewchunk = true;

// private AutoResetEvent manualResetEvent = new AutoResetEvent(true);

public Stream DecodedStream { get; private set; }

public bool Finished { get; private set; }

public ChunkedDecoderOptimized()

DecodedStream = new MemoryStream(1024);

internal void Decode(ref ReadOnlySequence<byte> buff) => ParseNewChunk(ref buff);

private void ParseNewChunk(ref ReadOnlySequence<byte> buff)

if (Isnewchunk)

templength = GetChunkLength(ref buff);

Isnewchunk = false;

if (templength == 0 && buff.Length >= 2)

Finished = true;

buff = buff.Slice(2);//skip last crlf

return;

else if (templength == -1)

Isnewchunk = true;
return;

if (buff.Length > templength + 2)

var chunk = buff.Slice(buff.Start, templength);

WritetoStream(chunk);

Isnewchunk = true;

buff = buff.Slice(chunk.End);

buff = buff.Slice(2); //skip CRLF

ParseNewChunk(ref buff);

private int GetChunkLength(ref ReadOnlySequence<byte> buff)

if (buff.IsSingleSegment)

var index = -1;

var span = buff.FirstSpan;

index = span.IndexOf(CRLF_Bytes);

if (index != -1)

var line = span.Slice(0, index);

var pos = line.IndexOf((byte)';');

if (pos != -1)

line = line.Slice(0, pos);

}
buff = buff.Slice(index + 2);

return Convert.ToInt32(Encoding.ASCII.GetString(line), 16);

else

// Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}");

return -1;

else

SequenceReader<byte> reader = new SequenceReader<byte>(buff);

if (reader.TryReadTo(out ReadOnlySpan<byte> line, CRLF_Bytes.AsSpan(), true))

var pos = line.IndexOf((byte)';');

if (pos > 0)

line = line.Slice(0, pos);

buff = buff.Slice(reader.Position);

return Convert.ToInt32(Encoding.ASCII.GetString(line), 16);

else

// Console.WriteLine($"error payload: {Encoding.ASCII.GetString(buff.FirstSpan)}");

return -1;

}
}

private void WritetoStream(ReadOnlySequence<byte> buff)

if (buff.IsSingleSegment)

DecodedStream.Write(buff.FirstSpan);

else

foreach (var seg in buff)

DecodedStream.Write(seg.Span);

public void Dispose() => DecodedStream?.Dispose();

Helpers/ContentHelper.cs

using System;

using System.Linq;

namespace RuriLib.Http.Helpers

static internal class ContentHelper

{
//https://github.com/dotnet/corefx/blob/3e72ee5971db5d0bd46606fa672969adde29e307/src/Syste

private static readonly string[] contentHeaders = new []

"Last-Modified",

"Expires",

"Content-Type",

"Content-Range",

"Content-MD5",

"Content-Location",

"Content-Length",

"Content-Language",

"Content-Encoding",

"Allow"

};

public static bool IsContentHeader(string name) => contentHeaders.Any(h => h.Equals(name, Str

Helpers/SpanHelpers.cs

using System;

namespace RuriLib.Http.Helpers

static internal class SpanHelpers

private static readonly byte[] CRLF_Bytes = { 13, 10 };

public static LineSplitEnumerator SplitLines(this Span<byte> span)


{

// LineSplitEnumerator is a struct so there is no allocation here

return new LineSplitEnumerator(span);

public static LineSplitEnumerator SplitLines(this ReadOnlySpan<byte> span)

// LineSplitEnumerator is a struct so there is no allocation here

return new LineSplitEnumerator(span);

// Must be a ref struct as it contains a ReadOnlySpan<char>

public ref struct LineSplitEnumerator

private ReadOnlySpan<byte> _span;

public LineSplitEntry Current { get; private set; }

public LineSplitEnumerator(ReadOnlySpan<byte> span)

_span = span;

Current = default;

// Needed to be compatible with the foreach operator

public LineSplitEnumerator GetEnumerator() => this;

public bool MoveNext()

var span = _span;


if (span.Length == 0) // Reach the end of the string

return false;

var index = span.IndexOf(CRLF_Bytes);

if (index == -1) // The string is composed of only one line

_span = ReadOnlySpan<byte>.Empty; // The remaining string is an empty string

Current = new LineSplitEntry(span, ReadOnlySpan<byte>.Empty);

return true;

else

Current = new LineSplitEntry(span.Slice(0, index), span.Slice(index, 2));

_span = span.Slice(index + 2);

return true;

public readonly ref struct LineSplitEntry

public LineSplitEntry(ReadOnlySpan<byte> line, ReadOnlySpan<byte> separator)

Line = line;

Separator = separator;

}
public ReadOnlySpan<byte> Line { get; }

public ReadOnlySpan<byte> Separator { get; }

// This method allow to deconstruct the type, so you can write any of the following code

// foreach (var entry in str.SplitLines()) { _ = entry.Line; }

// foreach (var (line, endOfLine) in str.SplitLines()) { _ = line; }

// https://docs.microsoft.com/en-us/dotnet/csharp/deconstruct#deconstructing-user-defined-type

public void Deconstruct(out ReadOnlySpan<byte> line, out ReadOnlySpan<byte> separator)

line = Line;

separator = Separator;

// This method allow to implicitly cast the type into a ReadOnlySpan<byte>, so you can write th

// foreach (ReadOnlySpan<byte> entry in str.SplitLines())

public static implicit operator ReadOnlySpan<byte>(LineSplitEntry entry) => entry.Line;

Models/HttpRequest.cs

using RuriLib.Http.Extensions;

using System;

using System.Collections.Generic;

using System.IO;

using System.Linq;

using System.Net.Http;

using System.Text;

using System.Threading;
using System.Threading.Tasks;

namespace RuriLib.Http.Models

/// <summary>

/// An HTTP request that can be sent using a <see cref="RLHttpClient"/>.

/// </summary>

public class HttpRequest : IDisposable

/// <summary>

/// Whether to write the absolute URI in the first line of the request instead of

/// the relative path (e.g. https://example.com/abc instead of /abc)

/// </summary>

public bool AbsoluteUriInFirstLine { get; set; } = false;

/// <summary>

/// The HTTP version to use.

/// </summary>

public Version Version { get; set; } = new(1, 1);

/// <summary>

/// The HTTP method to use.

/// </summary>

public HttpMethod Method { get; set; } = HttpMethod.Get;

/// <summary>

/// The URI of the remote resource.

/// </summary>

public Uri Uri { get; set; }


/// <summary>

/// The cookies to send inside the Cookie header of this request.

/// </summary>

public Dictionary<string, string> Cookies { get; set; } = new();

/// <summary>

/// The headers of this request.

/// </summary>

public Dictionary<string, string> Headers { get; set; } = new();

/// <summary>

/// The content of this request.

/// </summary>

public HttpContent Content { get; set; }

/// <summary>

/// Gets the raw bytes that will be sent on the network stream.

/// </summary>

/// <param name="cancellationToken">The token to cancel the operation</param>

public async Task<byte[]> GetBytesAsync(CancellationToken cancellationToken = default)

using var ms = new MemoryStream();

ms.Write(Encoding.ASCII.GetBytes(BuildFirstLine()));

ms.Write(Encoding.ASCII.GetBytes(BuildHeaders()));

if (Content != null)

ms.Write(await Content.ReadAsByteArrayAsync(cancellationToken));

}
return ms.ToArray();

private static readonly string newLine = "\r\n";

/// <summary>

/// Safely adds a header to the dictionary.

/// </summary>

public void AddHeader(string name, string value)

// Make sure Host is written properly otherwise it won't get picked up below

if (name.Equals("Host", StringComparison.OrdinalIgnoreCase))

Headers["Host"] = value;

else

Headers[name] = value;

// Builds the first line, for example

// GET /resource HTTP/1.1

private string BuildFirstLine()

if (Version >= new Version(2, 0))

throw new Exception($"HTTP/{Version.Major}.{Version.Minor} not supported yet");

return $"{Method.Method} {(AbsoluteUriInFirstLine ? Uri.AbsoluteUri : Uri.PathAndQuery)} HTT

}
// Builds the headers, for example

// Host: example.com

// Connection: Close

private string BuildHeaders()

// NOTE: Do not use AppendLine because it appends \n instead of \r\n

// on Unix-like systems.

var sb = new StringBuilder();

var finalHeaders = new List<KeyValuePair<string, string>>();

// Add the Host header if not already provided

if (!HeaderExists("Host", out _))

finalHeaders.Add("Host", Uri.Host);

// If there is no Connection header, add it

if (!HeaderExists("Connection", out var connectionHeaderName))

finalHeaders.Add("Connection", "Close");

// Add the non-content headers

foreach (var header in Headers)

finalHeaders.Add(header);

// Add the Cookie header if not set manually and container not null
if (!HeaderExists("Cookie", out _) && Cookies.Any())

var cookieBuilder = new StringBuilder();

foreach (var cookie in Cookies)

cookieBuilder

.Append($"{cookie.Key}={cookie.Value}; ");

// Remove the last ; and space if not empty

if (cookieBuilder.Length > 2)

cookieBuilder.Remove(cookieBuilder.Length - 2, 2);

finalHeaders.Add("Cookie", cookieBuilder);

// Add the content headers

if (Content != null)

foreach (var header in Content.Headers)

// If it was already set, skip

if (!HeaderExists(header.Key, out _))

finalHeaders.Add(header.Key, string.Join(' ', header.Value));

}
}

// Add the Content-Length header if not already present

if (!finalHeaders.Any(h => h.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreC

var contentLength = Content.Headers.ContentLength;

if (contentLength.HasValue && contentLength.Value > 0)

finalHeaders.Add("Content-Length", contentLength);

// Write all non-empty headers to the StringBuilder

foreach (var header in finalHeaders.Where(h => !string.IsNullOrEmpty(h.Value)))

sb

.Append(header.Key)

.Append(": ")

.Append(header.Value)

.Append(newLine);

// Write the final blank line after all headers

sb.Append(newLine);

return sb.ToString();

/// <summary>
/// Checks whether a header that matches a given <paramref name="name"/> exists. If it exists,

/// its original name will be written to <paramref name="actualName"/>.

/// </summary>

public bool HeaderExists(string name, out string actualName)

var key = Headers.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreC

actualName = key;

return key != null;

/// <inheritdoc/>

public void Dispose() => Content?.Dispose();

Models/HttpResponse.cs

using System;

using System.Collections.Generic;

using System.Net;

using System.Net.Http;

namespace RuriLib.Http.Models

/// <summary>

/// An HTTP response obtained with a <see cref="RLHttpClient"/>.

/// </summary>

public class HttpResponse : IDisposable

/// <summary>
/// The request that retrieved this response.

/// </summary>

public HttpRequest Request { get; set; }

/// <summary>

/// The HTTP version.

/// </summary>

public Version Version { get; set; } = new(1, 1);

/// <summary>

/// The status code of the response.

/// </summary>

public HttpStatusCode StatusCode { get; set; }

/// <summary>

/// The headers of the response.

/// </summary>

public Dictionary<string, string> Headers { get; set; } = new(StringComparer.InvariantCultureIgnor

/// <summary>

/// The content of the response.

/// </summary>

public HttpContent Content { get; set; }

/// <inheritdoc/>

public void Dispose() => Content?.Dispose();

}
File: /mnt/data/RuriLib_Tests_Documentation.pdf
ExportDescriptors.cs

using Newtonsoft.Json;

using System.IO;

using Xunit;

namespace RuriLib.Tests

public class ExportDescriptors

[Fact]

public void ExportAllDescriptors()

var settings = new JsonSerializerSettings

Formatting = Formatting.Indented

};

var json = JsonConvert.SerializeObject(

Globals.DescriptorsRepository.Descriptors, settings);

File.WriteAllText("descriptors.json", json);

RuriLib.Tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>

</PropertyGroup>

<ItemGroup>

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />

<PackageReference Include="xunit" Version="2.8.1" />

<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">

<PrivateAssets>all</PrivateAssets>

<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

</PackageReference>

<PackageReference Include="coverlet.collector" Version="6.0.2">

<PrivateAssets>all</PrivateAssets>

<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

</PackageReference>

</ItemGroup>

<ItemGroup>

<ProjectReference Include="..\RuriLib\RuriLib.csproj" />

</ItemGroup>

</Project>

Extensions/StringExtensionsTests.cs

using System.Runtime.InteropServices;

using RuriLib.Extensions;

using Xunit;

namespace RuriLib.Tests.Extensions

{
public class StringExtensionsTests

[Fact]

public void PadLeftToNearestMultiple_NormalTest_Pad()

var padded = "ABCD".PadLeftToNearestMultiple(8, 'E');

Assert.Equal("EEEEABCD", padded);

[Fact]

public void WithEnding_NormalTest_CorrectResult()

Assert.Equal("hello", "hel".WithEnding("llo"));

[Fact]

public void RightMostCharacters_NormalTest_CorrectResult()

Assert.Equal("llo", "hello".RightMostCharacters(3));

[Theory]

[InlineData("C:/test/dir/file.txt")]

[InlineData("C:\\test\\dir\\file.txt")]

[InlineData("file.txt")]

[InlineData("./file.txt")]

[InlineData("Test/file.txt")]

public void IsSubPathOf_ValidSubPath_True(string path)

{
Assert.True(path.IsSubPathOf("C:/test/"));

[Theory]

[InlineData("/home/user/file.txt")]

[InlineData("file.txt")]

[InlineData("./file.txt")]

[InlineData("Test/file.txt")]

public void IsSubPathOf_ValidSubPathUnix_True(string path)

Assert.True(path.IsSubPathOf("/home/user/"));

[Theory]

[InlineData("C:/windows/system32/cmd.exe")]

[InlineData("C:/test/../windows/system32/cmd.exe")]

[InlineData("../../windows/system32/cmd.exe")]

public void IsSubPathOf_IllegalSubPath_False(string path)

// Skip if not on Windows

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

return;

Assert.False(path.IsSubPathOf("C:/test/"));

[Theory]
[InlineData("/opt/test")]

[InlineData("/home/user/../../root")]

[InlineData("../../root")]

public void IsSubPathOf_IllegalSubPathUnix_False(string path)

Assert.False(path.IsSubPathOf("/home/user/"));

Functions/Conversion/Base64ConverterTests.cs

using RuriLib.Functions.Conversion;

using System.Text;

using Xunit;

namespace RuriLib.Tests.Functions.Conversion

public class Base64ConverterTests

[Fact]

public void ToByteArray_DottedString_RemoveDotsAndDecode()

byte[] bytes = Base64Converter.ToByteArray("aGVsbG8gaG93I.GFyZSB5b3U/");

Assert.Equal("hello how are you?", Encoding.UTF8.GetString(bytes));

Functions/Conversion/BinaryConverterTests.cs
using RuriLib.Functions.Conversion;

using Xunit;

namespace RuriLib.Tests.Functions.Conversion

public class BinaryConverterTests

private readonly string fourCharactersString = "1101";

private readonly string eightCharactersString = "00101101";

[Fact]

public void ToByteArray_BackAndForth_NoChange()

byte[] bytes = BinaryConverter.ToByteArray(eightCharactersString);

string hex = BinaryConverter.ToBinaryString(bytes);

Assert.Equal(eightCharactersString, hex, true);

[Fact]

public void ToByteArray_FourCharacterString_AutoPad()

byte[] bytes = BinaryConverter.ToByteArray(fourCharactersString);

Assert.Equal(new byte[] { 13 }, bytes);

Functions/Conversion/HexConverterTests.cs

using RuriLib.Functions.Conversion;
using Xunit;

namespace RuriLib.Tests.Functions.Conversion

public class HexConverterTests

private readonly string threeCharactersString = "A16";

private readonly string fourCharactersString = "A16F";

[Fact]

public void ToByteArray_BackAndForth_NoChange()

byte[] bytes = HexConverter.ToByteArray(fourCharactersString);

string hex = HexConverter.ToHexString(bytes);

Assert.Equal(fourCharactersString, hex, true);

[Fact]

public void ToByteArray_ThreeCharacterString_AutoPad()

byte[] bytes = HexConverter.ToByteArray(threeCharactersString);

Assert.Equal(new byte[] { 0xA, 0x16 }, bytes);

Functions/Conversion/SizeConverterTests.cs

using RuriLib.Functions.Conversion;

using Xunit;
namespace RuriLib.Tests.Functions.Conversion

public class SizeConverterTests

[Fact]

public void ToReadableSize_GigaBytes_PrintSize()

Assert.Equal("4.12 GB", SizeConverter.ToReadableSize(4123456789, false, false, 2));

[Fact]

public void ToReadableSize_GibiBytes_PrintSize()

Assert.Equal("3.8 GiB", SizeConverter.ToReadableSize(4123456789, false, true, 1));

[Fact]

public void ToReadableSize_GigaBits_PrintSize()

Assert.Equal("32.98 Gbit", SizeConverter.ToReadableSize(4123456789, true, false, 2));

[Fact]

public void ToReadableSize_GibiBits_PrintSize()

Assert.Equal("30.722 Gibit", SizeConverter.ToReadableSize(4123456789, true, true, 3));

}
Functions/Crypto/AWS4SignatureTests.cs

using RuriLib.Functions.Conversion;

using System;

using System.Security.Cryptography;

using System.Text;

using Xunit;

namespace RuriLib.Tests.Functions.Crypto

public class AWS4SignatureTests

[Fact]

public void Aws4SignatureGenerator_ValidInputs_ValidOutput()

const string key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";

const string dateStamp = "20120215";

const string regionName = "us-east-1";

const string serviceName = "iam";

var signature = RuriLib.Functions.Crypto.Crypto.AWS4Encrypt(key, dateStamp, regionName, s

Assert.Equal("f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d", H

Functions/Crypto/AesTests.cs

using System;

using System.Security.Cryptography;

using System.Text;
using Xunit;

namespace RuriLib.Tests.Functions.Crypto

public class AesTests

[Fact]

public void AesEncrypt_ValidInputs_ValidOutput()

var input = Encoding.UTF8.GetBytes("test");

var key = Convert.FromBase64String("WGxReE43YTUxODFiMDdjalhsUXhON2E1MTgxYjA3Y

var iv = Convert.FromBase64String("bjVKSk0wNEZONGtRMTc4ZA==");

var encrypted = RuriLib.Functions.Crypto.Crypto.AESEncrypt(input, key, iv, CipherMode.CBC,

Assert.Equal("Yu5IU/XZS3tMjw2m5p4Pgw==", Convert.ToBase64String(encrypted));

Functions/Files/FileUtilsTests.cs

using RuriLib.Functions.Files;

using System.IO;

using Xunit;

namespace RuriLib.Tests.Functions.Files

public class FileUtilsTests

[Fact]

public void GetFirstAvailableFileName_ValidFileName_ReturnSame()


{

var file = Path.GetRandomFileName();

Assert.Equal(file, FileUtils.GetFirstAvailableFileName(file));

[Fact]

public void GetFirstAvailableFileName_OneFileWithSameName_Add1()

var file = Path.GetTempFileName();

var fileWithOne = file.Replace(".tmp", "1.tmp");

Assert.Equal(fileWithOne, FileUtils.GetFirstAvailableFileName(file));

[Fact]

public void GetFirstAvailableFileName_TwoFilesWithSameName_Add2()

var file = Path.GetTempFileName();

var fileWithOne = file.Replace(".tmp", "1.tmp");

var fileWithTwo = file.Replace(".tmp", "2.tmp");

File.Create(fileWithOne);

Assert.Equal(fileWithTwo, FileUtils.GetFirstAvailableFileName(file));

Functions/Http/HttpTests.cs

using RuriLib.Functions.Http;
using RuriLib.Logging;

using RuriLib.Models.Bots;

using RuriLib.Models.Configs;

using RuriLib.Models.Data;

using RuriLib.Models.Environment;

using System;

using System.Collections.Generic;

using System.Text;

using Xunit;

using RuriLib.Blocks.Requests.Http;

using System.Threading.Tasks;

using Newtonsoft.Json;

using RuriLib.Tests.Utils;

using RuriLib.Models.Blocks.Custom.HttpRequest.Multipart;

using System.IO;

using RuriLib.Tests.Utils.Mockup;

using RuriLib.Functions.Http.Options;

namespace RuriLib.Tests.Functions.Http

public class HttpTests

private static BotData NewBotData() => new(

new(null)

ProxySettings = new MockedProxySettingsProvider(),

Security = new MockedSecurityProvider()

},
new ConfigSettings(),

new BotLogger(),

new DataLine("", new WordlistType()),

null,

false);

private readonly string httpBin = "https://httpbin.org/anything";

[Theory]

[InlineData(HttpLibrary.RuriLibHttp)]

[InlineData(HttpLibrary.SystemNet)]

public async Task HttpRequestStandard_Get_Verify(HttpLibrary library)

var data = NewBotData();

var cookies = new Dictionary<string, string>

{ "name1", "value1" },

{ "name2", "value2" }

};

var headers = new Dictionary<string, string>

{ "Custom", "value" }

};

var options = new StandardHttpRequestOptions

Url = httpBin,

Method = HttpMethod.GET,
HttpLibrary = library,

CustomHeaders = headers,

CustomCookies = cookies

};

await Methods.HttpRequestStandard(data, options);

var response = JsonConvert.DeserializeObject<HttpBinResponse>(data.SOURCE);

Assert.Equal("value", response.Headers["Custom"]);

Assert.Equal("httpbin.org", response.Headers["Host"]);

Assert.Equal("name1=value1; name2=value2", response.Headers["Cookie"]);

Assert.Equal("GET", response.Method);

Assert.Equal(httpBin, response.Url);

[Theory]

[InlineData(HttpLibrary.RuriLibHttp)]

[InlineData(HttpLibrary.SystemNet)]

public async Task HttpRequestStandard_Post_Verify(HttpLibrary library)

var data = NewBotData();

var options = new StandardHttpRequestOptions

Url = httpBin,

Method = HttpMethod.POST,

HttpLibrary = library,

Content = "name1=value1&name2=value2",

ContentType = "application/x-www-form-urlencoded"
};

await Methods.HttpRequestStandard(data, options);

var response = JsonConvert.DeserializeObject<HttpBinResponse>(data.SOURCE);

Assert.Equal("POST", response.Method);

Assert.Equal("value1", response.Form["name1"]);

Assert.Equal("value2", response.Form["name2"]);

Assert.Equal("application/x-www-form-urlencoded", response.Headers["Content-Type"]);

[Theory]

[InlineData(HttpLibrary.RuriLibHttp)]

[InlineData(HttpLibrary.SystemNet)]

public async Task HttpRequestRaw_Post_Verify(HttpLibrary library)

var data = NewBotData();

var options = new RawHttpRequestOptions

Url = httpBin,

Method = HttpMethod.POST,

HttpLibrary = library,

Content = Encoding.UTF8.GetBytes("name1=value1&name2=value2"),

ContentType = "application/x-www-form-urlencoded"

};

await Methods.HttpRequestRaw(data, options);

var response = JsonConvert.DeserializeObject<HttpBinResponse>(data.SOURCE);

Assert.Equal("POST", response.Method);
Assert.Equal("value1", response.Form["name1"]);

Assert.Equal("value2", response.Form["name2"]);

Assert.Equal("application/x-www-form-urlencoded", response.Headers["Content-Type"]);

[Theory]

[InlineData(HttpLibrary.RuriLibHttp)]

[InlineData(HttpLibrary.SystemNet)]

public async Task HttpRequestBasicAuth_Normal_Verify(HttpLibrary library)

var data = NewBotData();

var options = new BasicAuthHttpRequestOptions

Url = httpBin,

Method = HttpMethod.GET,

HttpLibrary = library,

Username = "myUsername",

Password = "myPassword"

};

await Methods.HttpRequestBasicAuth(data, options);

var response = JsonConvert.DeserializeObject<HttpBinResponse>(data.SOURCE);

Assert.Equal("GET", response.Method);

Assert.Equal("Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes("myUsername:myP

[Theory]

[InlineData(HttpLibrary.RuriLibHttp)]
[InlineData(HttpLibrary.SystemNet)]

public async Task HttpRequestMultipart_Post_Verify(HttpLibrary library)

var data = NewBotData();

var tempFile = Path.GetTempFileName();

File.WriteAllText(tempFile, "fileContent");

var contents = new List<MyHttpContent>

new StringHttpContent("stringName", "stringContent", "application/x-www-form-urlencoded")

new RawHttpContent("rawName", Encoding.UTF8.GetBytes("rawContent"), "application/octe

new FileHttpContent("fileName", tempFile, "application/octet-stream")

};

var options = new MultipartHttpRequestOptions

Url = httpBin,

Method = HttpMethod.POST,

HttpLibrary = library,

Boundary = "myBoundary",

Contents = contents

};

await Methods.HttpRequestMultipart(data, options);

var response = JsonConvert.DeserializeObject<HttpBinResponse>(data.SOURCE);

Assert.Equal("POST", response.Method);

Assert.Equal("stringContent", response.Form["stringName"]);

Assert.Equal("rawContent", response.Form["rawName"]);
Assert.Equal("fileContent", response.Files["fileName"]);

/*

// Test for future implementation of HTTP/2.0

[Fact]

public async Task HttpRequestStandard_Http2_Verify()

var data = NewBotData();

var options = new StandardHttpRequestOptions

Url = "https://http2.golang.org/reqinfo",

Method = HttpMethod.GET,

HttpVersion = "2.0"

};

await Methods.HttpRequestStandard(data, options);

Assert.Contains("Protocol: HTTP/2.0", data.SOURCE);

*/

Functions/Interop/IronPyTests.cs

using IronPython.Compiler;

using IronPython.Hosting;

using IronPython.Runtime;

using Microsoft.Scripting.Hosting;
using System;

using System.Collections.Generic;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Interop

public class IronPyTests

private readonly ScriptEngine engine;

public IronPyTests()

var runtime = Python.CreateRuntime();

engine = runtime.GetEngine("py");

var pco = (PythonCompilerOptions)engine.GetCompilerOptions();

pco.Module &= ~ModuleOptions.Optimized;

[Fact]

public void ExecuteIronPyScript_IntegerSum_ReturnInteger()

var scope = engine.CreateScope();

scope.SetVariable("x", 3);

scope.SetVariable("y", 5);

var code = engine.CreateScriptSourceFromString("result = x + y");

code.Execute(scope);

Assert.Equal(8, scope.GetVariable<int>("result"));

}
[Fact]

public void ExecuteIronPyScript_FloatSum_ReturnFloat()

var scope = engine.CreateScope();

scope.SetVariable("x", 3.5f);

scope.SetVariable("y", 5.2f);

var code = engine.CreateScriptSourceFromString("result = x + y");

code.Execute(scope);

Assert.Equal(8.7f, scope.GetVariable<float>("result"));

[Fact]

public void ExecuteIronPyScript_StringChain_ReturnString()

var scope = engine.CreateScope();

scope.SetVariable("x", "my");

scope.SetVariable("y", "string");

var code = engine.CreateScriptSourceFromString("result = x + y");

code.Execute(scope);

Assert.Equal("mystring", scope.GetVariable<string>("result"));

[Fact]

public void ExecuteIronPyScript_OutputList_ReturnList()

var scope = engine.CreateScope();

scope.SetVariable("x", "a");

scope.SetVariable("y", "b");
var code = engine.CreateScriptSourceFromString("result = [ x, y ]");

code.Execute(scope);

var outputList = scope.GetVariable<IList<object>>("result").Cast<string>().ToList();

Assert.Equal(2, outputList.Count);

Assert.Equal("a", outputList[0]);

Assert.Equal("b", outputList[1]);

[Fact]

public void ExecuteIronPyScript_InputList_ReturnString()

var scope = engine.CreateScope();

scope.SetVariable("x", new List<string> { "a", "b" });

var code = engine.CreateScriptSourceFromString("result = x[0]");

code.Execute(scope);

Assert.Equal("a", scope.GetVariable<string>("result"));

[Fact]

public void ExecuteIronPyScript_NoOutputs_ReturnNothing()

var scope = engine.CreateScope();

scope.SetVariable("x", 1);

var code = engine.CreateScriptSourceFromString("y = x + 1");

code.Execute(scope);

Assert.Throws<MissingMemberException>(() => scope.GetVariable<string>("result"));

}
}

Functions/Interop/JintTests.cs

using Jint;

using RuriLib.Extensions;

using System.Collections.Generic;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Interop

public class JintTests

[Fact]

public void InvokeJint_IntegerSum_ReturnInteger()

var engine = new Engine();

engine.SetValue("x", 3);

engine.SetValue("y", 5);

engine.Execute("var result = x + y;");

Assert.Equal(8, engine.Global.GetProperty("result").Value.AsNumber().ToInt());

[Fact]

public void InvokeJint_FloatSum_ReturnFloat()

var engine = new Engine();

engine.SetValue("x", 3.5f);

engine.SetValue("y", 5.2f);
engine.Execute("var result = x + y;");

Assert.Equal(8.7f, engine.Global.GetProperty("result").Value.AsNumber().ToSingle());

[Fact]

public void InvokeJint_StringChain_ReturnString()

var engine = new Engine();

engine.SetValue("x", "my");

engine.SetValue("y", "string");

engine.Execute("var result = x + y;");

Assert.Equal("mystring", engine.Global.GetProperty("result").Value.AsString());

[Fact]

public void InvokeJint_OutputList_ReturnList()

var engine = new Engine();

engine.SetValue("x", "a");

engine.SetValue("y", "b");

engine.Execute("var result = [ x, y ];");

var outputList = engine.Global.GetProperty("result").Value.AsArray().GetEnumerator().ToEnum

Assert.Equal(2, outputList.Count);

Assert.Equal("a", outputList[0]);

Assert.Equal("b", outputList[1]);

[Fact]

public void InvokeJint_InputList_ReturnString()


{

var engine = new Engine();

engine.SetValue("x", new List<string> { "a", "b" });

engine.Execute("var result = x[0];");

Assert.Equal("a", engine.Global.GetProperty("result").Value.AsString());

[Fact]

public void InvokeJint_NoOutputs_ReturnNothing()

var engine = new Engine();

engine.SetValue("x", 1);

engine.Execute("var y = x + 1;");

Assert.Null(engine.Global.GetProperty("result").Value);

Functions/Interop/NodeJSTests.cs

using Jering.Javascript.NodeJS;

using System.Collections.Generic;

using System.Linq;

using System.Text.Json;

using System.Text.RegularExpressions;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Tests.Functions.Interop

{
public class NodeJsTests

private static string BuildScript(string innerScript, string[] inputs, string[] outputs)

return @$"module.exports = (callback, {MakeInputs(inputs)}) => {{

{innerScript}

var noderesult = {{

{MakeNodeObject(outputs)}

}};

callback(null, noderesult);

}}";

private static string MakeNodeObject(string[] outputs)

=> string.Join("\r\n", outputs.Select(o => $" '{o}': {o},"));

private static string MakeInputs(string[] inputs)

=> string.Join(",", inputs.Select(i => Regex.Match(i, "[A-Za-z0-9]+$")));

[Fact]

public async Task InvokeNode_IntegerSum_ReturnInteger()

var script = BuildScript("var result = x + y;", ["x", "y"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Equal(8, result.GetProperty("result").GetInt32());

[Fact]

public async Task InvokeNode_FloatSum_ReturnFloat()

{
var script = BuildScript("var result = x + y;", ["x", "y"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Equal(8.7f, result.GetProperty("result").GetSingle());

[Fact]

public async Task InvokeNode_BoolAnd_ReturnBool()

var script = BuildScript("var result = x && y;", ["x", "y"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.False(result.GetProperty("result").GetBoolean());

[Fact]

public async Task InvokeNode_StringChain_ReturnString()

var script = BuildScript("var result = x + y;", ["x", "y"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Equal("mystring", result.GetProperty("result").GetString());

[Fact]

public async Task InvokeNode_OutputList_ReturnList()

var script = BuildScript("var result = [ x, y ];", ["x", "y"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

var outputList = result.GetProperty("result").EnumerateArray().Select(e => e.GetString()).ToLis

Assert.Equal(2, outputList.Count);

Assert.Equal("a", outputList[0]);
Assert.Equal("b", outputList[1]);

[Fact]

public async Task InvokeNode_InputList_ReturnString()

List<string> inputList = ["a", "b"];

var script = BuildScript("var result = x[0];", ["x"], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Equal("a", result.GetProperty("result").GetString());

[Fact]

public async Task InvokeNode_NoInputs_ReturnString()

var script = BuildScript("var result = 'hello';", [], ["result"]);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Equal("hello", result.GetProperty("result").GetString());

[Fact]

public async Task InvokeNode_NoOutputs_ReturnNothing()

var script = BuildScript("var result = x + y;", ["x", "y"], []);

var result = await StaticNodeJSService.InvokeFromStringAsync<JsonElement>(script, null, nul

Assert.Throws<KeyNotFoundException>(() => result.GetProperty("result"));

}
Functions/Networking/DnsLookupTests.cs

using RuriLib.Functions.Networking;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Tests.Functions.Networking

public class DnsLookupTests

[Fact]

public async Task FromGoogle_MailDotCom_MXRecords()

var entries = await DnsLookup.FromGoogleAsync("mail.com", "MX");

Assert.Contains("mx00.mail.com", entries);

Assert.Contains("mx01.mail.com", entries);

Functions/Parsing/HtmlParserTests.cs

using RuriLib.Functions.Parsing;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Parsing

public class HtmlParserTests

{
private readonly string htmlPage = @"

<html>

<body>

<p>text <strong>123</strong></p>

<ul>

<li><p id='testid' testattr='testvalue'>innertext1</p></li>

<li><p>innertext2</p></li>

<ul/>

</body>

</html>

";

// CSS Selector

[Fact]

public void QueryAttributeAll_SingleElementQueryInnerHTML_GetText()

var match = HtmlParser.QueryAttributeAll(htmlPage, "#testid", "innerHTML").FirstOrDefault();

Assert.Equal("innertext1", match);

[Fact]

public void QueryAttributeAll_SingleElementQueryOuterHTML_GetText()

var match = HtmlParser.QueryAttributeAll(htmlPage, "strong", "outerHTML").FirstOrDefault();

Assert.Equal("<strong>123</strong>", match);

[Fact]

public void QueryAttributeAll_SingleElementQueryInnerText_GetText()


{

var match = HtmlParser.QueryAttributeAll(htmlPage, "body p", "innerText").FirstOrDefault();

Assert.Equal("text 123", match);

[Fact]

public void QueryAttributeAll_SingleElementNonExistant_MatchNothing()

var match = HtmlParser.QueryAttributeAll(htmlPage, "#nonexistant", "innerHTML").FirstOrDefa

Assert.Null(match);

[Fact]

public void QueryAttributeAll_SingleElementQueryAttribute_GetText()

var match = HtmlParser.QueryAttributeAll(htmlPage, "#testid", "testattr").FirstOrDefault();

Assert.Equal("testvalue", match);

[Fact]

public void QueryAttributeAll_ManyElementsQueryAttribute_GetText()

var matches = HtmlParser.QueryAttributeAll(htmlPage, "ul li p", "innerHTML").ToArray();

Assert.Equal(new string[] { "innertext1", "innertext2" }, matches);

// XPATH

[Fact]

public void QueryXPathAll_SingleElementQueryInnerHTML_GetText()


{

var match = HtmlParser.QueryXPathAll(htmlPage, "//p[@id='testid']", "innerHTML").FirstOrDefa

Assert.Equal("innertext1", match);

[Fact]

public void QueryXPathAll_SingleElementQueryOuterHTML_GetText()

var match = HtmlParser.QueryXPathAll(htmlPage, "//strong", "outerHTML").FirstOrDefault();

Assert.Equal("<strong>123</strong>", match);

[Fact]

public void QueryXPathAll_SingleElementQueryInnerText_GetText()

var match = HtmlParser.QueryXPathAll(htmlPage, "//body/p", "innerText").FirstOrDefault();

Assert.Equal("text 123", match);

[Fact]

public void QueryXPathAll_SingleElementNonExistant_MatchNothing()

var match = HtmlParser.QueryXPathAll(htmlPage, "//p[@id='nonexistant']", "innerHTML").FirstO

Assert.Null(match);

[Fact]

public void QueryXPathAll_SingleElementQueryAttribute_GetText()

var match = HtmlParser.QueryXPathAll(htmlPage, "//p[@id='testid']", "testattr").FirstOrDefault()


Assert.Equal("testvalue", match);

[Fact]

public void QueryXPathAll_ManyElementsQueryAttribute_GetText()

var matches = HtmlParser.QueryXPathAll(htmlPage, "//ul/li/p", "innerHTML").ToArray();

Assert.Equal(new string[] { "innertext1", "innertext2" }, matches);

Functions/Parsing/JsonParserTests.cs

using RuriLib.Functions.Parsing;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Parsing

public class JsonParserTests

private readonly string jsonObject = "{ \"key\": \"value\" }";

private readonly string jsonArray = "[ \"elem1\", \"elem2\" ]";

[Fact]

public void GetValuesByKey_GetStringFromObject_ReturnValue()

var match = JsonParser.GetValuesByKey(jsonObject, "key").FirstOrDefault();

Assert.Equal("value", match);
}

[Fact]

public void GetValuesByKey_MissingKey_MatchNothing()

var match = JsonParser.GetValuesByKey(jsonObject, "dummy").FirstOrDefault();

Assert.Null(match);

[Fact]

public void GetValuesByKey_GetStringsFromArray_ReturnValues()

var match = JsonParser.GetValuesByKey(jsonArray, "[*]").ToArray();

Assert.Equal(new string[] { "elem1", "elem2" }, match);

Functions/Parsing/LRParserTests.cs

using RuriLib.Functions.Parsing;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Parsing

public class LRParserTests

private readonly string oneLineString = "The cat is on the table";

private readonly string oneLineTwoMatchesString = "The cat is fat, the dog is lazy";
[Fact]

public void ParseBetweenStrings_OneLineStringCaseSensitive_GetText()

string match = LRParser.ParseBetween(oneLineString, "The ", " is").FirstOrDefault();

Assert.Equal("cat", match);

[Fact]

public void ParseBetweenStrings_OneLineStringCaseSensitive_MatchNothing()

string match = LRParser.ParseBetween(oneLineString, "THE ", " IS").FirstOrDefault();

Assert.Null(match);

[Fact]

public void ParseBetweenStrings_OneLineStringCaseInsensitive_GetText()

string match = LRParser.ParseBetween(oneLineString, "THE ", " IS", false).FirstOrDefault();

Assert.Equal("cat", match);

[Fact]

public void ParseBetweenStrings_OneLineStringManyMatches_GetText()

var matches = LRParser.ParseBetween(oneLineTwoMatchesString, "the ", " is", false).ToArray

Assert.Equal(new string[] { "cat", "dog" }, matches);

[Fact]

public void ParseBetweenStrings_LeftDelimEmpty_ParseFromBeginning()


{

string match = LRParser.ParseBetween(oneLineString, string.Empty, " is").FirstOrDefault();

Assert.Equal("The cat", match);

[Fact]

public void ParseBetweenStrings_RightDelimEmpty_ParseUntilEnd()

string match = LRParser.ParseBetween(oneLineString, "is ", string.Empty).FirstOrDefault();

Assert.Equal("on the table", match);

[Fact]

public void ParseBetweenStrings_BothDelimsEmpty_ParseEntireInput()

string match = LRParser.ParseBetween(oneLineString, string.Empty, string.Empty).FirstOrDefa

Assert.Equal(oneLineString, match);

[Fact]

public void ParseBetweenStrings_LeftDelimNotFound_MatchNothing()

string match = LRParser.ParseBetween(oneLineString, "John", "table").FirstOrDefault();

Assert.Null(match);

[Fact]

public void ParseBetweenStrings_RightDelimNotFound_MatchNothing()

{
string match = LRParser.ParseBetween(oneLineString, "cat", "John").FirstOrDefault();

Assert.Null(match);

[Fact]

public void ParseBetweenStrings_BothDelimsNotFound_MatchNothing()

string match = LRParser.ParseBetween(oneLineString, "John", "Mary").FirstOrDefault();

Assert.Null(match);

Functions/Parsing/RegexParserTests.cs

using RuriLib.Functions.Parsing;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Functions.Parsing

public class RegexParserTests

private readonly string oneLineString = "The cat is on the table";

[Fact]

public void MatchGroupsToString_SingleMatch_GetFullMatch()

var match = RegexParser.MatchGroupsToString(oneLineString, @"The ([^ ]*) is", "[0]").FirstOrD

Assert.Equal("The cat is", match);

}
[Fact]

public void MatchGroupsToString_SingleMatch_GetGroups()

var match = RegexParser.MatchGroupsToString(oneLineString, @"The ([^ ]*) is on the (.*)$", "

Assert.Equal("cat - table", match);

[Fact]

public void MatchGroupsToString_SingleMatchEmptyOutputFormat_ReturnEmptyString()

var match = RegexParser.MatchGroupsToString(oneLineString, @"The ([^ ]*) is", string.Empty

Assert.Equal(string.Empty, match);

[Fact]

public void MatchGroupsToString_NoMatches_MatchNothing()

var match = RegexParser.MatchGroupsToString(oneLineString, @"John went to the (.*)$", "[0]

Assert.Null(match);

Functions/Time/TimeConverterTests.cs

using RuriLib.Functions.Time;

using System;

using Xunit;

namespace RuriLib.Tests.Functions.Time
{

public class TimeConverterTests

private readonly DateTime recentDate = new DateTime(2020, 4, 18, 0, 0, 0, DateTimeKind.Utc);

private readonly long recentUnixTimeSeconds = 1587168000;

private readonly string recentISO8601Date = "2020-04-18T00:00:00.000Z";

[Fact]

public void ToUnixTime_Seconds_OutputSeconds()

long unixTime = recentDate.ToUnixTime();

Assert.Equal(recentUnixTimeSeconds, unixTime);

[Fact]

public void ToUnixTime_Milliseconds_OutputMilliseconds()

long unixTime = recentDate.ToUnixTime(true);

Assert.Equal(recentUnixTimeSeconds * 1000, unixTime);

[Fact]

public void ToDateTimeUtc_Seconds_OutputCorrectDate()

DateTime dateTime = recentUnixTimeSeconds.ToDateTimeUtc();

Assert.Equal(recentDate.ToUniversalTime(), dateTime);

[Fact]
public void ToDateTimeUtc_Milliseconds_OutputCorrectDate()

DateTime dateTime = (recentUnixTimeSeconds * 1000).ToDateTimeUtc(true);

Assert.Equal(recentDate.ToUniversalTime(), dateTime);

[Fact]

public void ToDateTime_FullTimeString_OutputCorrectDate()

DateTime dateTime = recentISO8601Date.ToDateTime("yyyy-MM-ddTHH:mm:ss.fffZ");

Assert.Equal(recentDate, dateTime);

[Fact]

public void ToISO8601_Normal_OutputISO8601String()

string iso = recentDate.ToISO8601();

Assert.Equal(recentISO8601Date, iso);

Helpers/PauseTokenSourceTests.cs

using RuriLib.Helpers;

using System;

using System.Diagnostics;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Tests.Helpers
{

public class PauseTokenSourceTests

// This test is temporarily disabled until I figure out why it works perfectly fine on a

// local machine but fails on half the GitHub Actions runs...

// [Fact]

public async Task PauseIfRequestedAsync_Paused_Wait()

var pts = new PauseTokenSource();

var sw = new Stopwatch();

sw.Start();

// Start a task that takes 2000 ms to complete

var task = Task.Run(async () =>

// Loop 10 times (200 ms each) and pause on each iteration if needed

for (var i = 0; i < 10; i++)

await Task.Delay(200);

await pts.Token.PauseIfRequestedAsync();

});

// Wait 300 ms, then pause the task for 1 second, then resume

await Task.Delay(300);

await pts.PauseAsync();

await Task.Delay(1000);

await pts.ResumeAsync();
// Wait until the task completes

await task.WaitAsync(TimeSpan.FromSeconds(5));

sw.Stop();

var elapsed = sw.Elapsed;

// Make sure the elapsed time is over 2500 ms

Assert.True(elapsed > TimeSpan.FromMilliseconds(2500));

Helpers/StepperTests.cs

using RuriLib.Helpers;

using System;

using System.Diagnostics;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Tests.Helpers

public class StepperTests

// This test is temporarily disabled until I figure out why it works perfectly fine on a

// local machine but fails on half the GitHub Actions runs...

// [Fact]

public async Task TryTakeStep_StepperWaiting_Proceed()

var stepper = new Stepper();


var sw = new Stopwatch();

sw.Start();

// Start a task that would take 1000 ms to complete, but waits

// for a step twice

var task = Task.Run(async () =>

await stepper.WaitForStepAsync();

await Task.Delay(500);

await stepper.WaitForStepAsync();

await Task.Delay(500);

});

// Wait 300 ms, then take the first step

await Task.Delay(300);

var tookStep = stepper.TryTakeStep();

Assert.True(tookStep);

// Wait 800 ms (500 + 300), then take the last step

await Task.Delay(800);

tookStep = stepper.TryTakeStep();

Assert.True(tookStep);

// Wait until the task completes

await task.WaitAsync(TimeSpan.FromSeconds(5));

sw.Stop();

var elapsed = sw.Elapsed;


// Make sure the elapsed time is over 1500 ms

Assert.True(elapsed > TimeSpan.FromMilliseconds(1500));

Helpers/VariableNamesTests.cs

using RuriLib.Helpers;

using System;

using Xunit;

namespace RuriLib.Tests.Helpers

public class VariableNamesTests

[Fact]

public void MakeValid_Null_Exception()

Assert.Throws<ArgumentNullException>(() => VariableNames.MakeValid(null));

[Fact]

public void MakeValid_Empty_Random()

Assert.True(VariableNames.IsValid(VariableNames.MakeValid(string.Empty)));

[Fact]

public void MakeValid_AllInvalid_Random()

{
Assert.True(VariableNames.IsValid(VariableNames.MakeValid("$<&/")));

[Fact]

public void MakeValid_SomeInvalid_OnlyValid()

Assert.Equal("hello", VariableNames.MakeValid("!h£ello?"));

[Fact]

public void MakeValid_StartingNumber_StartingUnderscore()

Assert.Equal("_2pac", VariableNames.MakeValid("2pac"));

[Fact]

public void MakeValid_Dots_AllValid()

Assert.Equal("data.SOURCE", VariableNames.MakeValid("data.SOURCE"));

Helpers/CSharp/CSharpWriterTests.cs

using RuriLib.Helpers.CSharp;

using Xunit;

namespace RuriLib.Tests.Helpers.CSharp

public class CSharpWriterTests


{

[Fact]

public void SerializeInterpString_Alone_ReplaceCorrectly()

Assert.Equal("$\"{value}\"", CSharpWriter.SerializeInterpString("<value>"));

[Fact]

public void SerializeInterpString_Surrounded_ReplaceCorrectly()

Assert.Equal("$\"my {value} is cool\"", CSharpWriter.SerializeInterpString("my <value> is cool")

[Fact]

public void SerializeInterpString_SingleCharacter_ReplaceCorrectly()

Assert.Equal("$\"my {a} is cool\"", CSharpWriter.SerializeInterpString("my <a> is cool"));

Helpers/LoliCode/LoliCodeParserTests.cs

using RuriLib.Helpers.Blocks;

using RuriLib.Helpers.LoliCode;

using RuriLib.Models.Blocks;

using RuriLib.Models.Blocks.Settings;

using RuriLib.Models.Blocks.Settings.Interpolated;

using Xunit;
namespace RuriLib.Tests.Helpers.LoliCode

public class LoliCodeParserTests

[Fact]

public void ParseLiteral_NormalLiteral_Parse()

var input = "\"hello\" how are you";

Assert.Equal("hello", LineParser.ParseLiteral(ref input));

Assert.Equal("how are you", input);

[Fact]

public void ParseLiteral_LiteralWithEscapedQuotes_Parse()

var input = "\"I \\\"escape\\\" quotes\"";

Assert.Equal("I \"escape\" quotes", LineParser.ParseLiteral(ref input));

[Fact]

public void ParseLiteral_LiteralWithDoubleEscaping_Parse()

var input = "\"I \\\\\"don't\\\\\" escape quotes\"";

Assert.Equal("I \\", LineParser.ParseLiteral(ref input));

[Fact]

public void ParseInt_NormalInt_Parse()

{
var input = "42 is the answer";

Assert.Equal(42, LineParser.ParseInt(ref input));

Assert.Equal("is the answer", input);

[Fact]

public void ParseFloat_NormalFloat_Parse()

var input = "3.14 is not pi";

Assert.Equal(3.14f, LineParser.ParseFloat(ref input));

Assert.Equal("is not pi", input);

[Fact]

public void ParseList_EmptyList_Parse()

var input = "[] such emptiness";

Assert.Empty(LineParser.ParseList(ref input));

Assert.Equal("such emptiness", input);

[Fact]

public void ParseList_EmptyListWithSpacesInside_Parse()

var input = "[ ]";

Assert.Empty(LineParser.ParseList(ref input));

[Fact]

public void ParseList_SingleElementList_Parse()


{

var input = "[\"one\"]";

Assert.Equal(new string[] { "one" }, LineParser.ParseList(ref input).ToArray());

[Fact]

public void ParseList_MultiElementList_Parse()

var input = "[\"one\", \"two\", \"three\"]";

Assert.Equal(new string[] { "one", "two", "three" }, LineParser.ParseList(ref input).ToArray());

[Fact]

public void ParseByteArray_NormalBase64_Parse()

var input = "/wA= is my byte array";

Assert.Equal(new byte[] { 0xFF, 0x00 }, LineParser.ParseByteArray(ref input));

Assert.Equal("is my byte array", input);

[Fact]

public void ParseBool_NormalBool_Parse()

var input = "True indeed";

Assert.True(LineParser.ParseBool(ref input));

Assert.Equal("indeed", input);

[Fact]
public void ParseDictionary_EmptyDictionary_Parse()

var input = "{} such emptiness";

Assert.Empty(LineParser.ParseDictionary(ref input));

Assert.Equal("such emptiness", input);

[Fact]

public void ParseDictionary_EmptyDictionaryWithSpacesInside_Parse()

var input = "{ }";

Assert.Empty(LineParser.ParseDictionary(ref input));

[Fact]

public void ParseDictionary_SingleEntry_Parse()

var input = "{ (\"key1\", \"value1\") }";

var dict = LineParser.ParseDictionary(ref input);

Assert.Equal("value1", dict["key1"]);

[Fact]

public void ParseDictionary_MultiEntry_Parse()

var input = "{ (\"key1\", \"value1\"), (\"key2\", \"value2\") }";

var dict = LineParser.ParseDictionary(ref input);

Assert.Equal("value1", dict["key1"]);

Assert.Equal("value2", dict["key2"]);
}

[Fact]

public void ParseSetting_FixedStringSetting_Parse()

var block = BlockFactory.GetBlock<AutoBlockInstance>("ConstantString");

var valueSetting = block.Settings["value"];

var input = "value = \"myValue\"";

LoliCodeParser.ParseSetting(ref input, block.Settings, block.Descriptor);

Assert.Equal(SettingInputMode.Fixed, valueSetting.InputMode);

Assert.IsType<StringSetting>(valueSetting.FixedSetting);

Assert.Equal("myValue", (valueSetting.FixedSetting as StringSetting).Value);

[Fact]

public void ParseSetting_Variable_Parse()

var block = BlockFactory.GetBlock<AutoBlockInstance>("ConstantString");

var valueSetting = block.Settings["value"];

var input = "value = @myVariable";

LoliCodeParser.ParseSetting(ref input, block.Settings, block.Descriptor);

Assert.Equal(SettingInputMode.Variable, valueSetting.InputMode);

Assert.Equal("myVariable", valueSetting.InputVariableName);

[Fact]

public void ParseSetting_InterpolatedString_Parse()

{
var block = BlockFactory.GetBlock<AutoBlockInstance>("ConstantString");

var valueSetting = block.Settings["value"];

var input = "value = $\"my <interp> string\"";

LoliCodeParser.ParseSetting(ref input, block.Settings, block.Descriptor);

Assert.Equal(SettingInputMode.Interpolated, valueSetting.InputMode);

Assert.IsType<InterpolatedStringSetting>(valueSetting.InterpolatedSetting);

Assert.Equal("my <interp> string", ((InterpolatedStringSetting)valueSetting.InterpolatedSetting)

Helpers/Transpilers/TranspilerTests.cs

using System;

using RuriLib.Helpers.Transpilers;

using Xunit;

namespace RuriLib.Tests.Helpers.Transpilers;

public class TranspilerTests

[Fact]

public void Transpile_LoliStackLoliRoundTrip_SameScript()

var nl = Environment.NewLine;

var script = $"#CodeLabel{nl}if(a < b) {{{nl}BLOCK:ConstantString{nl} => VAR @outputVar{nl}EN

var stack = Loli2StackTranspiler.Transpile(script);

var newScript = Stack2LoliTranspiler.Transpile(stack);

Assert.Equal(script, newScript);
}

Models/Blocks/AutoBlockInstanceTests.cs

using System;

using RuriLib.Helpers.Blocks;

using RuriLib.Models.Blocks;

using RuriLib.Models.Blocks.Settings;

using RuriLib.Models.Blocks.Settings.Interpolated;

using RuriLib.Models.Configs;

using Xunit;

namespace RuriLib.Tests.Models.Blocks;

public class AutoBlockInstanceTests

private readonly string _nl = Environment.NewLine;

[Fact]

public void ToLC_ParseLRBlock_OutputScript()

var block = BlockFactory.GetBlock<AutoBlockInstance>("Substring");

block.OutputVariable = "myOutput";

block.IsCapture = false;

block.Disabled = true;

block.Label = "My Label";

var input = block.Settings["input"];

var index = block.Settings["index"];

var length = block.Settings["length"];


input.InputMode = SettingInputMode.Variable;

input.InputVariableName = "myInput";

index.InputMode = SettingInputMode.Fixed;

(index.FixedSetting as IntSetting)!.Value = 3;

length.InputMode = SettingInputMode.Fixed;

var expected = $"DISABLED{_nl}LABEL:My Label{_nl} input = @myInput{_nl} index = 3{_nl} =>

Assert.Equal(expected, block.ToLC());

[Fact]

public void FromLC_ParseLRBlock_BuildBlock()

var block = BlockFactory.GetBlock<AutoBlockInstance>("Substring");

var script = $"DISABLED{_nl}LABEL:My Label{_nl} input = @myInput{_nl} index = 3{_nl} => CA

int lineNumber = 0;

block.FromLC(ref script, ref lineNumber);

Assert.True(block.Disabled);

Assert.Equal("My Label", block.Label);

Assert.Equal("myOutput", block.OutputVariable);

Assert.True(block.IsCapture);

var input = block.Settings["input"];

var index = block.Settings["index"];

Assert.Equal(SettingInputMode.Variable, input.InputMode);

Assert.Equal("myInput", input.InputVariableName);

Assert.Equal(SettingInputMode.Fixed, index.InputMode);
Assert.Equal(3, (index.FixedSetting as IntSetting)!.Value);

[Fact]

public void ToCSharp_SyncReturnValue_OutputScript()

var block = BlockFactory.GetBlock<AutoBlockInstance>("Substring");

block.OutputVariable = "myOutput";

var input = block.Settings["input"];

var index = block.Settings["index"];

var length = block.Settings["length"];

input.InputMode = SettingInputMode.Variable;

input.InputVariableName = "myInput";

index.InputMode = SettingInputMode.Fixed;

(index.FixedSetting as IntSetting)!.Value = 3;

length.InputMode = SettingInputMode.Fixed;

(length.FixedSetting as IntSetting)!.Value = 5;

var expected = $"string myOutput = Substring(data, myInput.AsString(), 3, 5);{_nl}data.LogVariab

Assert.Equal(expected, block.ToCSharp([], new ConfigSettings()));

[Fact]

public void ToCSharp_SyncReturnValueCapture_OutputScript()

var block = BlockFactory.GetBlock<AutoBlockInstance>("Substring");

block.OutputVariable = "myOutput";
block.IsCapture = true;

var input = block.Settings["input"];

var index = block.Settings["index"];

var length = block.Settings["length"];

input.InputMode = SettingInputMode.Variable;

input.InputVariableName = "myInput";

index.InputMode = SettingInputMode.Fixed;

(index.FixedSetting as IntSetting)!.Value = 3;

length.InputMode = SettingInputMode.Fixed;

(length.FixedSetting as IntSetting)!.Value = 5;

var expected = $"string myOutput = Substring(data, myInput.AsString(), 3, 5);{_nl}data.LogVariab

Assert.Equal(expected, block.ToCSharp([], new ConfigSettings()));

[Fact]

public void ToCSharp_AsyncNoReturnValue_OutputScript()

var block = BlockFactory.GetBlock<AutoBlockInstance>("TcpConnect");

var url = block.Settings["host"];

var port = block.Settings["port"];

var ssl = block.Settings["useSSL"];

var timeout = block.Settings["timeoutMilliseconds"];

(url.FixedSetting as StringSetting)!.Value = "example.com";

(port.FixedSetting as IntSetting)!.Value = 80;

(ssl.FixedSetting as BoolSetting)!.Value = false;


(timeout.FixedSetting as IntSetting)!.Value = 1000;

var expected = $"await TcpConnect(data, \"example.com\", 80, false, 1000).ConfigureAwait(false

Assert.Equal(expected, block.ToCSharp([], new ConfigSettings()));

[Fact]

public void ToCSharp_SyncReturnValueAlreadyDeclared_OutputScript()

var block = BlockFactory.GetBlock<AutoBlockInstance>("Substring");

block.OutputVariable = "myOutput";

var input = block.Settings["input"];

var index = block.Settings["index"];

var length = block.Settings["length"];

input.InputMode = SettingInputMode.Interpolated;

input.InterpolatedSetting = new InterpolatedStringSetting { Value = "my <interp> string" };

index.InputMode = SettingInputMode.Fixed;

(index.FixedSetting as IntSetting)!.Value = 3;

length.InputMode = SettingInputMode.Variable;

length.InputVariableName = "myLength";

var expected = $"myOutput = Substring(data, $\"my {{interp}} string\", 3, myLength.AsInt());{_nl}da

Assert.Equal(expected, block.ToCSharp(["myOutput"], new ConfigSettings()));

Models/Blocks/Custom/HttpRequestBlockInstanceTests.cs

using RuriLib.Helpers.Blocks;
using RuriLib.Models.Blocks.Custom;

using RuriLib.Models.Blocks.Custom.HttpRequest;

using RuriLib.Models.Blocks.Custom.HttpRequest.Multipart;

using RuriLib.Models.Blocks.Settings;

using RuriLib.Models.Configs;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Xunit;

namespace RuriLib.Tests.Models.Blocks.Custom

public class HttpRequestBlockInstanceTests

/*

[Fact]

public void ToLC_StandardPost_OutputScript()

var repo = new DescriptorsRepository();

var descriptor = repo.GetAs<HttpRequestBlockDescriptor>("HttpRequest");

var block = new HttpRequestBlockInstance(descriptor);

var url = block.Settings["url"];

url.InputMode = SettingInputMode.Fixed;

(url.FixedSetting as StringSetting).Value = "https://example.com";

var method = block.Settings["method"];

method.InputMode = SettingInputMode.Fixed;
(method.FixedSetting as EnumSetting).Value = "POST";

block.RequestParams = new StandardRequestParams

Content = new BlockSetting { FixedSetting = new StringSetting { Value = "key1=value1&key2

ContentType = new BlockSetting { FixedSetting = new StringSetting { Value = "application/x-

};

var expected = " url = \"https://example.com\"\r\n method = POST\r\n TYPE:STANDARD\r\n

Assert.Equal(expected, block.ToLC());

[Fact]

public void ToLC_MultipartPost_OutputScript()

var repo = new DescriptorsRepository();

var descriptor = repo.GetAs<HttpRequestBlockDescriptor>("HttpRequest");

var block = new HttpRequestBlockInstance(descriptor);

var url = block.Settings["url"];

url.InputMode = SettingInputMode.Fixed;

(url.FixedSetting as StringSetting).Value = "https://example.com";

var method = block.Settings["method"];

method.InputMode = SettingInputMode.Fixed;

(method.FixedSetting as EnumSetting).Value = "POST";

block.RequestParams = new MultipartRequestParams

Boundary = new BlockSetting { InputMode = SettingInputMode.Variable, InputVariableName


Contents = new List<HttpContentSettingsGroup>

new StringHttpContentSettingsGroup

Name = new BlockSetting { FixedSetting = new StringSetting { Value = "stringName" } }

Data = new BlockSetting { FixedSetting = new StringSetting { Value = "stringContent" }

ContentType = new BlockSetting { FixedSetting = new StringSetting { Value = "stringCo

},

new FileHttpContentSettingsGroup

Name = new BlockSetting { FixedSetting = new StringSetting { Value = "fileName" } },

FileName = new BlockSetting { FixedSetting = new StringSetting { Value = "file.txt" } },

ContentType = new BlockSetting { FixedSetting = new StringSetting { Value = "fileConte

},

};

var expected = " url = \"https://example.com\"\r\n method = POST\r\n TYPE:MULTIPART\r\n

Assert.Equal(expected, block.ToLC());

[Fact]

public void FromLC_MultipartPost_BuildBlock()

var block = BlockFactory.GetBlock<HttpRequestBlockInstance>("HttpRequest");

var script = " url = \"https://example.com\"\r\n method = POST\r\n TYPE:MULTIPART\r\n @m

int lineNumber = 0;

block.FromLC(ref script, ref lineNumber);


var url = block.Settings["url"];

Assert.Equal("https://example.com", (url.FixedSetting as StringSetting).Value);

var method = block.Settings["method"];

Assert.Equal("POST", (method.FixedSetting as EnumSetting).Value);

Assert.IsType<MultipartRequestParams>(block.RequestParams);

var multipart = (MultipartRequestParams)block.RequestParams;

Assert.Equal(SettingInputMode.Variable, multipart.Boundary.InputMode);

Assert.Equal("myBoundary", multipart.Boundary.InputVariableName);

var content = multipart.Contents[0];

Assert.IsType<StringHttpContentSettingsGroup>(content);

var stringContent = (StringHttpContentSettingsGroup)content;

Assert.Equal("stringName", (stringContent.Name.FixedSetting as StringSetting).Value);

Assert.Equal("stringContent", (stringContent.Data.FixedSetting as StringSetting).Value);

Assert.Equal("stringContentType", (stringContent.ContentType.FixedSetting as StringSetting).V

content = multipart.Contents[1];

Assert.IsType<FileHttpContentSettingsGroup>(content);

var fileContent = (FileHttpContentSettingsGroup)content;

Assert.Equal("fileName", (fileContent.Name.FixedSetting as StringSetting).Value);

Assert.Equal("file.txt", (fileContent.FileName.FixedSetting as StringSetting).Value);

Assert.Equal("fileContentType", (fileContent.ContentType.FixedSetting as StringSetting).Value

[Fact]

public void ToCSharp_MultipartPost_OutputScript()

var block = BlockFactory.GetBlock<HttpRequestBlockInstance>("HttpRequest");


var script = " url = \"https://example.com\"\r\n method = POST\r\n TYPE:MULTIPART\r\n @m

int lineNumber = 0;

block.FromLC(ref script, ref lineNumber);

var headers = block.Settings["customHeaders"];

(headers.FixedSetting as DictionaryOfStringsSetting).Value.Clear();

string expected = "await HttpRequestMultipart(data, \"https://example.com\", RuriLib.Functions

Assert.Equal(expected, block.ToCSharp(new List<string>(), new ConfigSettings()));

*/

Models/Blocks/Custom/KeycheckBlockInstanceTests.cs

using RuriLib.Helpers.Blocks;

using RuriLib.Models.Blocks.Custom;

using RuriLib.Models.Blocks.Custom.Keycheck;

using RuriLib.Models.Blocks.Settings;

using RuriLib.Models.Conditions.Comparisons;

using RuriLib.Models.Configs;

using System.Collections.Generic;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Models.Blocks.Custom

public class KeycheckBlockInstanceTests

/*
[Fact]

public void ToLC_NormalBlock_OutputScript()

var repo = new DescriptorsRepository();

var descriptor = repo.GetAs<KeycheckBlockDescriptor>("Keycheck");

var block = new KeycheckBlockInstance(descriptor);

var banIfNoMatch = block.Settings["banIfNoMatch"];

banIfNoMatch.InputMode = SettingInputMode.Fixed;

(banIfNoMatch.FixedSetting as BoolSetting).Value = false;

block.Disabled = true;

block.Label = "My Label";

block.Keychains = new List<Keychain>

new Keychain

ResultStatus = "SUCCESS",

Mode = KeychainMode.OR,

Keys = new List<Key>

new StringKey

Left = new BlockSetting

InputMode = SettingInputMode.Variable,

InputVariableName = "myString"

},
Comparison = StrComparison.Contains,

Right = new BlockSetting

FixedSetting = new StringSetting { Value = "abc" }

},

new FloatKey

Left = new BlockSetting

FixedSetting = new FloatSetting { Value = 3f }

},

Comparison = NumComparison.GreaterThan,

Right = new BlockSetting

FixedSetting = new FloatSetting { Value = 1.5f }

},

new Keychain

ResultStatus = "FAIL",

Mode = KeychainMode.AND,

Keys = new List<Key>

new ListKey
{

Left = new BlockSetting

InputMode = SettingInputMode.Variable,

InputVariableName = "myList"

},

Comparison = ListComparison.Contains,

Right = new BlockSetting

FixedSetting = new StringSetting { Value = "abc" }

},

new DictionaryKey

Left = new BlockSetting

InputMode = SettingInputMode.Variable,

InputVariableName = "myDict"

},

Comparison = DictComparison.HasKey,

Right = new BlockSetting

FixedSetting = new StringSetting { Value = "abc" }

};
var expected = "DISABLED\r\nLABEL:My Label\r\n banIfNoMatch = False\r\n KEYCHAIN SUC

Assert.Equal(expected, block.ToLC());

[Fact]

public void FromLC_NormalScript_BuildBlock()

var block = BlockFactory.GetBlock<KeycheckBlockInstance>("Keycheck");

var script = "DISABLED\r\nLABEL:My Label\r\n banIfNoMatch = False\r\n KEYCHAIN SUCCE

int lineNumber = 0;

block.FromLC(ref script, ref lineNumber);

Assert.True(block.Disabled);

Assert.Equal("My Label", block.Label);

BlockSetting left, right;

var banIfNoMatch = block.Settings["banIfNoMatch"];

Assert.False((banIfNoMatch.FixedSetting as BoolSetting).Value);

var kc1 = block.Keychains[0];

Assert.Equal("SUCCESS", kc1.ResultStatus);

Assert.Equal(KeychainMode.OR, kc1.Mode);

var k1 = kc1.Keys[0] as StringKey;

var k2 = kc1.Keys[1] as FloatKey;

left = k1.Left;

right = k1.Right;

Assert.Equal(SettingInputMode.Variable, left.InputMode);
Assert.Equal("myString", left.InputVariableName);

Assert.Equal(StrComparison.Contains, k1.Comparison);

Assert.Equal(SettingInputMode.Fixed, right.InputMode);

Assert.Equal("abc", (right.FixedSetting as StringSetting).Value);

left = k2.Left;

right = k2.Right;

Assert.Equal(SettingInputMode.Fixed, left.InputMode);

Assert.Equal(3F, (left.FixedSetting as FloatSetting).Value);

Assert.Equal(NumComparison.GreaterThan, k2.Comparison);

Assert.Equal(SettingInputMode.Fixed, right.InputMode);

Assert.Equal(1.5F, (right.FixedSetting as FloatSetting).Value);

var kc2 = block.Keychains[1];

Assert.Equal("FAIL", kc2.ResultStatus);

Assert.Equal(KeychainMode.AND, kc2.Mode);

var k3 = kc2.Keys[0] as ListKey;

var k4 = kc2.Keys[1] as DictionaryKey;

left = k3.Left;

right = k3.Right;

Assert.Equal(SettingInputMode.Variable, left.InputMode);

Assert.Equal("myList", left.InputVariableName);

Assert.Equal(ListComparison.Contains, k3.Comparison);

Assert.Equal(SettingInputMode.Fixed, right.InputMode);

Assert.Equal("abc", (right.FixedSetting as StringSetting).Value);

left = k4.Left;
right = k4.Right;

Assert.Equal(SettingInputMode.Variable, left.InputMode);

Assert.Equal("myDict", left.InputVariableName);

Assert.Equal(DictComparison.HasKey, k4.Comparison);

Assert.Equal(SettingInputMode.Fixed, right.InputMode);

Assert.Equal("abc", (right.FixedSetting as StringSetting).Value);

[Fact]

public void ToCSharp_NormalBlock_OutputScript()

var repo = new DescriptorsRepository();

var descriptor = repo.GetAs<KeycheckBlockDescriptor>("Keycheck");

var block = new KeycheckBlockInstance(descriptor);

var banIfNoMatch = block.Settings["banIfNoMatch"];

banIfNoMatch.InputMode = SettingInputMode.Variable;

banIfNoMatch.InputVariableName = "myBool";

block.Disabled = true;

block.Label = "My Label";

block.Keychains = new List<Keychain>

new Keychain

ResultStatus = "SUCCESS",

Mode = KeychainMode.OR,

Keys = new List<Key>

{
new StringKey

Left = BlockSettingFactory.CreateStringSetting("", "myString", SettingInputMode.Vari

Comparison = StrComparison.Contains,

Right = BlockSettingFactory.CreateStringSetting("", "abc", SettingInputMode.Fixed)

},

new FloatKey

Left = BlockSettingFactory.CreateFloatSetting("", 3F),

Comparison = NumComparison.GreaterThan,

Right = BlockSettingFactory.CreateFloatSetting("", 1.5F)

},

new Keychain

ResultStatus = "FAIL",

Mode = KeychainMode.AND,

Keys = new List<Key>

new ListKey

Left = BlockSettingFactory.CreateListOfStringsSetting("", "myList"),

Comparison = ListComparison.Contains,

Right = BlockSettingFactory.CreateStringSetting("", "abc", SettingInputMode.Fixed)

},

new DictionaryKey
{

Left = BlockSettingFactory.CreateDictionaryOfStringsSetting("", "myDict"),

Comparison = DictComparison.HasKey,

Right = BlockSettingFactory.CreateStringSetting("", "abc", SettingInputMode.Fixed)

};

string expected = "if (Conditions.Check(myString.AsString(), StrComparison.Contains, \"abc\")

Assert.Equal(expected, block.ToCSharp(new List<string>(), new ConfigSettings()));

[Fact]

public void ToCSharp_NoKeychains_OutputScript()

var repo = new DescriptorsRepository();

var descriptor = repo.GetAs<KeycheckBlockDescriptor>("Keycheck");

var block = new KeycheckBlockInstance(descriptor);

var banIfNoMatch = block.Settings["banIfNoMatch"];

banIfNoMatch.InputMode = SettingInputMode.Variable;

banIfNoMatch.InputVariableName = "myBool";

block.Disabled = true;

block.Label = "My Label";

block.Keychains = new List<Keychain> { };

string expected = "if (myBool.AsBool())\r\n { data.STATUS = \"BAN\"; return; }\r\n";

Assert.Equal(expected, block.ToCSharp(new List<string>(), new ConfigSettings()));


}

*/

Models/Data/DataLineTests.cs

using RuriLib.Models.Data;

using RuriLib.Models.Environment;

using RuriLib.Models.Variables;

using System.Collections.Generic;

using Xunit;

namespace RuriLib.Tests.Models.Data

public class DataLineTests

private readonly WordlistType wordlistType = new WordlistType

Name = "Default",

Slices = new string[] { "ONE", "TWO" },

Separator = ","

};

[Fact]

public void GetVariables_ViaWordlistType_Parse()

DataLine dataLine = new DataLine("good,day", wordlistType);

List<StringVariable> variables = dataLine.GetVariables();

Assert.Equal(2, variables.Count);
Assert.Equal("ONE", variables[0].Name);

Assert.Equal("TWO", variables[1].Name);

Assert.Equal("good", variables[0].AsString());

Assert.Equal("day", variables[1].AsString());

Models/Data/DataPoolTests.cs

using RuriLib.Models.Data;

using RuriLib.Models.Data.DataPools;

using System.Linq;

using Xunit;

namespace RuriLib.Tests.Models.Data

public class DataPoolTests

[Fact]

public void CombinationsDataPool_Constructor_GenerateCombinations()

DataPool pool = new CombinationsDataPool("AB", 2);

Assert.Equal(new string[] { "AA", "AB", "BA", "BB" }, pool.DataList.ToArray());

[Fact]

public void RangeDataPool_Constructor_GenerateRange()

DataPool pool = new RangeDataPool(5, 3, 5, true);


Assert.Equal(new string[] { "05", "10", "15" }, pool.DataList.ToArray());

[Fact]

public void InfinityDataPool_Constructor_GenerateMany()

DataPool pool = new InfiniteDataPool();

Assert.Equal(100, pool.DataList.Take(100).Count());

Models/Data/DataRuleTests.cs

using RuriLib.Models.Data.Rules;

using Xunit;

namespace RuriLib.Tests.Models.Data

public class DataRuleTests

[Fact]

public void IsSatisfied_Contains_True()

var rule = new SimpleDataRule

Comparison = StringRule.Contains,

StringToCompare = "abc",

Invert = false

};
Assert.True(rule.IsSatisfied("abcdef"));

[Fact]

public void IsSatisfied_ContainsInvert_False()

var rule = new SimpleDataRule

Comparison = StringRule.Contains,

StringToCompare = "abc",

Invert = true

};

Assert.False(rule.IsSatisfied("abcdef"));

[Fact]

public void IsSatisfied_ContainsCaseInsensitive_False()

var rule = new SimpleDataRule

Comparison = StringRule.Contains,

StringToCompare = "ABC",

CaseSensitive = false,

Invert = false

};

Assert.True(rule.IsSatisfied("abcdef"));

}
[Fact]

public void IsSatisfied_ContainsAll_True()

var rule = new SimpleDataRule

Comparison = StringRule.ContainsAll,

StringToCompare = "acf",

Invert = false

};

Assert.True(rule.IsSatisfied("abcdef"));

[Fact]

public void IsSatisfied_ContainsAny_True()

var rule = new SimpleDataRule

Comparison = StringRule.ContainsAny,

StringToCompare = "a78",

Invert = false

};

Assert.True(rule.IsSatisfied("abcdef"));

Models/Proxies/ProxyPoolTests.cs
using RuriLib.Models.Proxies;

using RuriLib.Models.Proxies.ProxySources;

using System.IO;

using System.Linq;

using System.Runtime.InteropServices;

using System.Text;

using System.Threading.Tasks;

using Xunit;

namespace RuriLib.Tests.Models.Proxies

public class ProxyPoolTests

[Fact]

public async Task RemoveDuplicates_ListWithDuplicates_ReturnDistinct()

ListProxySource source = new(new Proxy[]

new Proxy("127.0.0.1", 8000),

new Proxy("127.0.0.1", 8000)

});

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync();

pool.RemoveDuplicates();

Assert.Single(pool.Proxies);

}
[Fact]

public async Task GetProxy_Available_ReturnValidProxy()

ListProxySource source = new(new Proxy[]

new Proxy("127.0.0.1", 8000)

});

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync();

Assert.NotNull(pool.GetProxy());

[Fact]

public async Task GetProxy_AllBusy_ReturnNull()

ListProxySource source = new(new Proxy[]

new Proxy("127.0.0.1", 8000) { ProxyStatus = ProxyStatus.Busy }

});

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync();

Assert.Null(pool.GetProxy());

[Fact]

public async Task GetProxy_EvenBusy_ReturnValidProxy()

{
ListProxySource source = new(new Proxy[]

new Proxy("127.0.0.1", 8000) { ProxyStatus = ProxyStatus.Busy }

});

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync();

Assert.NotNull(pool.GetProxy(true));

[Fact]

public async Task GetProxy_MaxUses_ReturnNull()

ListProxySource source = new(new Proxy[]

new Proxy("127.0.0.1", 8000) { TotalUses = 3 }

});

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync();

Assert.Null(pool.GetProxy(true, 3));

[Fact(Timeout = 10000)]

public async Task GetProxy_BatchFile_ReturnValidProxy()

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

{
// Well, Only Windows contains Powershell.

return;

var tmpBatchFilePath = Path.GetTempFileName() + ".bat";

await File.WriteAllTextAsync(tmpBatchFilePath, @"@echo off

echo 127.0.0.1:1111

echo 127.0.0.1:2222

echo (Socks5)127.0.0.1:3333

", Encoding.UTF8);

using FileProxySource source = new(tmpBatchFilePath);

using var pool = new ProxyPool(new ProxySource[] { source });

await pool.ReloadAllAsync(false);

File.Delete(tmpBatchFilePath);

Assert.Equal(3, pool.Proxies.Count());

var proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(1111, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(2222, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(3333, proxy.Port);
Assert.Equal(ProxyType.Socks5, proxy.Type);

[Fact(Timeout = 10000)]

public async Task GetProxy_PowershellFile_ReturnValidProxy()

if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

// Well, Only Windows contains Powershell.

return;

var tmpBatchFilePath = Path.GetTempFileName() + ".ps1";

// Setting Execution Policy is needed both in the test and real-world use cases of the functional

// users can use "Set-ExecutionPolicy unrestricted -Scope CurrentUser" apply for all scripts.

var command = $"/c powershell -executionpolicy unrestricted \"${tmpBatchFilePath}\"";

System.Diagnostics.Process.Start("cmd.exe", command);

await File.WriteAllTextAsync(tmpBatchFilePath, @"

Write-Output 127.0.0.1:1111

Write-Output 127.0.0.1:2222

Write-Output ""(Socks5)127.0.0.1:3333""

", Encoding.UTF8);

using FileProxySource source = new(tmpBatchFilePath);

using var pool = new ProxyPool([source]);

await pool.ReloadAllAsync(false);

File.Delete(tmpBatchFilePath);

Assert.Equal(3, pool.Proxies.Count());
var proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(1111, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(2222, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(3333, proxy.Port);

Assert.Equal(ProxyType.Socks5, proxy.Type);

[Fact(Timeout = 10000, Skip = "Randomly fails on CI")]

public async Task GetProxy_BashFile_ReturnValidProxy()

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

// Well, Windows doesn't have bash by default.

return;

var tmpBashFilePath = Path.GetTempFileName() + ".sh";

await File.WriteAllTextAsync(tmpBashFilePath, @"#!/bin/bash

echo 127.0.0.1:1111

echo 127.0.0.1:2222

echo ""(Socks5)127.0.0.1:3333""
", Encoding.UTF8);

using FileProxySource source = new(tmpBashFilePath);

using var pool = new ProxyPool([source]);

await pool.ReloadAllAsync(false);

File.Delete(tmpBashFilePath);

Assert.Equal(3, pool.Proxies.Count());

var proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(1111, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(2222, proxy.Port);

proxy = pool.GetProxy();

Assert.NotNull(proxy);

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(3333, proxy.Port);

Assert.Equal(ProxyType.Socks5, proxy.Type);

Models/Proxies/ProxyTests.cs

using RuriLib.Models.Proxies;

using Xunit;

namespace RuriLib.Tests.Models.Proxies
{

public class ProxyTests

[Fact]

public void Parse_HostAndPort_ParseCorrectly()

var proxy = Proxy.Parse("127.0.0.1:8000");

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(8000, proxy.Port);

[Fact]

public void Parse_TypeHostAndPort_ParseType()

var proxy = Proxy.Parse("(socks5)127.0.0.1:8000");

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(8000, proxy.Port);

Assert.Equal(ProxyType.Socks5, proxy.Type);

[Fact]

public void Parse_HostPortUserPass_ParseCredentials()

var proxy = Proxy.Parse("127.0.0.1:8000:user:pass");

Assert.Equal("127.0.0.1", proxy.Host);

Assert.Equal(8000, proxy.Port);

Assert.Equal("user", proxy.Username);

Assert.Equal("pass", proxy.Password);
}

Utils/CurrentOS.cs

using System.Runtime.InteropServices;

namespace RuriLib.Tests.Utils

public static class CurrentOS

public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

public static bool IsOSX() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);

public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

Utils/HttpBinResponse.cs

using System.Collections.Generic;

namespace RuriLib.Tests.Utils

/// <summary>

/// The deserialized data of a httpbin.org/anything response.

/// </summary>

public class HttpBinResponse

public string Url { get; set; }

public string Method { get; set; }

public Dictionary<string, string> Headers { get; set; }


public Dictionary<string, string> Form { get; set; }

public Dictionary<string, string> Files { get; set; }

Utils/Mockup/MockedGeneralSettingsProvider.cs

using RuriLib.Providers.Proxies;

namespace RuriLib.Tests.Utils.Mockup

public class MockedGeneralSettingsProvider : IGeneralSettingsProvider

public bool VerboseMode => true;

public bool LogAllResults => true;

Utils/Mockup/MockedProxySettingsProvider.cs

using RuriLib.Providers.Proxies;

using System;

namespace RuriLib.Tests.Utils.Mockup

public class MockedProxySettingsProvider : IProxySettingsProvider

public TimeSpan ConnectTimeout => TimeSpan.FromSeconds(10);

public TimeSpan ReadWriteTimeout => TimeSpan.FromSeconds(10);

public bool ContainsBanKey(string text, out string matchedKey, bool caseSensitive = false)
{

matchedKey = null;

return false;

public bool ContainsRetryKey(string text, out string matchedKey, bool caseSensitive = false)

matchedKey = null;

return false;

Utils/Mockup/MockedSecurityProvider.cs

using RuriLib.Providers.Security;

using System.Security.Cryptography.X509Certificates;

namespace RuriLib.Tests.Utils.Mockup

public class MockedSecurityProvider : ISecurityProvider

public bool RestrictBlocksToCWD => false;

public X509RevocationMode X509RevocationMode { get; set; } = X509RevocationMode.NoCheck

}
Detailed Documentation and Explanations

### Detailed Documentation and Explanations

#### LICENSE
- **Content:** Contains the license information for the project, specifying the terms under which the code can be used, mod

#### ProxyClient.cs
- **Purpose:** Abstract class representing a proxy client. Provides the blueprint for creating a connection through different t
- **Key Methods:**
- `CreateConnectionAsync`: Abstract method to create a connection to a specified host and port.
- `ValidateEndpoint`: Validates the host and port.
- `ConnectSocketAsync`: Connects a socket to a specified host and port.

#### ProxySettings.cs
- **Purpose:** Class representing the settings for a proxy, including credentials, host, and port.

#### README.md
- **Content:** Markdown file describing the library, listing the available proxy clients and helper classes.

#### RuriLib.Proxies.csproj
- **Purpose:** Project file for the `RuriLib.Proxies` library, specifying the target framework and package references.

### Clients

#### HttpProxyClient.cs
- **Purpose:** Represents an HTTP proxy client.
- **Key Methods:**
- `CreateConnectionAsync`: Creates a connection through an HTTP proxy.

#### NoProxyClient.cs
- **Purpose:** Represents a direct connection without using a proxy.
- **Key Methods:**
- `CreateConnectionAsync`: Creates a direct connection to the host and port.

#### Socks4ProxyClient.cs
- **Purpose:** Represents a SOCKS4 proxy client.
- **Key Methods:**
- `CreateConnectionAsync`: Creates a connection through a SOCKS4 proxy.

#### Socks4aProxyClient.cs
- **Purpose:** Represents a SOCKS4a proxy client.
- **Key Methods:**
- `CreateConnectionAsync`: Creates a connection through a SOCKS4a proxy.

#### Socks5ProxyClient.cs
- **Purpose:** Represents a SOCKS5 proxy client.
- **Key Methods:**
- `CreateConnectionAsync`: Creates a connection through a SOCKS5 proxy.

### Exceptions

#### ProxyException.cs
- **Purpose:** Custom exception class for proxy-related errors.
- **Key Methods:**
- `ProxyException`: Constructors for creating instances of `ProxyException`.

### Helpers

#### HostHelper.cs
- **Purpose:** Helper class for handling host-related operations.
- **Key Methods:**
- `GetPortBytes`: Converts a port number to its byte representation.
- `GetIPAddressBytesAsync`: Gets the byte representation of an IP address.

#### PortHelper.cs
- **Purpose:** Helper class for handling port-related operations.
- **Key Methods:**
- `ValidateTcpPort`: Validates if a port number is within the valid range for TCP connections.
Block Descriptions
Block: Keycheck
Description: Modifies the bot's status by checking conditions
Category: Conditions
Parameters:
banIfNoMatch: Ban If No Match - None

Block: Http Request


Description: Performs an Http request and reads the response
Category: Http
Parameters:
url: Url - None
method: Method - None
autoRedirect: Auto Redirect - None
maxNumberOfRedirects: Max Number Of Redirects - None
readResponseContent: Read Response Content - None
urlEncodeContent: Url Encode Content - None
absoluteUriInFirstLine: Absolute Uri In First Line - None
httpLibrary: Http Library - None
securityProtocol: Security Protocol - None
useCustomCipherSuites: Use Custom Cipher Suites - None
alwaysSendContent: Always Send Content - None
decodeHtml: Decode Html - None
codePagesEncoding: Code Pages Encoding - None
customCipherSuites: Custom Cipher Suites - None
customCookies: Custom Cookies - None
customHeaders: Custom Headers - None
timeoutMilliseconds: Timeout Milliseconds - None
httpVersion: Http Version - None

Block: Parse
Description: Parses text from a string
Category: Parsing
Parameters:
input: Input - None
prefix: Prefix - None
suffix: Suffix - None
urlEncodeOutput: Url Encode Output - None
leftDelim: Left Delim - None
rightDelim: Right Delim - None
caseSensitive: Case Sensitive - None
cssSelector: Css Selector - None
attributeName: Attribute Name - None
xPath: X Path - None
jToken: J Token - None
pattern: Pattern - None
outputFormat: Output Format - None
multiLine: Multi Line - None

Block: Script
Description: This block can invoke a script in a different language, pass some variables and return some results.
Category: Interop
Parameters:
Block: Clear Cookies
Description: Clears the cookie jar used for HTTP requests
Category: Utility
Parameters:

Block: Delay
Description: Sleeps for a specified amount of milliseconds
Category: Utility
Parameters:
milliseconds: Milliseconds - None

Block: Get HWID


Description: Retrieves a unique hardware ID for the current machine
Category: Utility
Parameters:

Block: Svg To Png


Description: Converts an svg image to a byte array containing a png image
Category: Images
Parameters:
xml: Xml - None
width: Width - None
height: Height - None

Block: File Exists


Description: Checks if a file exists
Category: Files
Parameters:
path: Path - None

Block: File Read


Description: Reads the entire content of a file to a single string
Category: Files
Parameters:
path: Path - None
encoding: Encoding - None

Block: File Read Lines


Description: Reads all lines of a file
Category: Files
Parameters:
path: Path - None
encoding: Encoding - None

Block: File Read Bytes


Description: Reads all bytes of a file
Category: Files
Parameters:
path: Path - None

Block: File Write


Description: Writes a string to a file
Category: Files
Parameters:
path: Path - None
content: Content - None
encoding: Encoding - None

Block: File Write Lines


Description: Writes lines to a file
Category: Files
Parameters:
path: Path - None
lines: Lines - None
encoding: Encoding - None

Block: File Write Bytes


Description: Writes bytes to a file
Category: Files
Parameters:
path: Path - None
content: Content - None

Block: File Append


Description: Appends a string at the end of a file
Category: Files
Parameters:
path: Path - None
content: Content - None
encoding: Encoding - None

Block: File Append Lines


Description: Appends lines at the end of a file
Category: Files
Parameters:
path: Path - None
lines: Lines - None
encoding: Encoding - None

Block: File Copy


Description: Copies a file to a new location
Category: Files
Parameters:
originPath: Origin Path - None
destinationPath: Destination Path - None

Block: File Move


Description: Moves a file to a new location
Category: Files
Parameters:
originPath: Origin Path - None
destinationPath: Destination Path - None

Block: File Delete


Description: Deletes a file
Category: Files
Parameters:
path: Path - None

Block: Folder Exists


Description: Checks if a folder exists
Category: Files
Parameters:
path: Path - None

Block: Create Path


Description: Creates a directory in the given path
Category: Files
Parameters:
path: Path - None

Block: Get Files In Folder


Description: Gets the paths to all files in a specific folder
Category: Files
Parameters:
path: Path - None

Block: Folder Delete


Description: Deletes a given directory
Category: Files
Parameters:
path: Path - None

Block: Hex => Bytes


Description: Converts a hex string to a byte array
Category: Conversion
Parameters:
hexString: Hex String - None
addPadding: Add Padding - None

Block: Bytes => Hex


Description: Converts a byte array to a hex string
Category: Conversion
Parameters:
bytes: Bytes - None

Block: Base64 => Bytes


Description: Converts a base64 string to a byte array
Category: Conversion
Parameters:
base64String: Base64 String - None
urlEncoded: Url Encoded - None

Block: Bytes => Base64


Description: Converts a byte array to a base64 string
Category: Conversion
Parameters:
bytes: Bytes - None
urlEncoded: Url Encoded - None

Block: Big Integer => Bytes


Description: Converts a (big) integer to a byte array
Category: Conversion
Parameters:
bigInteger: Big Integer - None
Block: Bytes => Big Integer
Description: Converts a byte array to a (big) integer
Category: Conversion
Parameters:
bytes: Bytes - None

Block: Binary String => Bytes


Description: Converts a binary string to a byte array
Category: Conversion
Parameters:
binaryString: Binary String - None
addPadding: Add Padding - None

Block: Bytes => Binary String


Description: Converts a byte array to a binary string
Category: Conversion
Parameters:
bytes: Bytes - None

Block: UTF8 => Base64


Description: Converts a UTF8 string to a base64 string
Category: Conversion
Parameters:
input: Input - None

Block: Base64 => UTF8


Description: Converts a base64 string to a UTF8 string
Category: Conversion
Parameters:
input: Input - None

Block: String => Bytes


Description: Converts an encoded string to a byte array
Category: Conversion
Parameters:
input: Input - None
encoding: Encoding - None

Block: Bytes => String


Description: Converts a byte array to an encoded string
Category: Conversion
Parameters:
input: Input - None
encoding: Encoding - None

Block: Readable Size


Description: Converts a long number representing bytes into a readable string like 4.5 Gbit or 2.14 Ki
Category: Conversion
Parameters:
input: Input - None
outputBits: Output Bits - None
binaryUnit: Binary Unit - None
decimalPlaces: Decimal Places - None
Block: Navigate To
Description: Navigates to a given URL in the current page
Category: Page
Parameters:
url: Url - None
timeout: Timeout - None

Block: Clear Cookies


Description: Clears the page cookies
Category: Page
Parameters:

Block: Page Type


Description: Sends a key to the page
Category: Page
Parameters:
text: Text - None

Block: Key Press in Page


Description: Presses and releases a key in the browser page
Category: Page
Parameters:
key: Key - None

Block: Key Down in Page


Description: Presses a key in the browser page without releasing it
Category: Page
Parameters:
key: Key - None

Block: Key Up in Page


Description: Releases a key that was previously pressed in the browser page
Category: Page
Parameters:
key: Key - None

Block: Screenshot Page


Description: Takes a screenshot of the entire browser page and saves it to an output file
Category: Page
Parameters:
file: File - None

Block: Screenshot Page Base64


Description: Takes a screenshot of the entire browser page and converts it to a base64 string
Category: Page
Parameters:

Block: Scroll by
Description: Scrolls the page by a given amount of pixels
Category: Page
Parameters:
x: X - None
y: Y - None

Block: Get DOM


Description: Gets the full DOM of the page
Category: Page
Parameters:

Block: Get Cookies


Description: Gets the cookies for a given domain from the browser
Category: Page
Parameters:
domain: Domain - None

Block: Set Cookies


Description: Sets the cookies for a given domain in the browser page
Category: Page
Parameters:
domain: Domain - None
cookies: Cookies - None

Block: Switch to Main Frame


Description: Switches to the main frame of the page
Category: Page
Parameters:

Block: Switch to Alert


Description: Switches to the alert frame of the page
Category: Page
Parameters:

Block: Switch to Parent Frame


Description: Switches to the parent frame
Category: Page
Parameters:

Block: Execute JS
Description: Evaluates a js expression in the current page and returns a json response
Category: Page
Parameters:
expression: Expression - None

Block: Set Attribute Value


Description: Sets the value of the specified attribute of an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
attributeName: Attribute Name - None
value: Value - None

Block: Clear Field


Description: Clears the text in an input field
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
Block: Type
Description: Types text in an input field
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None
timeBetweenKeystrokes: Time Between Keystrokes - None

Block: Type Human


Description: Types text in an input field with human-like random delays
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None

Block: Click
Description: Clicks an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Submit
Description: Submits a form
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Select
Description: Selects a value in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
value: Value - None

Block: Select by Index


Description: Selects a value by index in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
selectionIndex: Selection Index - None

Block: Select by Text


Description: Selects a value by text in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None

Block: Get Attribute Value


Description: Gets the value of an attribute of an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
attributeName: Attribute Name - None

Block: Get Attribute Value All


Description: Gets the values of an attribute of multiple elements
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
attributeName: Attribute Name - None

Block: Is Displayed
Description: Checks if an element is currently being displayed on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Is Enabled
Description: Checks if an element is currently enabled
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Exists
Description: Checks if an element exists on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Position X


Description: Gets the X coordinate of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Position Y


Description: Gets the Y coordinate of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Width


Description: Gets the width of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Height


Description: Gets the height of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Screenshot Element


Description: Takes a screenshot of the element and saves it to an output file
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
fileName: File Name - None

Block: Screenshot Element Base64


Description: Takes a screenshot of the element and converts it to a base64 string
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Switch to Frame


Description: Switches to a different iframe
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Wait for Element


Description: Waits for an element to appear on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
timeout: Timeout - None

Block: Open Browser


Description: Opens a new selenium browser
Category: Browser
Parameters:
extraCmdLineArgs: Extra Cmd Line Args - None

Block: Close Browser


Description: Closes an open selenium browser
Category: Browser
Parameters:

Block: New Tab


Description: Opens a new page in a new browser tab
Category: Browser
Parameters:

Block: Close Tab


Description: Closes the currently active browser tab
Category: Browser
Parameters:

Block: Switch to Tab


Description: Switches to the browser tab with a specified index
Category: Browser
Parameters:
index: Index - None

Block: Reload
Description: Reloads the current page
Category: Browser
Parameters:

Block: Go Back
Description: Goes back to the previously visited page
Category: Browser
Parameters:

Block: Go Forward
Description: Goes forward to the next visited page
Category: Browser
Parameters:

Block: Minimize
Description: Minimizes the browser window
Category: Browser
Parameters:

Block: Maximize
Description: Maximizes the browser window
Category: Browser
Parameters:
Block: Full Screen
Description: Makes the browser window full screen
Category: Browser
Parameters:

Block: Set Browser Size


Description: Sets the height and width of the browser window
Category: Browser
Parameters:
width: Width - None
height: Height - None

Block: WebSocket Connect


Description: Connects to a Web Socket
Category: Web Sockets
Parameters:
url: Url - None
keepAliveMilliseconds: Keep Alive Milliseconds - None
customHeaders: Custom Headers - None

Block: WebSocket Send


Description: Sends a message on the Web Socket
Category: Web Sockets
Parameters:
message: Message - None

Block: WebSocket Send Raw


Description: Sends a raw binary message on the Web Socket
Category: Web Sockets
Parameters:
message: Message - None

Block: WebSocket Read


Description: Gets unread messages that the server sent since the last read
Category: Web Sockets
Parameters:
pollIntervalInMilliseconds: Poll Interval In Milliseconds - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: WebSocket Disconnect


Description: Disconnects the existing Web Socket
Category: Web Sockets
Parameters:

Block: Tcp Connect


Description: Establishes a TCP connection with the given server
Category: TCP
Parameters:
host: Host - None
port: Port - None
useSSL: UseSSL - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Tcp Send Read


Description: Sends a message (ASCII) on the previously opened socket and read the response (ASC
Category: TCP
Parameters:
message: Message - None
unescape: Unescape - None
terminateWithCRLF: Terminate WithCRLF - None
bytesToRead: Bytes To Read - None
timeoutMilliseconds: Timeout Milliseconds - None
useUTF8: Use UTF8 - Enable to use UTF-8 encoding

Block: Tcp Send Read Bytes


Description: Sends a message on the previously opened socket and read the response right after
Category: TCP
Parameters:
bytes: Bytes - None
bytesToRead: Bytes To Read - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Tcp Send


Description: Sends a message(ASCII) on the previously opened socket
Category: TCP
Parameters:
message: Message - None
unescape: Unescape - None
terminateWithCRLF: Terminate WithCRLF - None
useUTF8: Use UTF8 - Enable to use UTF-8 encoding

Block: Tcp Send Bytes


Description: Sends a message on the previously opened socket
Category: TCP
Parameters:
bytes: Bytes - None

Block: Tcp Read


Description: Reads a message (ASCII) from the previously opened socket
Category: TCP
Parameters:
bytesToRead: Bytes To Read - None
timeoutMilliseconds: Timeout Milliseconds - None
useUTF8: Use UTF8 - Enable to use UTF-8 encoding

Block: Tcp Read Bytes


Description: Reads a raw message from the previously opened socket
Category: TCP
Parameters:
bytesToRead: Bytes To Read - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Tcp Send Read Http


Description: Sends an HTTP message on the previously opened socket and reads the response
Category: TCP
Parameters:
message: Message - None
unescape: Unescape - None
timeoutMilliseconds: Timeout Milliseconds - None
Block: Tcp Disconnect
Description: Closes the previously opened socket
Category: TCP
Parameters:

Block: Authenticate (Password)


Description: Logs in via SSH with the given credentials
Category: SSH
Parameters:
host: Host - None
port: Port - None
username: Username - None
password: Password - None
timeoutMilliseconds: Timeout Milliseconds - None
channelTimeoutMilliseconds: Channel Timeout Milliseconds - None
retryAttempts: Retry Attempts - None

Block: Authenticate (None)


Description: Logs in via SSH with no credentials
Category: SSH
Parameters:
host: Host - None
port: Port - None
username: Username - None
timeoutMilliseconds: Timeout Milliseconds - None
channelTimeoutMilliseconds: Channel Timeout Milliseconds - None
retryAttempts: Retry Attempts - None

Block: Authenticate (Private Key)


Description: Logs in via SSH with a private key stored in the given file
Category: SSH
Parameters:
host: Host - None
port: Port - None
username: Username - None
keyFile: Key File - None
keyFilePassword: Key File Password - None
timeoutMilliseconds: Timeout Milliseconds - None
channelTimeoutMilliseconds: Channel Timeout Milliseconds - None
retryAttempts: Retry Attempts - None

Block: Run Command


Description: Executes a command via SSH
Category: SSH
Parameters:
command: Command - None

Block: Smtp Auto Connect


Description: Connects to a SMTP server by automatically detecting the host and port
Category: SMTP
Parameters:
email: Email - None
timeoutMilliseconds: Timeout Milliseconds - None
Block: Smtp Connect
Description: Connects to a SMTP server
Category: SMTP
Parameters:
host: Host - None
port: Port - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Smtp Disconnect


Description: Disconnects from a SMTP server
Category: SMTP
Parameters:

Block: Smtp Login


Description: Logs into an account
Category: SMTP
Parameters:
email: Email - None
password: Password - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Get Smtp Log


Description: Gets the protocol log
Category: SMTP
Parameters:

Block: Smtp Send Mail


Description: Sends a mail to the recipient
Category: SMTP
Parameters:
senderName: Sender Name - None
senderAddress: Sender Address - None
recipientName: Recipient Name - None
recipientAddress: Recipient Address - None
subject: Subject - None
textBody: Text Body - None
htmlBody: Html Body - None

Block: Smtp Send Mail (Advanced)


Description: Sends a mail in advanced mode
Category: SMTP
Parameters:
senders: Senders - None
recipients: Recipients - None
subject: Subject - None
textBody: Text Body - None
htmlBody: Html Body - None
customHeaders: Custom Headers - None
fileAttachments: File Attachments - None

Block: Pop3 Auto Connect


Description: Connects to a POP3 server by automatically detecting the host and port
Category: POP3
Parameters:
email: Email - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Pop3 Connect


Description: Connects to a POP3 server
Category: POP3
Parameters:
host: Host - None
port: Port - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Pop3 Disconnect


Description: Disconnects from a POP3 server
Category: POP3
Parameters:

Block: Pop3 Login


Description: Logs into an account
Category: POP3
Parameters:
email: Email - None
password: Password - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Get Pop3 Log


Description: Gets the protocol log
Category: POP3
Parameters:

Block: Pop3 Read Mail


Description: Gets a text (or HTML) representation of a mail at a specified index
Category: POP3
Parameters:
index: Index - None
preferHtml: Prefer Html - None

Block: Pop3 Get Mails


Description: Gets a list of all mails in the form From|To|Subject (all if Max Amount is 0) from newest to
Category: POP3
Parameters:
maxAmount: Max Amount - None

Block: Delete Mail


Description: Deletes a mail
Category: POP3
Parameters:
index: Index - None

Block: Imap Auto Connect


Description: Connects to an IMAP server by automatically detecting the host and port
Category: IMAP
Parameters:
email: Email - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Imap Connect


Description: Connects to an IMAP server
Category: IMAP
Parameters:
host: Host - None
port: Port - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Imap Disconnect


Description: Disconnects from an IMAP server
Category: IMAP
Parameters:

Block: Imap Login


Description: Logs into an account
Category: IMAP
Parameters:
email: Email - None
password: Password - None
openInbox: Open Inbox - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Get Imap Log


Description: Gets the protocol log
Category: IMAP
Parameters:

Block: Imap Open Inbox


Description: Opens the inbox folder
Category: IMAP
Parameters:

Block: Imap Search Mails


Description: Searches for mails
Category: IMAP
Parameters:
field1: Field1 - None
text1: Text1 - None
field2: Field2 - None
text2: Text2 - None
deliveredAfter: Delivered After - None

Block: Imap Read Mail


Description: Gets a text (or HTML) representation of a mail
Category: IMAP
Parameters:
id: Id - None
preferHtml: Prefer Html - None

Block: Imap Read Mail Raw


Description: Gets a mail in EML format
Category: IMAP
Parameters:
id: Id - None

Block: Imap Delete Mail


Description: Deletes a mail
Category: IMAP
Parameters:
id: Id - None

Block: Imap List Folders


Description: Gets a list of folders
Category: IMAP
Parameters:

Block: Imap Open Folder


Description: Opens a folder given its full name
Category: IMAP
Parameters:
folderName: Folder Name - None
folderAccess: Folder Access - None

Block: Imap Close Folder


Description: Close folder
Category: IMAP
Parameters:

Block: Imap Get Mail Count


Description: Gets the number of email messages in a folder
Category: IMAP
Parameters:

Block: Imap Get Last Message Id


Description: Gets the id of the last message in the current folder
Category: IMAP
Parameters:

Block: Ftp Connect


Description: Connects to an FTP server
Category: FTP
Parameters:
host: Host - None
port: Port - None
username: Username - None
password: Password - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Ftp List Items


Description: Lists the folders on the FTP server
Category: FTP
Parameters:
kind: Kind - None
recursive: Recursive - None

Block: Ftp Download File


Description: Downloads a file from the FTP server
Category: FTP
Parameters:
remoteFileName: Remote File Name - None
localFileName: Local File Name - None
Block: Ftp Download Folder
Description: Downloads a folder from the FTP server
Category: FTP
Parameters:
remoteDir: Remote Dir - None
localDir: Local Dir - None
existsPolicy: Exists Policy - None

Block: Ftp Upload File


Description: Uploads a file to the FTP server
Category: FTP
Parameters:
remoteFileName: Remote File Name - None
localFileName: Local File Name - None
existsPolicy: Exists Policy - None

Block: Ftp Disconnect


Description: Disconnects from the connected FTP server
Category: FTP
Parameters:

Block: Get FTP Log


Description: Gets the protocol log
Category: FTP
Parameters:

Block: Navigate To
Description: Navigates to a given URL in the current page
Category: Page
Parameters:
url: Url - None
loadedEvent: Loaded Event - None
referer: Referer - None
timeout: Timeout - None

Block: Wait for Navigation


Description: Waits for navigation to complete
Category: Page
Parameters:
loadedEvent: Loaded Event - None
timeout: Timeout - None

Block: Clear Cookies


Description: Clears cookies in the page stored for a specific website
Category: Page
Parameters:
website: Website - None

Block: Type in Page


Description: Sends keystrokes to the browser page
Category: Page
Parameters:
text: Text - None
Block: Key Press in Page
Description: Presses and releases a key in the browser page
Category: Page
Parameters:
key: Key - None

Block: Key Down in Page


Description: Presses a key in the browser page without releasing it
Category: Page
Parameters:
key: Key - None

Block: Key Up in Page


Description: Releases a key that was previously pressed in the browser page
Category: Page
Parameters:
key: Key - None

Block: Screenshot Page


Description: Takes a screenshot of the entire browser page and saves it to an output file
Category: Page
Parameters:
file: File - None
fullPage: Full Page - None
omitBackground: Omit Background - None

Block: Screenshot Page Base64


Description: Takes a screenshot of the entire browser page and converts it to a base64 string
Category: Page
Parameters:
fullPage: Full Page - None
omitBackground: Omit Background - None

Block: Scroll to Top


Description: Scrolls to the top of the page
Category: Page
Parameters:

Block: Scroll to Bottom


Description: Scrolls to the bottom of the page
Category: Page
Parameters:

Block: Scroll by
Description: Scrolls the page by a certain amount horizontally and vertically
Category: Page
Parameters:
horizontalScroll: Horizontal Scroll - None
verticalScroll: Vertical Scroll - None

Block: Set Viewport


Description: Sets the viewport dimensions and options
Category: Page
Parameters:
width: Width - None
height: Height - None
isMobile: Is Mobile - None
isLandscape: Is Landscape - None
scaleFactor: Scale Factor - None

Block: Get DOM


Description: Gets the full DOM of the page
Category: Page
Parameters:

Block: Get Cookies


Description: Gets the cookies for a given domain from the browser. If the domain is empty, gets all co
Category: Page
Parameters:
domain: Domain - None

Block: Set Cookies


Description: Sets the cookies for a given domain in the browser page
Category: Page
Parameters:
domain: Domain - None
cookies: Cookies - None

Block: Set User-Agent


Description: Sets the User Agent of the browser page
Category: Page
Parameters:
userAgent: User Agent - None

Block: Switch to Main Frame


Description: Switches to the main frame of the page
Category: Page
Parameters:

Block: Execute JS
Description: Evaluates a js expression in the current page and returns a json response
Category: Page
Parameters:
expression: Expression - None

Block: Wait for Response


Description: Captures the response from the given URL
Category: Page
Parameters:
url: Url - None
timeoutMilliseconds: Timeout Milliseconds - None

Block: Set Attribute Value


Description: Sets the value of the specified attribute of an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
attributeName: Attribute Name - None
value: Value - None

Block: Type
Description: Types text in an input field
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None
timeBetweenKeystrokes: Time Between Keystrokes - None

Block: Type Human


Description: Types text in an input field with human-like random delays
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None

Block: Click
Description: Clicks an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
mouseButton: Mouse Button - None
clickCount: Click Count - None
timeBetweenClicks: Time Between Clicks - None

Block: Submit
Description: Submits a form
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Select
Description: Selects a value in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
value: Value - None

Block: Select by Index


Description: Selects a value by index in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
selectionIndex: Selection Index - None

Block: Select by Text


Description: Selects a value by text in a select element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
text: Text - None

Block: Get Attribute Value


Description: Gets the value of an attribute of an element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
attributeName: Attribute Name - None

Block: Get Attribute Value All


Description: Gets the values of an attribute of multiple elements
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
attributeName: Attribute Name - None

Block: Is Displayed
Description: Checks if an element is currently being displayed on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Exists
Description: Checks if an element exists on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Upload Files


Description: Uploads one or more files to the selected element
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
filePaths: File Paths - None

Block: Get Position X


Description: Gets the X coordinate of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Position Y


Description: Gets the Y coordinate of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Width


Description: Gets the width of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Get Height


Description: Gets the height of the element in pixels
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Screenshot Element


Description: Takes a screenshot of the element and saves it to an output file
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
fileName: File Name - None
fullPage: Full Page - None
omitBackground: Omit Background - None

Block: Screenshot Element Base64


Description: Takes a screenshot of the element and converts it to a base64 string
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None
fullPage: Full Page - None
omitBackground: Omit Background - None

Block: Switch to Frame


Description: Switches to a different iframe
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
index: Index - None

Block: Wait for Element


Description: Waits for an element to appear on the page
Category: Elements
Parameters:
findBy: Find By - None
identifier: Identifier - None
hidden: Hidden - None
visible: Visible - None
timeout: Timeout - None

Block: Open Browser


Description: Opens a new puppeteer browser
Category: Browser
Parameters:
extraCmdLineArgs: Extra Cmd Line Args - None

Block: Close Browser


Description: Closes an open puppeteer browser
Category: Browser
Parameters:

Block: New Tab


Description: Opens a new page in a new browser tab
Category: Browser
Parameters:

Block: Close Tab


Description: Closes the currently active browser tab
Category: Browser
Parameters:

Block: Switch to Tab


Description: Switches to the browser tab with a specified index
Category: Browser
Parameters:
index: Index - None

Block: Reload
Description: Reloads the current page
Category: Browser
Parameters:

Block: Go Back
Description: Goes back to the previously visited page
Category: Browser
Parameters:

Block: Go Forward
Description: Goes forward to the next visited page
Category: Browser
Parameters:

Block: Shell Command


Description: Executes a shell command and redirects all stdout to the output variable
Category: Interop
Parameters:
executable: Executable - None
arguments: Arguments - None

Block: Random User Agent


Description: Generates a random User Agent using the builtin provider or a custom list
Category: Functions
Parameters:
platform: Platform - None

Block: Current Unix Time


Description: Gets the current unix time in seconds
Category: Time
Parameters:
useUtc: Use Utc - None

Block: Unix Time To Date


Description: Converts a unix time to a formatted datetime string
Category: Time
Parameters:
unixTime: Unix Time - None
format: Format - None

Block: Date To Unix Time


Description: Parses a unix time from a formatted datetime string
Category: Time
Parameters:
datetime: Datetime - None
format: Format - None

Block: Unix Time To IS O8601


Description: Converts a unix time to an ISO8601 datetime string
Category: Time
Parameters:
unixTime: Unix Time - None

Block: Count Occurrences


Description: Rounds the value down to the nearest integer
Category: String Functions
Parameters:
input: Input - None
word: Word - None

Block: Substring
Description: Retrieves a piece of an input string
Category: String Functions
Parameters:
input: Input - None
index: Index - None
length: Length - None
Block: Reverse
Description: Reverses the characters in the input string
Category: String Functions
Parameters:
input: Input - None

Block: Trim
Description: Removes leading or trailing whitespace from the input string
Category: String Functions
Parameters:
input: Input - None

Block: Length
Description: Gets the length of a string
Category: String Functions
Parameters:
input: Input - None

Block: To Uppercase
Description: Changes all letters of a string to uppercase
Category: String Functions
Parameters:
input: Input - None

Block: To Lowercase
Description: Changes all letters of a string to lowercase
Category: String Functions
Parameters:
input: Input - None

Block: Replace
Description: Replaces all occurrences of some text in a string
Category: String Functions
Parameters:
original: Original - None
toReplace: To Replace - None
replacement: Replacement - None

Block: Regex Replace


Description: Replaces all regex matches with a given text
Category: String Functions
Parameters:
original: Original - None
pattern: Pattern - None
replacement: Replacement - None

Block: Translate
Description: Translates text in a string basing on a dictionary
Category: String Functions
Parameters:
input: Input - None
translations: Translations - None
replaceOne: Replace One - None
Block: Url Encode
Description: URL encodes a string
Category: String Functions
Parameters:
input: Input - None

Block: Url Decode


Description: URL decodes a string
Category: String Functions
Parameters:
input: Input - None

Block: Encode HTML Entities


Description: Encodes HTML entities in a string
Category: String Functions
Parameters:
input: Input - None

Block: Decode HTML Entities


Description: Decodes HTML entities in a string
Category: String Functions
Parameters:
input: Input - None

Block: Random String


Description: Generates a random string given a mask
Category: String Functions
Parameters:
input: Input - None
customCharset: Custom Charset - None

Block: Unescape
Description: Unescapes characters in a string
Category: String Functions
Parameters:
input: Input - None

Block: Split
Description: Splits a string into a list
Category: String Functions
Parameters:
input: Input - None
separator: Separator - None

Block: Char At
Description: Gets the character at a specific index
Category: String Functions
Parameters:
input: Input - None
index: Index - None

Block: Get List Length


Description: Counts the number of elements in the list
Category: List Functions
Parameters:
list: List - None

Block: Join List


Description: Joins all the strings in the list to create a single string with the given separator
Category: List Functions
Parameters:
list: List - None
separator: Separator - None

Block: Sort List


Description: Sorts a list alphabetically
Category: List Functions
Parameters:
list: List - None
ascending: Ascending - None
numeric: Numeric - None

Block: Concat Lists


Description: Concatenates two lists into a single one
Category: List Functions
Parameters:
list1: List1 - None
list2: List2 - None

Block: Zip Lists


Description: Zips two lists into a single one
Category: List Functions
Parameters:
list1: List1 - None
list2: List2 - None
fill: Fill - None
fillString: Fill String - None
format: Format - None

Block: Map Lists


Description: Maps two lists into a dictionary
Category: List Functions
Parameters:
list1: List1 - None
list2: List2 - None

Block: Add To List


Description: Adds an item to a list
Category: List Functions
Parameters:
list: List - None
item: Item - None
index: Index - None

Block: Remove From List


Description: Removes an item from a list
Category: List Functions
Parameters:
list: List - None
index: Index - None
Block: Remove All From List
Description: Removes all items that match a given condition from a list
Category: List Functions
Parameters:
list: List - None
comparison: Comparison - None
term: Term - None

Block: Remove Duplicates


Description: Removes duplicate items from a list
Category: List Functions
Parameters:
list: List - None

Block: Get Random Item


Description: Gets a random item from a list
Category: List Functions
Parameters:
list: List - None

Block: Shuffle
Description: Shuffles the items of a list
Category: List Functions
Parameters:
list: List - None

Block: To Dictionary
Description: Splits the items of a list to create a dictionary
Category: List Functions
Parameters:
list: List - None
separator: Separator - None
autoTrim: Auto Trim - None

Block: Index Of
Description: Gets the index of an element of a list
Category: List Functions
Parameters:
list: List - None
item: Item - None
exactMatch: Exact Match - None
caseSensitive: Case Sensitive - None

Block: Create List of Numbers


Description: Creates a list of N numbers starting from the specified number
Category: List Functions
Parameters:
start: Start - None
count: Count - None

Block: Random Integer


Description: Generates a random integer between two values (inclusive)
Category: Integer Functions
Parameters:
minimum: Minimum - None
maximum: Maximum - None

Block: Maximum Int


Description: Takes the maximum between two integers
Category: Integer Functions
Parameters:
first: First - None
second: Second - None

Block: Minimum int


Description: Takes the minimum between two integers
Category: Integer Functions
Parameters:
first: First - None
second: Second - None

Block: Ceil
Description: Rounds the value up to the nearest integer
Category: Float Functions
Parameters:
input: Input - None

Block: Floor
Description: Rounds the value down to the nearest integer
Category: Float Functions
Parameters:
input: Input - None

Block: Round To Integer


Description: Rounds the value to the nearest integer
Category: Float Functions
Parameters:
input: Input - None

Block: Round
Description: Rounds the value to the given decimal places
Category: Float Functions
Parameters:
input: Input - None
decimalPlaces: Decimal Places - None

Block: Compute
Description: Computes the value of a given mathematical expression
Category: Float Functions
Parameters:
input: Input - None

Block: Random Float


Description: Generates a random float between two values (inclusive)
Category: Float Functions
Parameters:
minimum: Minimum - None
maximum: Maximum - None
Block: Maximum Float
Description: Takes the maximum between two floats
Category: Float Functions
Parameters:
first: First - None
second: Second - None

Block: Minimum Float


Description: Takes the minimum between two floats
Category: Float Functions
Parameters:
first: First - None
second: Second - None

Block: Add Key Value Pair


Description: Adds an item to the dictionary
Category: Dictionary Functions
Parameters:
dictionary: Dictionary - None
key: Key - None
value: Value - None

Block: Remove By Key


Description: Removes an item with a given key from the dictionary
Category: Dictionary Functions
Parameters:
dictionary: Dictionary - None
key: Key - None

Block: Get Key


Description: Gets a dictionary key by value (old <DICT{value}>)
Category: Dictionary Functions
Parameters:
dictionary: Dictionary - None
value: Value - None

Block: XOR
Description: XOR En-/Decryption on byte arrays
Category: Crypto
Parameters:
bytes: Bytes - None
key: Key - None

Block: XOR Strings


Description: Does a simple XOR En-/Decryption on strings
Category: Crypto
Parameters:
text: Text - None
key: Key - None

Block: Hash
Description: Hashes data using the specified hashing function
Category: Crypto
Parameters:
input: Input - None
hashFunction: Hash Function - None

Block: Hash String


Description: Hashes a UTF8 string to a HEX-encoded lowercase string using the specified hashing fu
Category: Crypto
Parameters:
input: Input - None
hashFunction: Hash Function - None

Block: NTLM Hash


Description: Hashes a string using NTLM
Category: Crypto
Parameters:
input: Input - None

Block: Hmac
Description: Computes the HMAC signature of some data using the specified secret key and hashing
Category: Crypto
Parameters:
input: Input - None
key: Key - None
hashFunction: Hash Function - None

Block: Hmac String


Description: Computes the HMAC signature as a HEX-encoded lowercase string from a given UTF8 s
Category: Crypto
Parameters:
input: Input - None
key: Key - None
hashFunction: Hash Function - None

Block: Scrypt String


Description: Hashes data using the Scrypt algorithm
Category: Crypto
Parameters:
password: Password - None
salt: Salt - None
iterationCount: Iteration Count - None
blockSize: Block Size - None
threadCount: Thread Count - None

Block: RSA Encrypt


Description: Encrypts data using RSA
Category: Crypto
Parameters:
plainText: Plain Text - None
modulus: Modulus - None
exponent: Exponent - None
useOAEP: UseOAEP - None

Block: RSA Decrypt


Description: Decrypts data using RSA
Category: Crypto
Parameters:
cipherText: Cipher Text - None
modulus: Modulus - None
d: D - None
useOAEP: UseOAEP - None

Block: RSA PKCS1PAD2


Description: Encrypts data using RSA with PKCS1PAD2
Category: Crypto
Parameters:
plainText: Plain Text - None
hexModulus: Hex Modulus - None
hexExponent: Hex Exponent - None

Block: PBKDF2PKCS5
Description: Generates a PKCS v5 #2.0 key using a Password-Based Key Derivation Function
Category: Crypto
Parameters:
password: Password - None
salt: Salt - None
saltSize: Salt Size - None
iterations: Iterations - None
keyLength: Key Length - None
type: Type - None

Block: AES Encrypt


Description: Encrypts data with AES
Category: Crypto
Parameters:
plainText: Plain Text - None
key: Key - None
iv: Iv - None
mode: Mode - None
padding: Padding - None
keySize: Key Size - None

Block: AES Encrypt String


Description: Encrypts a string with AES
Category: Crypto
Parameters:
plainText: Plain Text - None
key: Key - None
iv: Iv - None
mode: Mode - None
padding: Padding - None
keySize: Key Size - None

Block: AES Decrypt


Description: Decrypts data with AES
Category: Crypto
Parameters:
cipherText: Cipher Text - None
key: Key - None
iv: Iv - None
mode: Mode - None
padding: Padding - None
keySize: Key Size - None
Block: AES Decrypt String
Description: Decrypts data with AES to string
Category: Crypto
Parameters:
cipherText: Cipher Text - None
key: Key - None
iv: Iv - None
mode: Mode - None
padding: Padding - None
keySize: Key Size - None

Block: JWT Encode


Description: Generates a JSON Web Token using a secret key, payload, optional extra headers and
Category: Crypto
Parameters:
algorithm: Algorithm - None
secret: Secret - None
extraHeaders: Extra Headers - None
payload: Payload - None

Block: BCrypt Hash


Description: Generates a BCrypt hash from an input and a salt
Category: Crypto
Parameters:
input: Input - None
salt: Salt - None

Block: BCrypt Hash (Gen Salt)


Description: Generates a BCrypt hash from an input by generating a salt
Category: Crypto
Parameters:
input: Input - None
rounds: Rounds - None
saltRevision: Salt Revision - None

Block: BCrypt Verify


Description: Verifies that a BCrypt hash is valid
Category: Crypto
Parameters:
input: Input - None
hash: Hash - None

Block: AWS4 Signature


Description: Generates an AWS4 Signature from a key, date, region and service
Category: Crypto
Parameters:
key: Key - None
date: Date - None
region: Region - None
service: Service - None

Block: Constant String


Description: Creates a constant string
Category: Constants
Parameters:
value: Value - None

Block: Constant Integer


Description: Creates a constant integer
Category: Constants
Parameters:
value: Value - None

Block: Constant Float


Description: Creates a constant float
Category: Constants
Parameters:
value: Value - None

Block: Constant Bool


Description: Creates a constant bool
Category: Constants
Parameters:
value: Value - None

Block: Constant Byte Array


Description: Creates a constant byte array
Category: Constants
Parameters:
value: Value - None

Block: Constant List


Description: Creates a constant list
Category: Constants
Parameters:
value: Value - None

Block: Constant Dictionary


Description: Creates a constant dictionary
Category: Constants
Parameters:
value: Value - None

Block: Merge Byte Arrays


Description: Merges two byte arrays together to form a longer one
Category: Byte Array Functions
Parameters:
first: First - None
second: Second - None

Block: Solve Text Captcha


Description: Solves a text captcha
Category: Captchas
Parameters:
question: Question - The description of the captcha to solve, e.g. What is 2+2?
languageGroup: Language Group - None
language: Language - None

Block: Solve Image Captcha


Description: Solves an image captcha
Category: Captchas
Parameters:
base64: Base64 - None
languageGroup: Language Group - None
language: Language - None
isPhrase: Is Phrase - None
caseSensitive: Case Sensitive - None
requiresCalculation: Requires Calculation - None
characterSet: Character Set - None
minLength: Min Length - None
maxLength: Max Length - None
textInstructions: Text Instructions - None

Block: Solve Recaptcha V2


Description: Solves a ReCaptcha V2
Category: Captchas
Parameters:
siteKey: Site Key - None
siteUrl: Site Url - None
sData: S Data - None
enterprise: Enterprise - None
isInvisible: Is Invisible - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve Recaptcha V3


Description: Solves a ReCaptcha V3
Category: Captchas
Parameters:
siteKey: Site Key - None
siteUrl: Site Url - None
action: Action - None
minScore: Min Score - None
enterprise: Enterprise - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve Fun Captcha


Description: Solves a FunCaptcha
Category: Captchas
Parameters:
publicKey: Public Key - None
serviceUrl: Service Url - None
siteUrl: Site Url - None
noJS: NoJS - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve H Captcha


Description: Solves a HCaptcha
Category: Captchas
Parameters:
siteKey: Site Key - None
siteUrl: Site Url - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve Capy Captcha


Description: Solves a Capy captcha
Category: Captchas
Parameters:
siteKey: Site Key - None
siteUrl: Site Url - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve Key Captcha


Description: Solves a KeyCaptcha
Category: Captchas
Parameters:
userId: User Id - None
sessionId: Session Id - None
webServerSign1: Web Server Sign1 - None
webServerSign2: Web Server Sign2 - None
siteUrl: Site Url - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Solve Gee Test Captcha


Description: Solves a GeeTest captcha
Category: Captchas
Parameters:
gt: Gt - None
apiChallenge: Api Challenge - None
apiServer: Api Server - None
siteUrl: Site Url - None
useProxy: Use Proxy - None
userAgent: User Agent - None

Block: Report Last Solution


Description: Reports an incorrectly solved captcha to the service in order to get funds back
Category: Captchas
Parameters:
Table of Contents
File: /mnt/data/RuriLib_Http_Tests_Documentation.pdf 20
File: /mnt/data/RuriLib_Http_Documentation.pdf 105
File: /mnt/data/RuriLib_Tests_Documentation.pdf 190
Detailed Documentation and Explanations 192
Block Descriptions 230

You might also like