Commit d10cbe25 authored by Hakan Arıcı's avatar Hakan Arıcı

ProcessId on WebSession is fixed to get source process' id not proxy's pid.

parent d7c48859
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Helpers;
using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Network;
using Titanium.Web.Proxy.Shared; using Titanium.Web.Proxy.Shared;
using Titanium.Web.Proxy.Tcp; using Titanium.Web.Proxy.Tcp;
namespace Titanium.Web.Proxy.Http namespace Titanium.Web.Proxy.Http
{ {
/// <summary> /// <summary>
/// Used to communicate with the server over HTTP(S) /// Used to communicate with the server over HTTP(S)
/// </summary> /// </summary>
public class HttpWebClient public class HttpWebClient
{ {
private int processId; private int processId;
/// <summary> /// <summary>
/// Connection to server /// Connection to server
/// </summary> /// </summary>
internal TcpConnection ServerConnection { get; set; } internal TcpConnection ServerConnection { get; set; }
public List<HttpHeader> ConnectHeaders { get; set; } public List<HttpHeader> ConnectHeaders { get; set; }
public Request Request { get; set; } public Request Request { get; set; }
public Response Response { get; set; } public Response Response { get; set; }
/// <summary> /// <summary>
/// PID of the process that is created the current session /// PID of the process that is created the current session
/// </summary> /// </summary>
public int ProcessId public int ProcessId { get; internal set; }
{
get /// <summary>
{ /// Is Https?
if (processId == 0) /// </summary>
{ public bool IsHttps => this.Request.RequestUri.Scheme == Uri.UriSchemeHttps;
TcpRow tcpRow = TcpHelper.GetExtendedTcpTable().TcpRows
.FirstOrDefault(row => row.LocalEndPoint.Port == ServerConnection.port); /// <summary>
/// Set the tcp connection to server used by this webclient
processId = tcpRow?.ProcessId ?? -1; /// </summary>
} /// <param name="connection">Instance of <see cref="TcpConnection"/></param>
internal void SetConnection(TcpConnection connection)
return processId; {
} connection.LastAccess = DateTime.Now;
} ServerConnection = connection;
}
/// <summary>
/// Is Https? internal HttpWebClient()
/// </summary> {
public bool IsHttps => this.Request.RequestUri.Scheme == Uri.UriSchemeHttps; this.Request = new Request();
this.Response = new Response();
/// <summary> }
/// Set the tcp connection to server used by this webclient
/// </summary> /// <summary>
/// <param name="connection">Instance of <see cref="TcpConnection"/></param> /// Prepare & send the http(s) request
internal void SetConnection(TcpConnection connection) /// </summary>
{ /// <returns></returns>
connection.LastAccess = DateTime.Now; internal async Task SendRequest(bool enable100ContinueBehaviour)
ServerConnection = connection; {
} Stream stream = ServerConnection.Stream;
internal HttpWebClient() StringBuilder requestLines = new StringBuilder();
{
this.Request = new Request(); //prepare the request & headers
this.Response = new Response(); if ((ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false) || (ServerConnection.UpStreamHttpsProxy != null && ServerConnection.IsHttps == true))
} {
requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.AbsoluteUri, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}"));
/// <summary> }
/// Prepare & send the http(s) request else
/// </summary> {
/// <returns></returns> requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.PathAndQuery, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}"));
internal async Task SendRequest(bool enable100ContinueBehaviour) }
{
Stream stream = ServerConnection.Stream; //Send Authentication to Upstream proxy if needed
if (ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false && !string.IsNullOrEmpty(ServerConnection.UpStreamHttpProxy.UserName) && ServerConnection.UpStreamHttpProxy.Password != null)
StringBuilder requestLines = new StringBuilder(); {
requestLines.AppendLine("Proxy-Connection: keep-alive");
//prepare the request & headers requestLines.AppendLine("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(ServerConnection.UpStreamHttpProxy.UserName + ":" + ServerConnection.UpStreamHttpProxy.Password)));
if ((ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false) || (ServerConnection.UpStreamHttpsProxy != null && ServerConnection.IsHttps == true)) }
{ //write request headers
requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.AbsoluteUri, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}")); foreach (var headerItem in this.Request.RequestHeaders)
} {
else var header = headerItem.Value;
{ if (headerItem.Key != "Proxy-Authorization")
requestLines.AppendLine(string.Join(" ", this.Request.Method, this.Request.RequestUri.PathAndQuery, $"HTTP/{this.Request.HttpVersion.Major}.{this.Request.HttpVersion.Minor}")); {
} requestLines.AppendLine(header.Name + ':' + header.Value);
}
//Send Authentication to Upstream proxy if needed }
if (ServerConnection.UpStreamHttpProxy != null && ServerConnection.IsHttps == false && !string.IsNullOrEmpty(ServerConnection.UpStreamHttpProxy.UserName) && ServerConnection.UpStreamHttpProxy.Password != null)
{ //write non unique request headers
requestLines.AppendLine("Proxy-Connection: keep-alive"); foreach (var headerItem in this.Request.NonUniqueRequestHeaders)
requestLines.AppendLine("Proxy-Authorization" + ": Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(ServerConnection.UpStreamHttpProxy.UserName + ":" + ServerConnection.UpStreamHttpProxy.Password))); {
} var headers = headerItem.Value;
//write request headers foreach (var header in headers)
foreach (var headerItem in this.Request.RequestHeaders) {
{ if (headerItem.Key != "Proxy-Authorization")
var header = headerItem.Value; {
if (headerItem.Key != "Proxy-Authorization") requestLines.AppendLine(header.Name + ':' + header.Value);
{ }
requestLines.AppendLine(header.Name + ':' + header.Value); }
} }
}
requestLines.AppendLine();
//write non unique request headers
foreach (var headerItem in this.Request.NonUniqueRequestHeaders) string request = requestLines.ToString();
{ byte[] requestBytes = Encoding.ASCII.GetBytes(request);
var headers = headerItem.Value;
foreach (var header in headers) await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
{ await stream.FlushAsync();
if (headerItem.Key != "Proxy-Authorization")
{ if (enable100ContinueBehaviour)
requestLines.AppendLine(header.Name + ':' + header.Value); {
} if (this.Request.ExpectContinue)
} {
} var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
var responseStatusCode = httpResult[1].Trim();
requestLines.AppendLine(); var responseStatusDescription = httpResult[2].Trim();
string request = requestLines.ToString(); //find if server is willing for expect continue
byte[] requestBytes = Encoding.ASCII.GetBytes(request); if (responseStatusCode.Equals("100")
&& responseStatusDescription.ToLower().Equals("continue"))
await stream.WriteAsync(requestBytes, 0, requestBytes.Length); {
await stream.FlushAsync(); this.Request.Is100Continue = true;
await ServerConnection.StreamReader.ReadLineAsync();
if (enable100ContinueBehaviour) }
{ else if (responseStatusCode.Equals("417")
if (this.Request.ExpectContinue) && responseStatusDescription.ToLower().Equals("expectation failed"))
{ {
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3); this.Request.ExpectationFailed = true;
var responseStatusCode = httpResult[1].Trim(); await ServerConnection.StreamReader.ReadLineAsync();
var responseStatusDescription = httpResult[2].Trim(); }
}
//find if server is willing for expect continue }
if (responseStatusCode.Equals("100") }
&& responseStatusDescription.ToLower().Equals("continue"))
{ /// <summary>
this.Request.Is100Continue = true; /// Receive & parse the http response from server
await ServerConnection.StreamReader.ReadLineAsync(); /// </summary>
} /// <returns></returns>
else if (responseStatusCode.Equals("417") internal async Task ReceiveResponse()
&& responseStatusDescription.ToLower().Equals("expectation failed")) {
{ //return if this is already read
this.Request.ExpectationFailed = true; if (this.Response.ResponseStatusCode != null) return;
await ServerConnection.StreamReader.ReadLineAsync();
} var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
}
} if (string.IsNullOrEmpty(httpResult[0]))
} {
//Empty content in first-line, try again
/// <summary> httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
/// Receive & parse the http response from server }
/// </summary>
/// <returns></returns> var httpVersion = httpResult[0].Trim().ToLower();
internal async Task ReceiveResponse()
{ var version = new Version(1, 1);
//return if this is already read if (httpVersion == "http/1.0")
if (this.Response.ResponseStatusCode != null) return; {
version = new Version(1, 0);
var httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3); }
if (string.IsNullOrEmpty(httpResult[0])) this.Response.HttpVersion = version;
{ this.Response.ResponseStatusCode = httpResult[1].Trim();
//Empty content in first-line, try again this.Response.ResponseStatusDescription = httpResult[2].Trim();
httpResult = (await ServerConnection.StreamReader.ReadLineAsync()).Split(ProxyConstants.SpaceSplit, 3);
} //For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request
if (this.Response.ResponseStatusCode.Equals("100")
var httpVersion = httpResult[0].Trim().ToLower(); && this.Response.ResponseStatusDescription.ToLower().Equals("continue"))
{
var version = new Version(1, 1); //Read the next line after 100-continue
if (httpVersion == "http/1.0") this.Response.Is100Continue = true;
{ this.Response.ResponseStatusCode = null;
version = new Version(1, 0); await ServerConnection.StreamReader.ReadLineAsync();
} //now receive response
await ReceiveResponse();
this.Response.HttpVersion = version; return;
this.Response.ResponseStatusCode = httpResult[1].Trim(); }
this.Response.ResponseStatusDescription = httpResult[2].Trim(); else if (this.Response.ResponseStatusCode.Equals("417")
&& this.Response.ResponseStatusDescription.ToLower().Equals("expectation failed"))
//For HTTP 1.1 comptibility server may send expect-continue even if not asked for it in request {
if (this.Response.ResponseStatusCode.Equals("100") //read next line after expectation failed response
&& this.Response.ResponseStatusDescription.ToLower().Equals("continue")) this.Response.ExpectationFailed = true;
{ this.Response.ResponseStatusCode = null;
//Read the next line after 100-continue await ServerConnection.StreamReader.ReadLineAsync();
this.Response.Is100Continue = true; //now receive response
this.Response.ResponseStatusCode = null; await ReceiveResponse();
await ServerConnection.StreamReader.ReadLineAsync(); return;
//now receive response }
await ReceiveResponse();
return; //Read the Response headers
} //Read the response headers in to unique and non-unique header collections
else if (this.Response.ResponseStatusCode.Equals("417") string tmpLine;
&& this.Response.ResponseStatusDescription.ToLower().Equals("expectation failed")) while (!string.IsNullOrEmpty(tmpLine = await ServerConnection.StreamReader.ReadLineAsync()))
{ {
//read next line after expectation failed response var header = tmpLine.Split(ProxyConstants.ColonSplit, 2);
this.Response.ExpectationFailed = true;
this.Response.ResponseStatusCode = null; var newHeader = new HttpHeader(header[0], header[1]);
await ServerConnection.StreamReader.ReadLineAsync();
//now receive response //if header exist in non-unique header collection add it there
await ReceiveResponse(); if (Response.NonUniqueResponseHeaders.ContainsKey(newHeader.Name))
return; {
} Response.NonUniqueResponseHeaders[newHeader.Name].Add(newHeader);
}
//Read the Response headers //if header is alread in unique header collection then move both to non-unique collection
//Read the response headers in to unique and non-unique header collections else if (Response.ResponseHeaders.ContainsKey(newHeader.Name))
string tmpLine; {
while (!string.IsNullOrEmpty(tmpLine = await ServerConnection.StreamReader.ReadLineAsync())) var existing = Response.ResponseHeaders[newHeader.Name];
{
var header = tmpLine.Split(ProxyConstants.ColonSplit, 2); var nonUniqueHeaders = new List<HttpHeader> {existing, newHeader};
var newHeader = new HttpHeader(header[0], header[1]); Response.NonUniqueResponseHeaders.Add(newHeader.Name, nonUniqueHeaders);
Response.ResponseHeaders.Remove(newHeader.Name);
//if header exist in non-unique header collection add it there }
if (Response.NonUniqueResponseHeaders.ContainsKey(newHeader.Name)) //add to unique header collection
{ else
Response.NonUniqueResponseHeaders[newHeader.Name].Add(newHeader); {
} Response.ResponseHeaders.Add(newHeader.Name, newHeader);
//if header is alread in unique header collection then move both to non-unique collection }
else if (Response.ResponseHeaders.ContainsKey(newHeader.Name)) }
{ }
var existing = Response.ResponseHeaders[newHeader.Name]; }
var nonUniqueHeaders = new List<HttpHeader> {existing, newHeader};
Response.NonUniqueResponseHeaders.Add(newHeader.Name, nonUniqueHeaders);
Response.ResponseHeaders.Remove(newHeader.Name);
}
//add to unique header collection
else
{
Response.ResponseHeaders.Add(newHeader.Name, newHeader);
}
}
}
}
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Security; using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Security.Authentication; using System.Security.Authentication;
...@@ -27,9 +28,14 @@ namespace Titanium.Web.Proxy ...@@ -27,9 +28,14 @@ namespace Titanium.Web.Proxy
//This is called when client is aware of proxy //This is called when client is aware of proxy
//So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy //So for HTTPS requests client would send CONNECT header to negotiate a secure tcp tunnel via proxy
private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient client) private async Task HandleClient(ExplicitProxyEndPoint endPoint, TcpClient tcpClient)
{ {
Stream clientStream = client.GetStream(); var tcpRow = TcpHelper.GetExtendedTcpTable().FirstOrDefault(
row => row.LocalEndPoint.Port == ((IPEndPoint) tcpClient.Client.RemoteEndPoint).Port);
var processId = tcpRow?.ProcessId ?? 0;
Stream clientStream = tcpClient.GetStream();
clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000; clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000;
clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000; clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000;
...@@ -156,7 +162,7 @@ namespace Titanium.Web.Proxy ...@@ -156,7 +162,7 @@ namespace Titanium.Web.Proxy
return; return;
} }
//Now create the request //Now create the request
await HandleHttpSessionRequest(client, httpCmd, clientStream, clientStreamReader, clientStreamWriter, await HandleHttpSessionRequest(tcpClient, processId, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, connectRequestHeaders, null, null); httpRemoteUri.Scheme == Uri.UriSchemeHttps ? httpRemoteUri.Host : null, connectRequestHeaders, null, null);
} }
catch (Exception ex) catch (Exception ex)
...@@ -169,7 +175,12 @@ namespace Titanium.Web.Proxy ...@@ -169,7 +175,12 @@ namespace Titanium.Web.Proxy
//So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client //So for HTTPS requests we would start SSL negotiation right away without expecting a CONNECT request from client
private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient) private async Task HandleClient(TransparentProxyEndPoint endPoint, TcpClient tcpClient)
{ {
Stream clientStream = tcpClient.GetStream(); var tcpRow = TcpHelper.GetExtendedTcpTable().FirstOrDefault(
row => row.LocalEndPoint.Port == ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Port);
var processId = tcpRow?.ProcessId ?? 0;
Stream clientStream = tcpClient.GetStream();
clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000; clientStream.ReadTimeout = ConnectionTimeOutSeconds * 1000;
clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000; clientStream.WriteTimeout = ConnectionTimeOutSeconds * 1000;
...@@ -198,12 +209,9 @@ namespace Titanium.Web.Proxy ...@@ -198,12 +209,9 @@ namespace Titanium.Web.Proxy
} }
catch (Exception) catch (Exception)
{ {
if (sslStream != null) sslStream.Dispose();
{
sslStream.Dispose();
}
Dispose(sslStream, clientStreamReader, clientStreamWriter, null); Dispose(sslStream, clientStreamReader, clientStreamWriter, null);
return; return;
} }
clientStream = sslStream; clientStream = sslStream;
...@@ -218,7 +226,7 @@ namespace Titanium.Web.Proxy ...@@ -218,7 +226,7 @@ namespace Titanium.Web.Proxy
var httpCmd = await clientStreamReader.ReadLineAsync(); var httpCmd = await clientStreamReader.ReadLineAsync();
//Now create the request //Now create the request
await HandleHttpSessionRequest(tcpClient, httpCmd, clientStream, clientStreamReader, clientStreamWriter, await HandleHttpSessionRequest(tcpClient, processId, httpCmd, clientStream, clientStreamReader, clientStreamWriter,
endPoint.EnableSsl ? endPoint.GenericCertificateName : null, null); endPoint.EnableSsl ? endPoint.GenericCertificateName : null, null);
} }
...@@ -353,13 +361,14 @@ namespace Titanium.Web.Proxy ...@@ -353,13 +361,14 @@ namespace Titanium.Web.Proxy
/// This is the core request handler method for a particular connection from client /// This is the core request handler method for a particular connection from client
/// </summary> /// </summary>
/// <param name="client"></param> /// <param name="client"></param>
/// <param name="processId"></param>
/// <param name="httpCmd"></param> /// <param name="httpCmd"></param>
/// <param name="clientStream"></param> /// <param name="clientStream"></param>
/// <param name="clientStreamReader"></param> /// <param name="clientStreamReader"></param>
/// <param name="clientStreamWriter"></param> /// <param name="clientStreamWriter"></param>
/// <param name="httpsHostName"></param> /// <param name="httpsHostName"></param>
/// <returns></returns> /// <returns></returns>
private async Task HandleHttpSessionRequest(TcpClient client, string httpCmd, Stream clientStream, private async Task HandleHttpSessionRequest(TcpClient client, int processId, string httpCmd, Stream clientStream,
CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName, List<HttpHeader> connectHeaders, ExternalProxy customUpStreamHttpProxy = null, ExternalProxy customUpStreamHttpsProxy = null) CustomBinaryReader clientStreamReader, StreamWriter clientStreamWriter, string httpsHostName, List<HttpHeader> connectHeaders, ExternalProxy customUpStreamHttpProxy = null, ExternalProxy customUpStreamHttpsProxy = null)
{ {
TcpConnection connection = null; TcpConnection connection = null;
...@@ -377,7 +386,9 @@ namespace Titanium.Web.Proxy ...@@ -377,7 +386,9 @@ namespace Titanium.Web.Proxy
var args = new SessionEventArgs(BUFFER_SIZE, HandleHttpSessionResponse); var args = new SessionEventArgs(BUFFER_SIZE, HandleHttpSessionResponse);
args.ProxyClient.TcpClient = client; args.ProxyClient.TcpClient = client;
args.WebSession.ConnectHeaders = connectHeaders; args.WebSession.ConnectHeaders = connectHeaders;
try args.WebSession.ProcessId = processId;
try
{ {
//break up the line into three components (method, remote URL & Http Version) //break up the line into three components (method, remote URL & Http Version)
var httpCmdSplit = httpCmd.Split(ProxyConstants.SpaceSplit, 3); var httpCmdSplit = httpCmd.Split(ProxyConstants.SpaceSplit, 3);
......
using System.Net; using System.Net;
using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Helpers;
namespace Titanium.Web.Proxy.Tcp namespace Titanium.Web.Proxy.Tcp
{ {
/// <summary> /// <summary>
/// Represents a managed interface of IP Helper API TcpRow struct /// Represents a managed interface of IP Helper API TcpRow struct
/// <see cref="http://msdn2.microsoft.com/en-us/library/aa366913.aspx"/> /// <see cref="http://msdn2.microsoft.com/en-us/library/aa366913.aspx"/>
/// </summary> /// </summary>
internal class TcpRow internal class TcpRow
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TcpRow"/> class. /// Initializes a new instance of the <see cref="TcpRow"/> class.
/// </summary> /// </summary>
/// <param name="tcpRow">TcpRow struct.</param> /// <param name="tcpRow">TcpRow struct.</param>
public TcpRow(NativeMethods.TcpRow tcpRow) public TcpRow(NativeMethods.TcpRow tcpRow)
{ {
ProcessId = tcpRow.owningPid; ProcessId = tcpRow.owningPid;
int localPort = (tcpRow.localPort1 << 8) + (tcpRow.localPort2) + (tcpRow.localPort3 << 24) + (tcpRow.localPort4 << 16); int localPort = (tcpRow.localPort1 << 8) + (tcpRow.localPort2) + (tcpRow.localPort3 << 24) + (tcpRow.localPort4 << 16);
long localAddress = tcpRow.localAddr; long localAddress = tcpRow.localAddr;
LocalEndPoint = new IPEndPoint(localAddress, localPort); LocalEndPoint = new IPEndPoint(localAddress, localPort);
}
int remotePort = (tcpRow.remotePort1 << 8) + (tcpRow.remotePort2) + (tcpRow.remotePort3 << 24) + (tcpRow.remotePort4 << 16);
/// <summary> long remoteAddress = tcpRow.remoteAddr;
/// Gets the local end point. RemoteEndPoint = new IPEndPoint(remoteAddress, remotePort);
/// </summary> }
public IPEndPoint LocalEndPoint { get; private set; }
/// <summary>
/// <summary> /// Gets the local end point.
/// Gets the process identifier. /// </summary>
/// </summary> public IPEndPoint LocalEndPoint { get; private set; }
public int ProcessId { get; private set; }
} /// <summary>
/// Gets the remote end point.
/// </summary>
public IPEndPoint RemoteEndPoint { get; private set; }
/// <summary>
/// Gets the process identifier.
/// </summary>
public int ProcessId { get; private set; }
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment