Skip to content

Instantly share code, notes, and snippets.

@abodalevsky
Created January 23, 2020 14:20
Show Gist options
  • Save abodalevsky/ee5c0e93b5e0b62912e4587aab715087 to your computer and use it in GitHub Desktop.
Save abodalevsky/ee5c0e93b5e0b62912e4587aab715087 to your computer and use it in GitHub Desktop.
Proxy class listen HTTP and redirect request to HTTPS
using Logging;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BlueJeans.VideoShare.Player
{
/// <summary>
/// Creates an HTTP listener and redirects requests to an HTTPS host.
/// </summary>
internal class Proxy : IDisposable
{
#region Constants
private const int BufferSize = 2048;
private const int EndPort = 65535;
private const int StartPort = 27001;
#endregion
#region Fields
private readonly ILogger logger;
private readonly Uri uri;
private HttpListener server;
private long downloadedBytes = 0;
#endregion
private static uint instance = 0;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Proxy"/> class.
/// </summary>
/// <param name="uri">
/// A <see cref="Uri"/> of the resource that has to be downloaded.
/// </param>
/// <param name="mode">
/// A <see cref="PlayerMode"/> that switches the proxy to a mode
/// in which either the whole file or its initial part should be received.
/// </param>
public Proxy(Uri uri, PlayerMode mode)
{
if (mode == PlayerMode.Undefined)
{
var errorMessage = $"Only {PlayerMode.Presenter} and {PlayerMode.Watcher} player modes are supported.";
throw new NotSupportedException(errorMessage);
}
this.logger = new LazyLogger<Proxy>($"VSS:Player:Proxy[{instance++}]");
this.uri = uri;
this.SecretKey = GenerateSecretKey();
this.StartServer();
ServicePointManager.SecurityProtocol =
SecurityProtocolType.Ssl3 |
SecurityProtocolType.Tls |
SecurityProtocolType.Tls11 |
SecurityProtocolType.Tls12;
Task.Run(() => { this.ProcessRequests(); });
}
#endregion
#region Events
/// <summary>
/// Indicates that downloading started.
/// </summary>
public event EventHandler Started;
/// <summary>
/// Indicates that download has completed.
/// </summary>
public event EventHandler Stopped;
#endregion
#region Properties
/// <summary>
/// Gets the port.
/// </summary>
public int Port { get; private set; }
#endregion
#region Methods
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
if (this.server != null)
{
try
{
this.server.Stop();
this.server.Close();
}
catch (Exception e)
{
this.logger.Error($"Error while is disposing proxy {e}");
}
}
}
/// <summary>
/// Returns the number of downloaded bytes and reset value to 0.
/// </summary>
/// <returns>number of bytes downloaded since previous call.</returns>
internal long GetDownloadedBytesAndReset()
{
return Interlocked.Exchange(ref this.downloadedBytes, 0);
}
private void InitializeResponseHeaders(HttpListenerResponse response, HttpResponseMessage httpsResponse)
{
var responseHeaders = response.Headers;
response.StatusCode = (int)httpsResponse.StatusCode;
// HTTP response headers.
foreach (var item in httpsResponse.Headers)
{
var headerKey = item.Key;
if (httpsResponse.Headers.TryGetValues(headerKey, out var headerValues))
{
var headerValue = string.Join(", ", headerValues.ToArray());
responseHeaders.Add(headerKey, headerValue);
}
}
// HTTP content headers.
var contentHeaders = httpsResponse.Content.Headers
.Where(x => x.Key != "Content-Length");
foreach (var item in contentHeaders)
{
var headerKey = item.Key;
if (httpsResponse.Content.Headers.TryGetValues(headerKey, out var headerValues))
{
var headerValue = string.Join(", ", headerValues.ToArray());
responseHeaders.Add(headerKey, headerValue);
}
}
// Content-Length.
var contentLength = httpsResponse.Content.Headers.ContentLength;
if (contentLength != null)
{
response.ContentLength64 = contentLength.Value;
}
response.SendChunked = false;
this.PrintHeaders(
responseHeaders,
"HTTP Response Headers",
$"{nameof(response.StatusCode)}: {response.StatusCode}",
$"{nameof(response.ContentLength64)}: {response.ContentLength64}");
}
private void PrintHeaders(object headers, string title, params string[] additionalParams)
{
if (!this.logger.IsTraceEnabled())
{
return;
}
var text = headers.ToString().Trim();
var lines = SplitTextIntoLines(text);
var count = lines.Length;
var output = new StringBuilder();
output.AppendLine();
output.AppendLine($"\t*** {title} ({count + additionalParams?.Length}) ***");
output.AppendLine();
foreach (var line in lines)
{
output.AppendLine($"\t{line}");
}
if (additionalParams != null)
{
for (int i = 0; i < additionalParams.Length; i++)
{
output.AppendLine($"\t{additionalParams[i]}");
}
}
this.logger.Trace(output.ToString());
}
private async void ProcessRequest(HttpListenerContext context)
{
var request = context.Request;
var requestHeaders = request.Headers;
this.logger.Info($"{request.HttpMethod} {request.Url}");
this.PrintHeaders(requestHeaders, "HTTP Request Headers");
// Send HTTPS request.
HttpResponseMessage httpsResponse;
HttpListenerResponse response = null;
try
{
httpsResponse = await this.SendHttpsRequest(request);
httpsResponse.EnsureSuccessStatusCode();
response = context.Response;
}
catch (HttpRequestException exception)
{
this.logger.Error($"HTTPS request failure. Aborting proxy request...\r\n{exception}");
response?.Abort();
return;
}
// Build response.
this.InitializeResponseHeaders(response, httpsResponse);
// Transfer data.
this.logger.Info("Transferring data...");
try
{
this.Started?.Invoke(this, EventArgs.Empty);
using (var httpOutput = new BinaryWriter(context.Response.OutputStream))
using (var httpsInput = await httpsResponse.Content.ReadAsStreamAsync())
{
var buffer = new byte[BufferSize];
int bytesRead;
do
{
bytesRead = await httpsInput.ReadAsync(buffer, 0, BufferSize);
Interlocked.Add(ref this.downloadedBytes, bytesRead);
try
{
httpOutput.Write(buffer, 0, bytesRead);
}
catch (HttpListenerException exception)
{
this.logger.Error($"Client (player) aborted connection, aborting proxy request...\r\n{exception}");
return;
}
}
while (bytesRead != 0);
}
}
catch
{
this.logger.Info("Connection terminated.");
}
finally
{
this.logger.Info("Releasing resources...");
this.Stopped?.Invoke(this, EventArgs.Empty);
response.Abort();
httpsResponse.Dispose();
}
}
private async void ProcessRequests()
{
while (this.server.IsListening)
{
try
{
var context = await this.server.GetContextAsync();
if (this.CheckSecretKey(context))
{
_ = Task.Run(() => { this.ProcessRequest(context); });
}
else
{
context.Response.Abort();
}
}
catch (Exception exception)
{
this.logger.Error($"Server stopped.\r\n{exception}");
break;
}
}
this.server.Close();
}
private async Task<HttpResponseMessage> SendHttpsRequest(HttpListenerRequest request)
{
this.logger.Info($"Connecting to {this.uri}...");
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = this.uri,
Version = new Version(1, 1),
};
var headers = request.Headers;
var headerKeys = headers.AllKeys.Where(x => x != "Host");
foreach (var headerKey in headerKeys)
{
var headerValue = headers[headerKey];
requestMessage.Headers.Add(headerKey, headerValue);
}
// this.PrintHeaders(requestMessage.Headers, "HTTPS Request Headers");
var httpClientHandler = new HttpClientHandler
{
AllowAutoRedirect = false,
Proxy = WebRequest.GetSystemWebProxy(),
};
httpClientHandler.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
var httpClient = new HttpClient(httpClientHandler);
var responseMessage = await httpClient.SendAsync(
requestMessage, HttpCompletionOption.ResponseHeadersRead);
this.logger.Trace("HTTPS response received.");
// this.PrintHeaders(
// responseMessage.Headers,
// "HTTPS Response Headers",
// $"{nameof(responseMessage.StatusCode)}: {(int)responseMessage.StatusCode}",
// $"{nameof(responseMessage.Content.Headers.ContentLength)}: {responseMessage.Content.Headers.ContentLength}",
// $"{nameof(responseMessage.Content.Headers.ContentRange)}: {responseMessage.Content.Headers.ContentRange}",
// $"{nameof(responseMessage.Content.Headers.ContentType)}: {responseMessage.Content.Headers.ContentType}");
return responseMessage;
}
private static string[] SplitTextIntoLines(string text)
{
return text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
}
private void StartServer()
{
this.logger.Info("Starting HTTP server...");
for (this.Port = StartPort; this.Port <= EndPort; this.Port++)
{
this.server = new HttpListener();
this.server.Prefixes.Add($"http://localhost:{this.Port}/");
try
{
this.server.Start();
}
catch (HttpListenerException exception)
{
// ERROR_ALREADY_EXISTS
if (exception.ErrorCode == 0xB7)
{
continue;
}
throw;
}
break;
}
this.logger.Info($"HTTP server running on port {this.Port}.");
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment