Created
January 23, 2020 14:20
-
-
Save abodalevsky/ee5c0e93b5e0b62912e4587aab715087 to your computer and use it in GitHub Desktop.
Proxy class listen HTTP and redirect request to HTTPS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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